diff --git a/plugins/web-ui/package-lock.json b/plugins/web-ui/package-lock.json index 5bd6f43ed..49d6b5535 100644 --- a/plugins/web-ui/package-lock.json +++ b/plugins/web-ui/package-lock.json @@ -664,6 +664,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -712,6 +713,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1333,6 +1335,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@mariozechner/mini-lit/-/mini-lit-0.2.1.tgz", "integrity": "sha512-u300euLgCsDDlb8o2Wbz+55eSJga5X2vB58s9XBuFIr2Bi3iI+GMR7t/NYo/O6Vr6obXShXgYjR3SRUJVgo+kQ==", + "peer": true, "dependencies": { "@preact/signals-core": "^1.12.1", "class-variance-authority": "^0.7.1", @@ -2952,6 +2955,7 @@ "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", @@ -3189,6 +3193,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3449,6 +3454,7 @@ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -3829,6 +3835,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/plugins/web-ui/src/chat.ts b/plugins/web-ui/src/chat.ts index c21e6a185..c57445d6c 100644 --- a/plugins/web-ui/src/chat.ts +++ b/plugins/web-ui/src/chat.ts @@ -128,6 +128,7 @@ import { createForkOriginController, forkOriginView } from "./fork-origin"; import { base64ToBytes } from "./paste-text"; import { tip } from "./tooltip"; import { workSeconds, workedLabel } from "./work-duration"; +import { markClampedPrompts } from "./prompt-clamp"; import { decorateTextCodeBlocks, normalizePlainTextFences } from "./text-code"; import { createTranscriptViewport } from "./transcript-viewport"; @@ -150,6 +151,7 @@ interface SettledRowKey { speakerLabel: string | undefined; edited: boolean; deleted: boolean; + expanded: boolean; tpl: TemplateResult | typeof nothing; } const settledRowCache = new WeakMap(); @@ -219,6 +221,7 @@ export function createChatSurface( inheritedLoaded: false, pins: [] as SessionPin[], pinsExpanded: false, + expandedPrompt: null as number | null, labelSpeakers: false, }; @@ -398,6 +401,7 @@ export function createChatSurface( chatState.earlierCount = 0; chatState.loadingEarlier = false; chatState.pins = []; + chatState.expandedPrompt = null; chatState.host = document.createElement("div"); chatState.host.className = "custom-chat"; @@ -929,6 +933,7 @@ export function createChatSurface( if (!sameSession) { chatState.inheritedExpanded = false; chatState.pins = []; + chatState.expandedPrompt = null; } syncLocation(); @@ -1029,6 +1034,7 @@ export function createChatSurface( ); requestAnimationFrame(() => { decorateTextCodeBlocks(host); + markClampedPrompts(host); if (host.isConnected) transcriptViewport.sync(host.querySelector(".chat-scroll")); }); }; @@ -1069,6 +1075,12 @@ export function createChatSurface( else readonlyRedraw?.(); } + function togglePromptExpanded(index: number): void { + chatState.expandedPrompt = chatState.expandedPrompt === index ? null : index; + if (chatState.agent) drawActiveChat(chatState.agent); + else readonlyRedraw?.(); + } + function linkifiedText(text: string): TemplateResult { return html`${splitLinks(text).map((seg) => seg.kind === "link" @@ -1286,7 +1298,10 @@ export function createChatSurface( chatState.host, ); decorateStreamingTail(); - requestAnimationFrame(() => decorateTextCodeBlocks(chatState.host)); + requestAnimationFrame(() => { + decorateTextCodeBlocks(chatState.host); + markClampedPrompts(chatState.host); + }); ctx.composer.resizeComposer(); scrollTranscript(opts.forceScroll); postCurrentPaneState(); @@ -1422,6 +1437,7 @@ export function createChatSurface( const speakerLabel = speakerLabelFor(message); const edited = Boolean((message as { edited?: boolean }).edited); const deleted = Boolean((message as { deleted?: boolean }).deleted); + const expanded = chatState.expandedPrompt === index; const hit = settledRowCache.get(message as object); if ( hit && @@ -1437,7 +1453,8 @@ export function createChatSurface( hit.forkable === forkable && hit.speakerLabel === speakerLabel && hit.edited === edited && - hit.deleted === deleted + hit.deleted === deleted && + hit.expanded === expanded ) { return hit.tpl; } @@ -1456,6 +1473,7 @@ export function createChatSurface( speakerLabel, edited, deleted, + expanded, tpl, }); return tpl; @@ -1476,10 +1494,21 @@ export function createChatSurface(
${steered ? html`
↪ steered the running task
` : nothing} ${speaker ? html`
${speaker}
` : nothing} -
+
${isReadOnlySlackView() ? slackWireBubble(messageText(message)) : markdown(messageText(message))} ${attachments.length ? html`
${attachments.map(userAttachmentBadge)}
` : nothing} ${edited || deleted ? html`(${deleted ? "deleted" : "edited"})` : nothing} +
${ sendFailure diff --git a/plugins/web-ui/src/contexts.ts b/plugins/web-ui/src/contexts.ts index 06455c4bf..88ce60c53 100644 --- a/plugins/web-ui/src/contexts.ts +++ b/plugins/web-ui/src/contexts.ts @@ -2,8 +2,6 @@ import { html, nothing, render, type TemplateResult } from "lit"; import { ArrowLeft, Boxes, - Check, - ChevronDown, Folder, FolderPlus, Hash, @@ -27,7 +25,7 @@ import { } from "./core-bridge"; import { UI_BASE } from "./deep-link"; import { errMessage } from "../../chassis/src/errors"; -import { actionSnippet, closeFormMenus, fieldSelect, formatBytes, icon, initials, relTime, toggleFormMenu } from "./ui"; +import { actionSnippet, fieldSelect, formatBytes, icon, initials, menuSelect, relTime } from "./ui"; import { appState, replacePanePreservingFocus, switchView, syncUrlFromState } from "./shell"; import { startNewChat } from "./sessions"; import { groupDmTitle, openSession, refreshSessions, sessionsState, slackLogo, surfaceOf } from "./sessions"; @@ -274,38 +272,21 @@ export function scopeChip(scopeId: string | null, fallbackName?: string | null): } export function scopeFilterControl(current: string | null, onSelect: (scopeId: string | null) => void): TemplateResult { - const label = current ? metaForScope(current).title : "All contexts"; - const option = (scopeId: string | null, text: string, glyph: IconNode) => { - const active = (current ?? null) === scopeId; - return html` - - `; - }; - return html` - - `; + return menuSelect({ + value: current, + prefix: "Filter by: ", + ariaLabel: "Filter by context", + className: "scope-filter", + onSelect, + options: [ + { value: null, label: "All contexts", glyph: Boxes }, + ...contextsState.list.map((c) => ({ + value: c.scopeId, + label: contextMeta(c).title, + glyph: contextMeta(c).glyph, + })), + ], + }); } function sessionsIn(scopeId: string): CoreSession[] { diff --git a/plugins/web-ui/src/crons.ts b/plugins/web-ui/src/crons.ts index 1061ae5ee..32ee7c42b 100644 --- a/plugins/web-ui/src/crons.ts +++ b/plugins/web-ui/src/crons.ts @@ -362,7 +362,7 @@ function cronPageRow(c: CronView, mine: boolean): TemplateResult { function cronRowActions(c: CronView): TemplateResult { let stateAction = html`
`; } -function syncAskEnabled(box: HTMLTextAreaElement): void { - const composer = box.closest(".inbox-chat-composer"); - const send = composer?.querySelector(".inbox-chat-send"); - if (send) send.disabled = !box.value.trim(); - composer?.classList.toggle("has-text", !!box.value.trim()); -} - export function chatTpl(item: InboxItem): TemplateResult { const busy = chatting.has(item.id); const pending = chatDrafts.get(item.id) ?? ""; const submit = (el: HTMLTextAreaElement): void => { + if (busy) return; const text = el.value; el.value = ""; + autosizeChatInput(el); void askAgent(item, text); }; const empty = item.thread.length === 0; @@ -774,7 +784,22 @@ export function chatTpl(item: InboxItem): TemplateResult {
${ empty - ? html`

What should I change?

` + ? html`
+

What should I change?

+
+ ${DRAFT_SUGGESTIONS.map( + (prompt) => + html``, + )} +
+
` : html`
${item.thread.map( (m) => @@ -784,18 +809,19 @@ export function chatTpl(item: InboxItem): TemplateResult { )}
` } - ${busy ? html`
Thinking…
` : nothing} + ${busy ? html`
${workingWave()}Thinking…
` : nothing}