summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 10:55:51 +0900
committerAdam Malczewski <[email protected]>2026-06-25 10:55:51 +0900
commit38db3827870960f466be89afbc49f91238d46144 (patch)
tree24cb1b896dfadc31e72552dbe67f00530881242e /src/features/chat
parent17ce47987e673b6618454d033885b17b2a01912e (diff)
downloaddispatch-web-38db3827870960f466be89afbc49f91238d46144.tar.gz
dispatch-web-38db3827870960f466be89afbc49f91238d46144.zip
feat: workspaces shell + cwd-lsp rename + mcp/settings/system-prompt features + app wiring
- workspaces: URL-driven conversation grouping (home listing at /, routing, store, http adapter, WorkspaceCard) wired into the App.svelte shell - rename features/workspace -> features/cwd-lsp (the cwd/lsp status feature) - new features: mcp (status view), settings (chat-limit field), system-prompt (prompt builder), all rendered via the generic surface host - chat: store + ChatView updates - tabs: tabs-store updates - app wiring: ErrorModal (full-screen error surface), app/App.svelte + store.svelte This commit makes HEAD typecheck clean for the first time: the prior HEAD (c95cc77) imported features/settings from app/App.svelte but never committed the feature, so only the full working tree was green.
Diffstat (limited to 'src/features/chat')
-rw-r--r--src/features/chat/index.ts9
-rw-r--r--src/features/chat/store.svelte.ts33
-rw-r--r--src/features/chat/ui/ChatView.svelte32
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>