summaryrefslogtreecommitdiffhomepage
path: root/src/core/chunks/selectors.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/chunks/selectors.ts')
-rw-r--r--src/core/chunks/selectors.ts88
1 files changed, 53 insertions, 35 deletions
diff --git a/src/core/chunks/selectors.ts b/src/core/chunks/selectors.ts
index 8fb832f..e46d5f5 100644
--- a/src/core/chunks/selectors.ts
+++ b/src/core/chunks/selectors.ts
@@ -1,4 +1,4 @@
-import type { ChatMessage, Chunk } from "@dispatch/wire";
+import type { ChatMessage, Chunk, TurnProviderRetryEvent } from "@dispatch/wire";
import type { RenderedChunk, TranscriptState } from "./types";
/**
@@ -6,46 +6,64 @@ import type { RenderedChunk, TranscriptState } from "./types";
* 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;
+ 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, streaming: true });
+ }
+ return result;
+}
+
+/**
+ * Whether a turn is currently generating (for a "generating…" indicator). True
+ * for ANY client watching the conversation — the sender, a second device, or a
+ * reconnected client whose in-flight turn was replayed.
+ */
+export function selectGenerating(state: TranscriptState): boolean {
+ return state.generating;
+}
+
+/**
+ * The latest `provider-retry` event for the current turn, or `null` when no retry
+ * is pending. Drives the transient yellow "retrying…" warning banner (rendered
+ * by ChatView). Never persisted — see `TranscriptState.providerRetry`.
+ */
+export function selectProviderRetry(state: TranscriptState): TurnProviderRetryEvent | null {
+ return state.providerRetry;
}
/**
* 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 rendered = selectChunks(state);
+ const first = rendered[0];
+ if (first === undefined) return [];
- const messages: ChatMessage[] = [];
- let role = first.role;
- let chunks: Chunk[] = [first.chunk];
+ 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;
+ 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;
}