diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index 0ca7ca671..bec69a724 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -1,10 +1,9 @@ import { html, nothing } from "lit"; import { repeat } from "lit/directives/repeat.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; - -import type { SessionsListResult } from "../types"; import { toSanitizedMarkdownHtml } from "../markdown"; -import { resolveToolDisplay, formatToolDetail } from "../tool-display"; +import { formatToolDetail, resolveToolDisplay } from "../tool-display"; +import type { SessionsListResult } from "../types"; export type ChatProps = { sessionKey: string; @@ -34,7 +33,7 @@ export function renderChat(props: ChatProps) { const canCompose = props.connected && !props.sending; const sessionOptions = resolveSessionOptions(props.sessionKey, props.sessions); const composePlaceholder = props.connected - ? "Message (⌘↩ to send)" + ? "Message (Shift+↩ for line breaks)" : "Connect to the gateway to start chatting…"; return html` @@ -53,7 +52,7 @@ export function renderChat(props: ChatProps) { (entry) => html``, + ` )} @@ -70,33 +69,41 @@ export function renderChat(props: ChatProps) { - ${props.disabledReason - ? html`
+ ${ + props.disabledReason + ? html`
${props.disabledReason}
` - : nothing} + : nothing + } - ${props.error - ? html`
${props.error}
` - : nothing} + ${ + props.error + ? html`
${props.error}
` + : nothing + }
${props.loading ? html`
Loading chat…
` : nothing} - ${repeat(buildChatItems(props), (item) => item.key, (item) => { - if (item.kind === "reading-indicator") return renderReadingIndicator(); - if (item.kind === "stream") { - return renderMessage( - { - role: "assistant", - content: [{ type: "text", text: item.text }], - timestamp: item.startedAt, - }, - props, - { streaming: true }, - ); + ${repeat( + buildChatItems(props), + (item) => item.key, + (item) => { + if (item.kind === "reading-indicator") return renderReadingIndicator(); + if (item.kind === "stream") { + return renderMessage( + { + role: "assistant", + content: [{ type: "text", text: item.text }], + timestamp: item.startedAt, + }, + props, + { streaming: true } + ); + } + return renderMessage(item.message, props); } - return renderMessage(item.message, props); - })} + )}
@@ -107,12 +114,11 @@ export function renderChat(props: ChatProps) { ?disabled=${!props.connected} @keydown=${(e: KeyboardEvent) => { if (e.key !== "Enter") return; - if (!e.metaKey && !e.ctrlKey) return; + if (e.shiftKey) return; // Allow Shift+Enter for line breaks e.preventDefault(); if (canCompose) props.onSend(); }} - @input=${(e: Event) => - props.onDraftChange((e.target as HTMLTextAreaElement).value)} + @input=${(e: Event) => props.onDraftChange((e.target as HTMLTextAreaElement).value)} placeholder=${composePlaceholder} > @@ -231,16 +237,11 @@ type SessionOption = { displayName?: string; }; -function resolveSessionOptions( - currentKey: string, - sessions: SessionsListResult | null, -) { +function resolveSessionOptions(currentKey: string, sessions: SessionsListResult | null) { const now = Date.now(); const cutoff = now - 24 * 60 * 60 * 1000; - const entries = Array.isArray(sessions?.sessions) ? sessions?.sessions ?? [] : []; - const sorted = [...entries].sort( - (a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0), - ); + const entries = Array.isArray(sessions?.sessions) ? (sessions?.sessions ?? []) : []; + const sorted = [...entries].sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)); const recent: SessionOption[] = []; const seen = new Set(); for (const entry of sorted) { @@ -292,7 +293,7 @@ function renderReadingIndicator() { function renderMessage( message: unknown, props?: Pick, - opts?: { streaming?: boolean }, + opts?: { streaming?: boolean } ) { const m = message as Record; const role = typeof m.role === "string" ? m.role : "unknown"; @@ -314,7 +315,7 @@ function renderMessage( const markdown = display?.kind === "json" ? ["```json", display.value, "```"].join("\n") - : display?.value ?? null; + : (display?.value ?? null); const timestamp = typeof m.timestamp === "number" ? new Date(m.timestamp).toLocaleTimeString() : ""; @@ -330,9 +331,11 @@ function renderMessage(
- ${markdown - ? html`
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
` - : nothing} + ${ + markdown + ? html`
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
` + : nothing + } ${toolCards.map((card, index) => renderToolCard(card, { id: `${toolCardBase}:${index}`, @@ -340,7 +343,7 @@ function renderMessage( ? props.isToolOutputExpanded(`${toolCardBase}:${index}`) : false, onToggle: props?.onToolOutputToggle, - }), + }) )}
@@ -421,7 +424,7 @@ function renderToolCard( id: string; expanded: boolean; onToggle?: (id: string, expanded: boolean) => void; - }, + } ) { const display = resolveToolDisplay({ name: card.name, args: card.args }); const detail = formatToolDetail(display); @@ -431,11 +434,10 @@ function renderToolCard( return html`
${display.emoji} ${display.label}
- ${detail - ? html`
${detail}
` - : nothing} - ${hasOutput - ? html` + ${detail ? html`
${detail}
` : nothing} + ${ + hasOutput + ? html`
- ${expanded - ? html`
+ ${ + expanded + ? html`
${unsafeHTML(toSanitizedMarkdownHtml(card.text ?? ""))}
` - : nothing} + : nothing + }
` - : nothing} + : nothing + }
`; }