summaryrefslogtreecommitdiffhomepage
path: root/src/core/chunks/selectors.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 00:02:32 +0900
committerAdam Malczewski <[email protected]>2026-06-07 00:02:32 +0900
commit312ed5a9fe0d7f2660a66d61baf429b8e021ffb1 (patch)
treedad46b8b88a6bc5629d50f18912ff944eaef4e95 /src/core/chunks/selectors.ts
parenta062045579de1bd0903f0f2c7f3e742162bcb7cc (diff)
downloaddispatch-web-312ed5a9fe0d7f2660a66d61baf429b8e021ffb1.tar.gz
dispatch-web-312ed5a9fe0d7f2660a66d61baf429b8e021ffb1.zip
Slice 2 wave 1: transcript reducer, wire conformance, ws chat, cache core
- core/chunks: the one pure transcript reducer (foldEvent live deltas + applyHistory seq-keyed reconcile + selectChunks/selectMessages); 27 tests - core/wire: FE-side contract-conformance exhaustiveness guards + drift smoke tests over wire/transport-contract unions (ยง2.9 drift signal); 10 tests - adapters/ws: additively multiplex chat.send/chat.delta/chat.error on the existing surface socket (onChat + widened send); surface API unchanged - features/conversation-cache: pure reconcileCache/nextSinceSeq/selectEvictions + ConversationChunkStore port + injected createConversationCache; 26 tests Verified green: svelte-check 0/0, vitest 169, biome clean, build ok.
Diffstat (limited to 'src/core/chunks/selectors.ts')
-rw-r--r--src/core/chunks/selectors.ts51
1 files changed, 51 insertions, 0 deletions
diff --git a/src/core/chunks/selectors.ts b/src/core/chunks/selectors.ts
new file mode 100644
index 0000000..8fb832f
--- /dev/null
+++ b/src/core/chunks/selectors.ts
@@ -0,0 +1,51 @@
+import type { ChatMessage, Chunk } from "@dispatch/wire";
+import type { RenderedChunk, TranscriptState } from "./types";
+
+/**
+ * Select all chunks for rendering: committed first (seq order),
+ * then provisional (seq: null).
+ */
+export function selectChunks(state: TranscriptState): readonly RenderedChunk[] {
+ const result: RenderedChunk[] = [];
+ for (const c of state.committed) {
+ result.push({ seq: c.seq, role: c.role, chunk: c.chunk, provisional: false });
+ }
+ for (const p of state.provisional) {
+ result.push({ seq: null, role: p.role, chunk: p.chunk, provisional: true });
+ }
+ if (state.accumulating !== null) {
+ const chunk: Chunk =
+ 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 });
+ }
+ return result;
+}
+
+/**
+ * Group consecutive same-role rendered chunks into ChatMessages.
+ */
+export function selectMessages(state: TranscriptState): readonly ChatMessage[] {
+ const rendered = selectChunks(state);
+ const first = rendered[0];
+ if (first === undefined) return [];
+
+ const messages: ChatMessage[] = [];
+ let role = first.role;
+ let chunks: Chunk[] = [first.chunk];
+
+ for (let i = 1; i < rendered.length; i++) {
+ const rc = rendered[i];
+ if (rc === undefined) continue;
+ if (rc.role === role) {
+ chunks.push(rc.chunk);
+ } else {
+ messages.push({ role, chunks });
+ role = rc.role;
+ chunks = [rc.chunk];
+ }
+ }
+ messages.push({ role, chunks });
+ return messages;
+}