diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 01:04:01 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 01:04:01 +0900 |
| commit | c0fa581c8ac563c916948f44596ef361817dc580 (patch) | |
| tree | 87cff17051cd4263a6b67a28ecf6e16fb1e138db | |
| parent | cce2380c116b9052f3839a0bc3f2bc4191f9afbd (diff) | |
| download | dispatch-web-c0fa581c8ac563c916948f44596ef361817dc580.tar.gz dispatch-web-c0fa581c8ac563c916948f44596ef361817dc580.zip | |
fix(chat): keep thinking <details> open while streaming
ChatView keyed the transcript each-block by object identity, but core/chunks
returns new RenderedChunk objects per delta, so Svelte recreated each
<article>/<details> every frame — an opened Thinking element snapped shut on
the next token. Key by stable identity instead (c${seq} for committed, p${i}
for append-only provisional) so streaming reuses the DOM. Adds a regression
test that the <details> stays open across a streaming update.
Verified: svelte-check 0/0, vitest 222, biome clean, build ok.
| -rw-r--r-- | src/features/chat/ui.test.ts | 34 | ||||
| -rw-r--r-- | src/features/chat/ui/ChatView.svelte | 2 |
2 files changed, 35 insertions, 1 deletions
diff --git a/src/features/chat/ui.test.ts b/src/features/chat/ui.test.ts index c4793a0..aebb97c 100644 --- a/src/features/chat/ui.test.ts +++ b/src/features/chat/ui.test.ts @@ -155,6 +155,40 @@ describe("ChatView", () => { expect(log).toBeInTheDocument(); expect(log.children).toHaveLength(0); }); + + it("thinking <details> stays open across a streaming update", async () => { + const initial: RenderedChunk[] = [ + { + seq: null, + role: "assistant", + chunk: { type: "thinking", text: "Let me think..." }, + provisional: true, + }, + ]; + + const { rerender } = render(ChatView, { props: { chunks: initial } }); + + const details = screen.getByText("Thinking").closest("details"); + expect(details).not.toBeNull(); + expect(details).not.toHaveAttribute("open"); + if (details) details.open = true; + expect(details).toHaveAttribute("open"); + + const updated: RenderedChunk[] = [ + { + seq: null, + role: "assistant", + chunk: { type: "thinking", text: "Let me think... step by step" }, + provisional: true, + }, + ]; + await rerender({ chunks: updated }); + + const detailsAfter = screen.getByText("Thinking").closest("details"); + expect(detailsAfter).not.toBeNull(); + expect(detailsAfter).toHaveAttribute("open"); + expect(detailsAfter).toHaveTextContent("Let me think... step by step"); + }); }); describe("Composer", () => { diff --git a/src/features/chat/ui/ChatView.svelte b/src/features/chat/ui/ChatView.svelte index a7c39cc..ce66798 100644 --- a/src/features/chat/ui/ChatView.svelte +++ b/src/features/chat/ui/ChatView.svelte @@ -5,7 +5,7 @@ </script> <div class="chat-transcript" role="log" aria-live="polite"> - {#each chunks as rendered (rendered)} + {#each chunks as rendered, i (rendered.seq != null ? `c${rendered.seq}` : `p${i}`)} <article class="message message--{rendered.role}" class:message--provisional={rendered.provisional} |
