diff options
| author | Adam Malczewski <[email protected]> | 2026-05-27 18:35:18 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-27 18:35:18 +0900 |
| commit | ca6ee91c5e1167b1929eedbb96c76dfa24e7d026 (patch) | |
| tree | bc23acac2e7caaf2e59eacbc21bfc9b41f3c1458 /packages/frontend/src/lib/components | |
| parent | da57842686ebfd157396551fc76d0c18f7676335 (diff) | |
| download | dispatch-ca6ee91c5e1167b1929eedbb96c76dfa24e7d026.tar.gz dispatch-ca6ee91c5e1167b1929eedbb96c76dfa24e7d026.zip | |
refactor: ChatMessage.chunks[] union — interleaved thinking, tool batching, error/system chunks
Diffstat (limited to 'packages/frontend/src/lib/components')
| -rw-r--r-- | packages/frontend/src/lib/components/ChatMessage.svelte | 76 | ||||
| -rw-r--r-- | packages/frontend/src/lib/components/ToolCallDisplay.svelte | 4 |
2 files changed, 55 insertions, 25 deletions
diff --git a/packages/frontend/src/lib/components/ChatMessage.svelte b/packages/frontend/src/lib/components/ChatMessage.svelte index c6b6034..0c85349 100644 --- a/packages/frontend/src/lib/components/ChatMessage.svelte +++ b/packages/frontend/src/lib/components/ChatMessage.svelte @@ -1,7 +1,7 @@ <script lang="ts"> import { appSettings } from "../settings.svelte.js"; import { tabStore } from "../tabs.svelte.js"; -import type { ChatMessage } from "../types.js"; +import type { ChatMessage, Chunk, SystemChunkKind } from "../types.js"; import MarkdownRenderer from "./MarkdownRenderer.svelte"; import ToolCallDisplay from "./ToolCallDisplay.svelte"; @@ -21,37 +21,67 @@ function cancelQueued() { void tabStore.cancelQueuedMessage(tabId, queuedMessageId); } } + +function chunkKey(chunk: Chunk, i: number): string { + if (chunk.type === "tool-batch") { + // Stable-ish: first call id + count keeps re-renders sane while streaming. + return `tb-${chunk.calls[0]?.id ?? i}-${chunk.calls.length}`; + } + return `${chunk.type}-${i}`; +} + +const SYSTEM_KIND_LABEL: Record<SystemChunkKind, string> = { + notice: "Notice", + "model-changed": "Model changed", + "config-reload": "Config reload", + cancelled: "Cancelled", +}; </script> -{#if isSystem} - <div class="flex justify-center my-2"> - <div class="badge badge-ghost gap-1 text-xs opacity-60"> - {#each message.content as segment} - {#if segment.type === "text"} - {segment.text} - {/if} - {/each} - </div> - </div> -{:else} -<div class="chat chat-start mb-2 [&>.chat-bubble]:max-w-full {isQueued ? 'opacity-60' : ''}"> - <div class="chat-bubble break-words {isUser ? 'chat-bubble-primary w-fit' : 'bg-transparent w-full'}"> - {#if message.thinking} +{#snippet renderChunks(chunks: Chunk[], streaming: boolean | undefined)} + {#each chunks as chunk, i (chunkKey(chunk, i))} + {#if chunk.type === "text"} + <MarkdownRenderer text={chunk.text} {streaming} /> + {:else if chunk.type === "thinking"} <div class="collapse collapse-arrow mb-2 p-1"> <input type="checkbox" checked={appSettings.autoExpandThinking} /> <div class="collapse-title text-sm opacity-60 italic py-0 pl-0 pr-8 min-h-0">Thinking...</div> <div class="collapse-content text-sm opacity-60 italic p-0"> - <p class="whitespace-pre-wrap mt-1">{message.thinking}</p> + <p class="whitespace-pre-wrap mt-1">{chunk.text}</p> </div> </div> + {:else if chunk.type === "tool-batch"} + {#each chunk.calls as call (call.id)} + <ToolCallDisplay toolCall={call} /> + {/each} + {:else if chunk.type === "error"} + <div class="alert alert-error my-2 py-2 px-3 text-sm rounded border border-error/60 bg-error/10 text-error"> + <div class="flex flex-col gap-0.5 w-full"> + <span class="break-words">{chunk.message}</span> + {#if chunk.statusCode !== undefined} + <span class="text-xs opacity-70">status {chunk.statusCode}</span> + {/if} + </div> + </div> + {:else if chunk.type === "system"} + <div class="my-1 text-xs italic opacity-50 flex gap-1 items-baseline"> + <span class="font-semibold not-italic">{SYSTEM_KIND_LABEL[chunk.kind]}:</span> + <span class="break-words">{chunk.text}</span> + </div> {/if} - {#each message.content as segment, i (segment.type === "tool-call" ? segment.id : i)} - {#if segment.type === "text"} - <MarkdownRenderer text={segment.text} streaming={message.isStreaming} /> - {:else if segment.type === "tool-call"} - <ToolCallDisplay toolCall={segment} /> - {/if} - {/each} + {/each} +{/snippet} + +{#if isSystem} + <div class="flex justify-center my-2 px-4"> + <div class="max-w-full text-center"> + {@render renderChunks(message.chunks, false)} + </div> + </div> +{:else} +<div class="chat chat-start mb-2 [&>.chat-bubble]:max-w-full {isQueued ? 'opacity-60' : ''}"> + <div class="chat-bubble break-words {isUser ? 'chat-bubble-primary w-fit' : 'bg-transparent w-full'}"> + {@render renderChunks(message.chunks, message.isStreaming)} {#if message.isStreaming} <span class="inline-block w-1.5 h-4 bg-current animate-pulse ml-0.5 align-middle rounded-sm"></span> {/if} diff --git a/packages/frontend/src/lib/components/ToolCallDisplay.svelte b/packages/frontend/src/lib/components/ToolCallDisplay.svelte index 7c7aef6..1b4ebca 100644 --- a/packages/frontend/src/lib/components/ToolCallDisplay.svelte +++ b/packages/frontend/src/lib/components/ToolCallDisplay.svelte @@ -1,8 +1,8 @@ <script lang="ts"> import { tabStore } from "../tabs.svelte.js"; -import type { ToolCallDisplay } from "../types.js"; +import type { ToolBatchEntry } from "../types.js"; -const { toolCall }: { toolCall: ToolCallDisplay } = $props(); +const { toolCall }: { toolCall: ToolBatchEntry } = $props(); let isExpanded = $state(false); |
