diff options
Diffstat (limited to 'src/features/chat')
| -rw-r--r-- | src/features/chat/index.ts | 9 | ||||
| -rw-r--r-- | src/features/chat/store.svelte.ts | 33 | ||||
| -rw-r--r-- | src/features/chat/ui/ChatView.svelte | 32 |
3 files changed, 68 insertions, 6 deletions
diff --git a/src/features/chat/index.ts b/src/features/chat/index.ts index 1596c53..c120916 100644 --- a/src/features/chat/index.ts +++ b/src/features/chat/index.ts @@ -1,5 +1,10 @@ -export type { RenderedChunk, RenderGroup, ToolBatchEntry } from "../../core/chunks"; -export { groupRenderedChunks } from "../../core/chunks"; +export type { + ProviderRetryView, + RenderedChunk, + RenderGroup, + ToolBatchEntry, +} from "../../core/chunks"; +export { groupRenderedChunks, viewProviderRetry } from "../../core/chunks"; export type { TurnMetricsEntry } from "../../core/metrics"; export type { ChatTransport, HistorySync, HistoryWindow, MetricsSync } from "./ports"; export type { diff --git a/src/features/chat/store.svelte.ts b/src/features/chat/store.svelte.ts index 9beabfc..a31fe55 100644 --- a/src/features/chat/store.svelte.ts +++ b/src/features/chat/store.svelte.ts @@ -4,7 +4,7 @@ import type { ChatQueueMessage, ChatSendMessage, } from "@dispatch/transport-contract"; -import type { ChatMessage, StoredChunk } from "@dispatch/wire"; +import type { ChatMessage, StoredChunk, TurnProviderRetryEvent } from "@dispatch/wire"; import type { RenderedChunk, TranscriptState } from "../../core/chunks"; import { appendUserMessage, @@ -19,6 +19,7 @@ import { selectGenerating, selectHasEarlier, selectMessages, + selectProviderRetry, trimTranscript, unloadCount, windowTranscript, @@ -42,6 +43,12 @@ export interface ChatStoreDependencies { readonly metricsSync: MetricsSync; readonly cache: ConversationCache; /** + * The workspace this conversation belongs to (its URL slug). Sent on + * `chat.send`/`chat.queue` so the backend stamps the conversation's workspace + * at creation (default "default"). Absent → omitted (legacy behavior). + */ + readonly workspaceId?: string; + /** * The chat limit: max loaded chunks before the oldest quarter is unloaded * (see `core/chunks/trim.ts`). Normalized via `normalizeChatLimit`; absent → * `DEFAULT_CHAT_LIMIT`. @@ -54,6 +61,12 @@ export interface ChatStoreDependencies { * to the bottom (the next fold retries). Absent → always allowed. */ readonly canUnload?: () => boolean; + /** + * Called when a swallowed error should be surfaced to the user (e.g. a + * metrics sync failure). Wired by the composition root to the app store's + * `reportError` → full-screen error modal. Absent → errors are logged only. + */ + readonly onError?: (context: string, err: unknown) => void; } export interface ChatStore { @@ -73,6 +86,13 @@ export interface ChatStore { * turn was replayed. Drives the composer's "generating…" indicator. */ readonly generating: boolean; + /** + * The latest `provider-retry` event for the current turn, or `null` when no + * retry is pending. Drives the transient yellow "retrying…" warning banner — + * never persisted (never sent to the model or replayed on reload). Coalesces + * to the newest attempt + delay; cleared when content resumes or the turn ends. + */ + readonly providerRetry: TurnProviderRetryEvent | null; readonly pendingSync: boolean; readonly error: string | null; readonly model: string | undefined; @@ -183,9 +203,11 @@ export function createChatStore(deps: ChatStoreDependencies): ChatStore { try { const res = await deps.metricsSync(deps.conversationId); metrics = applyDurableMetrics(metrics, res.turns); - } catch { + } catch (err) { // Metrics fetch failure must not block history sync or throw; - // live-folded metrics remain intact. + // live-folded metrics remain intact. Surface via onError (modal). + console.error("[syncMetrics] failed:", err); + deps.onError?.("Failed to sync conversation metrics", err); } } @@ -251,6 +273,9 @@ export function createChatStore(deps: ChatStoreDependencies): ChatStore { get generating(): boolean { return selectGenerating(transcript); }, + get providerRetry(): TurnProviderRetryEvent | null { + return selectProviderRetry(transcript); + }, get pendingSync(): boolean { return _pendingSync; }, @@ -295,6 +320,7 @@ export function createChatStore(deps: ChatStoreDependencies): ChatStore { conversationId: deps.conversationId, message: text, ...(_model !== undefined ? { model: _model } : {}), + ...(deps.workspaceId !== undefined ? { workspaceId: deps.workspaceId } : {}), }; deps.transport.send(msg); }, @@ -306,6 +332,7 @@ export function createChatStore(deps: ChatStoreDependencies): ChatStore { type: "chat.queue", conversationId: deps.conversationId, text: trimmed, + ...(deps.workspaceId !== undefined ? { workspaceId: deps.workspaceId } : {}), }; deps.transport.send(msg); }, diff --git a/src/features/chat/ui/ChatView.svelte b/src/features/chat/ui/ChatView.svelte index 2b55eb3..f2899c7 100644 --- a/src/features/chat/ui/ChatView.svelte +++ b/src/features/chat/ui/ChatView.svelte @@ -1,5 +1,6 @@ <script lang="ts"> - import { groupRenderedChunks, type RenderedChunk } from "../index"; + import type { TurnProviderRetryEvent } from "@dispatch/wire"; + import { groupRenderedChunks, type RenderedChunk, viewProviderRetry } from "../index"; import { interleaveTurnMetrics, viewCacheRate, @@ -22,6 +23,7 @@ hasEarlier = false, onShowEarlier, thinkingKeyBase = 0, + providerRetry = null, }: { chunks: readonly RenderedChunk[]; turnMetrics?: readonly TurnMetricsEntry[]; @@ -35,6 +37,13 @@ * swap collapse state) when a trim removes older thinking blocks. */ thinkingKeyBase?: number; + /** + * The latest `provider-retry` event for the current turn, or `null` when + * no retry is pending → renders the transient yellow "retrying…" banner. + * Never persisted (never part of the message history); coalesces to the + * newest attempt + delay, and is cleared when content resumes / turn ends. + */ + providerRetry?: TurnProviderRetryEvent | null; } = $props(); // True while a show-earlier page-in is awaited (disables the button). @@ -261,4 +270,25 @@ </div> {/if} {/each} + {#if providerRetry} + {@const rv = viewProviderRetry(providerRetry)} + <!-- Transient yellow warning: a provider error is being retried with backoff. + NOT a message chunk (never persisted/replayed) — a live UI notification only, + shown where the reply would appear. Coalesces to the newest attempt + delay, + and is cleared (foldEvent) when content resumes or the turn ends. --> + <div class="chat chat-start [&>.chat-bubble]:max-w-5xl"> + <div class="chat-bubble w-full bg-transparent"> + <div class="alert alert-warning flex-wrap items-start gap-2 py-2 text-sm" role="status"> + <div class="flex flex-wrap items-center gap-2 font-medium"> + <span aria-hidden="true">⚠</span> + <span>{rv.attemptLabel} — retrying in {rv.delayLabel}…</span> + {#if rv.code} + <span class="badge badge-warning badge-sm font-mono">{rv.code}</span> + {/if} + </div> + <div class="w-full font-mono text-xs opacity-70">{rv.message}</div> + </div> + </div> + </div> + {/if} </div> |
