diff options
Diffstat (limited to 'packages/frontend/src')
| -rw-r--r-- | packages/frontend/src/lib/components/ChatMessage.svelte | 58 | ||||
| -rw-r--r-- | packages/frontend/src/lib/tabs.svelte.ts | 1 | ||||
| -rw-r--r-- | packages/frontend/src/lib/types.ts | 9 |
3 files changed, 62 insertions, 6 deletions
diff --git a/packages/frontend/src/lib/components/ChatMessage.svelte b/packages/frontend/src/lib/components/ChatMessage.svelte index 0c85349..54e99c8 100644 --- a/packages/frontend/src/lib/components/ChatMessage.svelte +++ b/packages/frontend/src/lib/components/ChatMessage.svelte @@ -36,6 +36,42 @@ const SYSTEM_KIND_LABEL: Record<SystemChunkKind, string> = { "config-reload": "Config reload", cancelled: "Cancelled", }; + +/** + * Returns true if the given chunk has visible content worth rendering. + * Used by `hasRenderableContent` to suppress empty assistant bubbles. + * + * Note: `ThinkingChunk.metadata` is intentionally excluded — it is + * internal wire data (Anthropic's providerMetadata / signature) and + * must never appear in the UI. + */ +function chunkHasRenderableContent(chunk: Chunk): boolean { + switch (chunk.type) { + case "text": + return chunk.text.length > 0; + case "thinking": + return chunk.text.length > 0; + case "tool-batch": + return chunk.calls.length > 0; + case "error": + return true; + case "system": + return true; + } +} + +/** + * True when the assistant bubble has something worth showing. + * Guards the assistant render path so we don't emit an empty box + * (e.g. a message that only had empty/signature-only thinking blocks + * from Anthropic adaptive thinking mode). + * + * Streaming messages always have renderable content — the cursor + * needs somewhere to live. + */ +const hasRenderableContent = $derived( + message.isStreaming === true || message.chunks.some(chunkHasRenderableContent), +); </script> {#snippet renderChunks(chunks: Chunk[], streaming: boolean | undefined)} @@ -43,13 +79,18 @@ const SYSTEM_KIND_LABEL: Record<SystemChunkKind, string> = { {#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">{chunk.text}</p> + <!-- Skip empty thinking chunks: Anthropic adaptive thinking can emit + a reasoning-end with a signature but no thinking_delta content. + The metadata is internal wire data — never displayed. --> + {#if chunk.text.length > 0} + <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">{chunk.text}</p> + </div> </div> - </div> + {/if} {:else if chunk.type === "tool-batch"} {#each chunk.calls as call (call.id)} <ToolCallDisplay toolCall={call} /> @@ -78,6 +119,11 @@ const SYSTEM_KIND_LABEL: Record<SystemChunkKind, string> = { {@render renderChunks(message.chunks, false)} </div> </div> +{:else if !isUser && !hasRenderableContent} + <!-- Empty assistant message — no renderable chunks and not streaming. + Suppressed to avoid an empty bubble (e.g. a turn that produced + only empty/signature-only thinking blocks from Anthropic adaptive + thinking mode, or a done event with no content). --> {: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'}"> diff --git a/packages/frontend/src/lib/tabs.svelte.ts b/packages/frontend/src/lib/tabs.svelte.ts index b07d37a..1d60e0b 100644 --- a/packages/frontend/src/lib/tabs.svelte.ts +++ b/packages/frontend/src/lib/tabs.svelte.ts @@ -445,6 +445,7 @@ export function createTabStore() { break; } case "reasoning-delta": + case "reasoning-end": case "text-delta": case "tool-call": case "tool-result": diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts index 1043f64..6051810 100644 --- a/packages/frontend/src/lib/types.ts +++ b/packages/frontend/src/lib/types.ts @@ -28,6 +28,14 @@ export interface TextChunk { export interface ThinkingChunk { type: "thinking"; text: string; + /** + * Mirror of core. Anthropic's `providerMetadata` blob captured from + * the v6 `reasoning-end` stream event. Present once the backend has + * sealed the chunk; absent for in-flight thinking or for non-Anthropic + * models. The UI doesn't render this — it lives here for wire-format + * symmetry with the persisted chunk shape. + */ + metadata?: Record<string, unknown>; } export interface ToolBatchChunk { @@ -77,6 +85,7 @@ export type AgentEvent = | { type: "statuses"; statuses: Record<string, "idle" | "running" | "error"> } | { type: "text-delta"; delta: string } | { type: "reasoning-delta"; delta: string } + | { type: "reasoning-end"; metadata?: Record<string, unknown> } | { type: "tool-call"; toolCall: { |
