diff options
| author | Adam Malczewski <[email protected]> | 2026-10-10 01:36:39 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-10 01:36:39 +0900 |
| commit | 551a7a050e8cd25e795026a1060c9d5b5ea028bf (patch) | |
| tree | cba29eb5ada26778c09aea7cc767e2ddf665bce8 /src/core/chunks/selectors.test.ts | |
| parent | d8381e201cb1407a939b496642a0803aec58a47d (diff) | |
| download | dispatch-web-551a7a050e8cd25e795026a1060c9d5b5ea028bf.tar.gz dispatch-web-551a7a050e8cd25e795026a1060c9d5b5ea028bf.zip | |
perf(chat): fast open of long in-flight turns
- Batch live chat events: fold a burst in one pass per animation frame
(rAF + timeout fallback for hidden tabs) instead of one reactive cycle per
WS frame. Flushed before send/resync to keep ordering.
- De-dup the late-join replay against committed history: provisional chunks
of the in-flight turn already persisted (CR-6 per-step persistence) are
hidden by aligned-prefix matching (fail-safe: mismatch shows, never hides).
- Memoize chunks/turnMetrics getters by state identity.
- LazyCollapse: tool/thinking card bodies mount only on first open.
- content-visibility:auto on transcript rows.
- scripts/probe-replay.ts + bench-pure.ts for measuring the replay path.
Diffstat (limited to 'src/core/chunks/selectors.test.ts')
| -rw-r--r-- | src/core/chunks/selectors.test.ts | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/src/core/chunks/selectors.test.ts b/src/core/chunks/selectors.test.ts new file mode 100644 index 0000000..57c1415 --- /dev/null +++ b/src/core/chunks/selectors.test.ts @@ -0,0 +1,145 @@ +import type { AgentEvent, StepId, StoredChunk } from "@dispatch/wire"; +import { describe, expect, it } from "vitest"; +import { applyHistory, foldEvent, initialState } from "./reducer"; +import { selectChunks } from "./selectors"; +import type { TranscriptState } from "./types"; + +const C = "conv"; +const T = "turn-1"; + +function fold(state: TranscriptState, events: readonly AgentEvent[]): TranscriptState { + return events.reduce(foldEvent, state); +} + +const turnStart: AgentEvent = { type: "turn-start", conversationId: C, turnId: T }; +const userMsg: AgentEvent = { type: "user-message", conversationId: C, turnId: T, text: "hi" }; +const think = (delta: string): AgentEvent => ({ + type: "reasoning-delta", + conversationId: C, + turnId: T, + delta, +}); +const text = (delta: string): AgentEvent => ({ + type: "text-delta", + conversationId: C, + turnId: T, + delta, +}); +const call = (id: string, step = 0): AgentEvent => ({ + type: "tool-call", + conversationId: C, + turnId: T, + stepId: `${T}#${step}` as StepId, + toolCallId: id, + toolName: "run_shell", + input: {}, +}); +const result = (id: string, step = 0): AgentEvent => ({ + type: "tool-result", + conversationId: C, + turnId: T, + stepId: `${T}#${step}` as StepId, + toolCallId: id, + toolName: "run_shell", + content: "ok", + isError: false, +}); + +/** The persisted chunks for steps 0 (user + think + call/result). */ +const committedStep0: StoredChunk[] = [ + { seq: 1, role: "user", chunk: { type: "text", text: "hi" }, turnId: T }, + { seq: 2, role: "assistant", chunk: { type: "thinking", text: "plan" }, turnId: T }, + { + seq: 3, + role: "assistant", + chunk: { + type: "tool-call", + toolCallId: "a", + toolName: "run_shell", + input: {}, + stepId: `${T}#0` as StepId, + }, + turnId: T, + }, + { + seq: 4, + role: "tool", + chunk: { + type: "tool-result", + toolCallId: "a", + toolName: "run_shell", + content: "ok", + isError: false, + stepId: `${T}#0` as StepId, + }, + turnId: T, + }, +]; + +describe("selectChunks — de-dup of a late-join replay against committed history", () => { + it("hides replayed provisional chunks already persisted for the in-flight turn", () => { + // Fresh load mid-turn: history has step 0; the replay re-delivers step 0 + // and then step 1 (still streaming). + let s = applyHistory(initialState(), committedStep0); + s = fold(s, [ + userMsg, + turnStart, + think("pl"), + think("an"), + call("a"), + result("a"), + text("now"), + ]); + + const rendered = selectChunks(s); + expect(rendered.map((r) => [r.seq, r.chunk.type])).toEqual([ + [1, "text"], + [2, "thinking"], + [3, "tool-call"], + [4, "tool-result"], + [null, "text"], + ]); + expect(rendered[4]?.streaming).toBe(true); + }); + + it("keeps provisional chunks beyond the committed run", () => { + let s = applyHistory(initialState(), committedStep0); + s = fold(s, [userMsg, turnStart, think("plan"), call("a"), result("a"), call("b", 1)]); + const rendered = selectChunks(s); + expect(rendered).toHaveLength(5); + expect(rendered[4]?.chunk).toMatchObject({ type: "tool-call", toolCallId: "b" }); + expect(rendered[4]?.provisional).toBe(true); + }); + + it("hides the accumulating chunk once its content is committed", () => { + const committed: StoredChunk[] = [ + ...committedStep0, + { seq: 5, role: "assistant", chunk: { type: "text", text: "final" }, turnId: T }, + ]; + let s = applyHistory(initialState(), committed); + s = fold(s, [userMsg, turnStart, think("plan"), call("a"), result("a"), text("final")]); + const rendered = selectChunks(s); + expect(rendered).toHaveLength(5); + expect(rendered.every((r) => !r.provisional)).toBe(true); + }); + + it("stops de-duping at the first mismatch (never hides divergent content)", () => { + let s = applyHistory(initialState(), committedStep0); + s = fold(s, [userMsg, turnStart, think("DIFFERENT"), call("a"), result("a")]); + const rendered = selectChunks(s); + // user echo matched (1 hidden); everything from the mismatch on is shown. + expect(rendered.filter((r) => r.provisional)).toHaveLength(3); + }); + + it("does nothing when the committed tail belongs to another turn", () => { + const other = committedStep0.map((c) => ({ ...c, turnId: "older" })); + let s = applyHistory(initialState(), other); + s = fold(s, [userMsg, turnStart, think("plan")]); + expect(selectChunks(s).filter((r) => r.provisional)).toHaveLength(2); + }); + + it("is the identity when there is no committed history", () => { + const s = fold(initialState(), [userMsg, turnStart, text("hello")]); + expect(selectChunks(s)).toHaveLength(2); + }); +}); |
