summaryrefslogtreecommitdiffhomepage
path: root/src/features
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 01:04:01 +0900
committerAdam Malczewski <[email protected]>2026-06-07 01:04:01 +0900
commitc0fa581c8ac563c916948f44596ef361817dc580 (patch)
tree87cff17051cd4263a6b67a28ecf6e16fb1e138db /src/features
parentcce2380c116b9052f3839a0bc3f2bc4191f9afbd (diff)
downloaddispatch-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.
Diffstat (limited to 'src/features')
-rw-r--r--src/features/chat/ui.test.ts34
-rw-r--r--src/features/chat/ui/ChatView.svelte2
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}