diff options
Diffstat (limited to 'src/app/store.svelte.ts')
| -rw-r--r-- | src/app/store.svelte.ts | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index 22b0a25..629e6c6 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -54,6 +54,7 @@ import { } from "../core/protocol"; import type { ChatStore, HistorySync, MetricsSync } from "../features/chat"; import { createChatStore } from "../features/chat"; +import type { SetThinkingRequest, ThinkingResponse } from "../features/chat/reasoning-effort"; import type { ConcurrencyCooldownResult, ConcurrencyDeleteResult, @@ -132,6 +133,14 @@ export type ReasoningEffortResult = | { readonly ok: true; readonly reasoningEffort: ReasoningEffort } | { readonly ok: false; readonly error: string }; +/** + * Outcome of `PUT /conversations/:id/thinking` (PROPOSED — see + * `backend-handoff.md`; the endpoint is not yet shipped by the backend). + */ +export type ThinkingResult = + | { readonly ok: true; readonly thinking: boolean } + | { readonly ok: false; readonly error: string }; + /** Outcome of `POST /conversations/:id/compact` (manual compaction). */ export type CompactResult = | { readonly ok: true; readonly response: CompactResponse } @@ -206,6 +215,15 @@ export interface AppStore { * wants to add input — the server owns the idle-vs-generating decision. */ queueMessage(text: string): void; + /** + * Cancel (remove) a single queued steering message by id so it never runs + * (`chat.queue.cancel` WS op). Fire-and-forget + idempotent: the + * message-queue surface update reconciles the queue UI (the cancelled message + * leaves the snapshot). Targets the focused conversation's queue. The caller + * optimistically hides the row; a cancel of an already-drained / unknown + * message is a silent server no-op (nothing to roll back). + */ + cancelQueuedMessage(messageId: string): void; selectModel(model: string): void; newDraft(): void; /** Switch the active workspace (on route change) + reset to a fresh draft in it. */ @@ -273,6 +291,22 @@ export interface AppStore { */ setReasoningEffort(level: ReasoningEffort): Promise<ReasoningEffortResult | null>; /** + * The workspace conversation's persisted thinking flag, or null when never + * set (the server then resolves turns with thinking ON — the default). + * `false` ⇒ thinking disabled entirely (the SEPARATE "off" axis — NOT a + * zero-effort level; the umans route maps it to `reasoning_effort: "none"`). + * PROPOSED backend contract — see `backend-handoff.md`. + */ + readonly thinking: boolean | null; + /** + * Persist the workspace conversation's thinking flag + * (`PUT /conversations/:id/thinking`). Works for a draft too (its id survives + * promotion), so the first turn already runs with the chosen setting. Takes + * effect from the NEXT turn; resolution stays server-owned. + * PROPOSED backend contract — see `backend-handoff.md`. + */ + setThinking(enabled: boolean): Promise<ThinkingResult | null>; + /** * Manually trigger conversation compaction (`POST /conversations/:id/compact`). * Summarizes old messages + retains the most recent N. Returns null when no * conversation is focused (a draft has nothing to compact). @@ -729,6 +763,30 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { } } + // The workspace conversation's persisted thinking flag (SEPARATE from the + // effort level). Seeded from the backend on focus change; null = never set + // (thinking ON — the default). PROPOSED endpoint (see backend-handoff.md): + // a 404 (endpoint not yet shipped) leaves `thinking` null ⇒ ON (default), so + // the selector simply shows the effort level until the backend ships it. + let thinking = $state<boolean | null>(null); + + /** Refetch the workspace conversation's thinking flag (works for a draft too). */ + async function refreshThinking(): Promise<void> { + const id = workspaceConversationId(); + // Clear immediately so a switch never shows the PREVIOUS conversation's + // setting while the fetch is in flight (null ⇒ ON, the default). + thinking = null; + try { + const res = await fetchImpl(`${httpBase}/conversations/${encodeURIComponent(id)}/thinking`); + if (!res.ok) return; + const data = (await res.json()) as ThinkingResponse; + // Guard a slow response losing a race with a conversation switch. + if (workspaceConversationId() === id) thinking = data.thinking ?? null; + } catch (err) { + reportError("Failed to load thinking setting", err); + } + } + // The workspace conversation's auto-compact percent. Seeded from the // backend on focus change; null = not yet fetched. 0 = disabled. let compactPercent = $state<number | null>(null); @@ -975,6 +1033,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { void refreshCwd(); void refreshComputer(); void refreshReasoningEffort(); + void refreshThinking(); void refreshCompactPercent(); } @@ -1208,6 +1267,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { void refreshComputer(); void refreshModel(); void refreshReasoningEffort(); + void refreshThinking(); void refreshCompactPercent(); void refreshVisionSettings(); @@ -1243,6 +1303,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { void refreshComputer(); void refreshModel(); void refreshReasoningEffort(); + void refreshThinking(); void refreshCompactPercent(); }, get activeChat(): ChatStore { @@ -1286,6 +1347,9 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { get reasoningEffort(): ReasoningEffort | null { return reasoningEffort; }, + get thinking(): boolean | null { + return thinking; + }, get compactPercent(): number | null { return compactPercent; }, @@ -1340,6 +1404,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { void refreshCwd(); void refreshComputer(); void refreshReasoningEffort(); + void refreshThinking(); void refreshCompactPercent(); // Now send on the promoted store chatStores.get(conversationId)?.send(text, images); @@ -1357,6 +1422,15 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { activeChat.queueMessage(text); }, + cancelQueuedMessage(messageId: string): void { + // Fire-and-forget + idempotent. The message-queue surface (conversation- + // scoped) reconciles the queue UI — the cancelled row leaves the snapshot. + // A cancel of an already-drained / unknown message is a silent no-op, so + // there is no local state to roll back. Delegates to the focused + // conversation's chat store, which owns the conversationId + transport. + activeChat.cancelQueuedMessage(messageId); + }, + selectModel(model: string): void { activeModel = model; const activeId = tabsStore.activeConversationId; @@ -1386,6 +1460,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { void refreshComputer(); void refreshModel(); void refreshReasoningEffort(); + void refreshThinking(); void refreshCompactPercent(); }, @@ -1401,6 +1476,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { void refreshComputer(); void refreshModel(); void refreshReasoningEffort(); + void refreshThinking(); void refreshCompactPercent(); }, @@ -1587,6 +1663,37 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { } }, + async setThinking(enabled: boolean): Promise<ThinkingResult | null> { + const id = workspaceConversationId(); + const body: SetThinkingRequest = { thinking: enabled }; + try { + const res = await fetchImpl( + `${httpBase}/conversations/${encodeURIComponent(id)}/thinking`, + { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }, + ); + if (!res.ok) { + const errBody = (await res.json().catch(() => null)) as { error?: string } | null; + return { + ok: false, + error: errBody?.error ?? `Set thinking failed (HTTP ${res.status})`, + }; + } + const data = (await res.json()) as ThinkingResponse; + const next = data.thinking ?? enabled; + if (workspaceConversationId() === id) thinking = next; + return { ok: true, thinking: next }; + } catch (err) { + return { + ok: false, + error: err instanceof Error ? err.message : "Set thinking request failed", + }; + } + }, + stopGeneration(): void { const conversationId = tabsStore.activeConversationId; if (conversationId === null) return; |
