summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-27 18:35:18 +0900
committerAdam Malczewski <[email protected]>2026-05-27 18:35:18 +0900
commitca6ee91c5e1167b1929eedbb96c76dfa24e7d026 (patch)
treebc23acac2e7caaf2e59eacbc21bfc9b41f3c1458 /packages/frontend/src/lib/components
parentda57842686ebfd157396551fc76d0c18f7676335 (diff)
downloaddispatch-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.svelte76
-rw-r--r--packages/frontend/src/lib/components/ToolCallDisplay.svelte4
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);