diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 17:14:40 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 17:14:40 +0900 |
| commit | 2e79dd122e5664353e02e0d33715ae8c1041a379 (patch) | |
| tree | 737822344118e5c1c840b8399a554a1898f07093 /src/core/chunks | |
| parent | c8c86dbc3fd23001cca7904791ab539300ec60f4 (diff) | |
| download | dispatch-web-2e79dd122e5664353e02e0d33715ae8c1041a379.tar.gz dispatch-web-2e79dd122e5664353e02e0d33715ae8c1041a379.zip | |
feat(chat): restyle thinking — visible bubble, collapse, title swap, persisted open
Thinking renders inside a visible rounded-card bubble (like tool calls),
capped to the same max-w-5xl column as assistant text. Uses a DaisyUI
checkbox collapse (no arrow/plus icon) with smooth animation. Title reads
"Thinking" + loading-dots while the model is actively generating, then
flips to "Thoughts" with no dots once done. Open/closed state persists
across the generating→completed→sealed transition via stable ordinal keys
(per-conversation isolation via {#key} in App). Added optional streaming
flag to RenderedChunk (pure selector, only on the accumulating chunk).
Diffstat (limited to 'src/core/chunks')
| -rw-r--r-- | src/core/chunks/reducer.test.ts | 15 | ||||
| -rw-r--r-- | src/core/chunks/selectors.ts | 2 | ||||
| -rw-r--r-- | src/core/chunks/types.ts | 7 |
3 files changed, 23 insertions, 1 deletions
diff --git a/src/core/chunks/reducer.test.ts b/src/core/chunks/reducer.test.ts index 7ecc349..f2f1b75 100644 --- a/src/core/chunks/reducer.test.ts +++ b/src/core/chunks/reducer.test.ts @@ -371,6 +371,21 @@ describe("selectChunks", () => { expect(chunks[0]?.provisional).toBe(true); expect(chunks[0]?.chunk).toEqual({ type: "text", text: "building..." }); }); + + it("marks ONLY the actively-accumulating chunk as streaming", () => { + let s = initialState(); + s = foldEvent(s, turnStart("t1")); + // A flushed-but-still-provisional thinking chunk, then a live accumulating one. + s = foldEvent(s, reasoningDelta("t1", "first thought")); + s = foldEvent(s, toolCall("t1", "tc1", "bash", {})); // flushes the thinking + s = foldEvent(s, textDelta("t1", "now writing")); + const chunks = selectChunks(s); + const thinking = chunks.find((c) => c.chunk.type === "thinking"); + const accumulating = chunks.find((c) => c.streaming === true); + expect(thinking?.streaming).toBeFalsy(); // flushed → not streaming + expect(accumulating?.chunk).toEqual({ type: "text", text: "now writing" }); + expect(chunks.filter((c) => c.streaming === true)).toHaveLength(1); + }); }); describe("selectMessages", () => { diff --git a/src/core/chunks/selectors.ts b/src/core/chunks/selectors.ts index 8fb832f..839ba65 100644 --- a/src/core/chunks/selectors.ts +++ b/src/core/chunks/selectors.ts @@ -18,7 +18,7 @@ export function selectChunks(state: TranscriptState): readonly RenderedChunk[] { state.accumulating.kind === "text" ? { type: "text", text: state.accumulating.text } : { type: "thinking", text: state.accumulating.text }; - result.push({ seq: null, role: "assistant", chunk, provisional: true }); + result.push({ seq: null, role: "assistant", chunk, provisional: true, streaming: true }); } return result; } diff --git a/src/core/chunks/types.ts b/src/core/chunks/types.ts index 3792445..e031ce3 100644 --- a/src/core/chunks/types.ts +++ b/src/core/chunks/types.ts @@ -28,4 +28,11 @@ export interface RenderedChunk { readonly role: Role; readonly chunk: Chunk; readonly provisional: boolean; + /** + * True only for the single chunk currently being accumulated from live deltas + * (the in-flight text/thinking the model is actively generating). Absent/false + * once flushed or committed. Lets the UI show a live indicator (e.g. loading + * dots on streaming thinking) and drop it the moment generation moves on. + */ + readonly streaming?: boolean; } |
