summaryrefslogtreecommitdiffhomepage
path: root/src/app/App.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-28 22:21:06 +0900
committerAdam Malczewski <[email protected]>2026-06-28 22:21:06 +0900
commit901bd56228ecb105bdbbf18bb9f848192f134791 (patch)
treed25ea66950c729387aa92a5e019f7c704a980dbe /src/app/App.svelte
parent89956f09a45db8bdd302f49d4028c0acc92a58a2 (diff)
downloaddispatch-web-901bd56228ecb105bdbbf18bb9f848192f134791.tar.gz
dispatch-web-901bd56228ecb105bdbbf18bb9f848192f134791.zip
feat(chat): add "Off" option to Reasoning Effort dropdown (thinking on/off)
Add an "Off" choice as the FIRST option in the per-conversation Reasoning Effort selector. Selecting it disables extended thinking ENTIRELY for the conversation's turns — NOT "set the effort to its lowest level". The two axes are kept SEPARATE on the wire (per the umans API model, where thinking-off is `reasoning_effort: "none"`, distinct from the effort level): - `reasoningEffort` is the thinking-DEPTH ladder (low→max) — UNCHANGED. - `thinking` is the on/off switch — a NEW per-conversation boolean. Turning thinking off PRESERVES the persisted effort level, so an off→on toggle restores the previously-chosen depth. The selector CONFLATES the two axes into one <select> (UX); the wire does not. Pure logic (reasoning-effort.ts): ThinkingSelection = "off" | ReasoningEffort, selectionOptions() (Off first + the ladder, default marked), isThinkingSelection, effectiveSelection(persistedEffort, persistedThinking), + FE-local PROPOSED wire types (ThinkingResponse, SetThinkingRequest) + SaveThinkingSelection port. The existing effortOptions()/isReasoningEffort()/effectiveEffort() are UNCHANGED — the heartbeat feature reuses them (its own dropdown is unaffected). Store: `thinking` state (boolean|null, null⇒ON default), refreshThinking() (GET, alongside refreshReasoningEffort() on every focus/draft/switch), setThinking() (PUT). App.svelte's saveThinkingSelection adapter fans one selection out: "off" → PUT /thinking {thinking:false} (effort untouched); a level → ensure thinking ON (if off) then PUT /reasoning-effort {level}. Backend contract gap (CR-14, backend-handoff.md §2l): the backend has NO thinking-off mechanism today (umans mapReasoningEffort can't emit "none"). The FE is built against the PROPOSED additive contract (GET/PUT /conversations/:id/thinking + ThinkingResponse/SetThinkingRequest; umans maps thinking===false → reasoning_effort:"none"). Until shipped, GET /thinking 404s and refreshThinking() leaves `thinking` null⇒ON — the selector gracefully shows the effort level, no crash. Verification: typecheck 0/0, 1128 tests green (run TWICE — touches the shared fetch fake), biome clean, build OK. 9 files (8 source + backend-handoff.md).
Diffstat (limited to 'src/app/App.svelte')
-rw-r--r--src/app/App.svelte44
1 files changed, 35 insertions, 9 deletions
diff --git a/src/app/App.svelte b/src/app/App.svelte
index 7e5ac7d..f87eddb 100644
--- a/src/app/App.svelte
+++ b/src/app/App.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
- import type { ImageInput, ReasoningEffort } from "@dispatch/transport-contract";
+ import type { ImageInput } from "@dispatch/transport-contract";
import type { InvokeMessage } from "@dispatch/ui-contract";
import { tick } from "svelte";
import Table from "../components/Table.svelte";
@@ -17,8 +17,9 @@
ReasoningEffortSelector,
type CompactNowResult,
type ComposerStatus,
- type ReasoningEffortSaveResult,
type SaveCompactPercentResult,
+ type ThinkingSelection,
+ type ThinkingSelectionSaveResult,
} from "../features/chat";
import { manifest as conversationCacheManifest } from "../features/conversation-cache";
import { manifest as markdownManifest } from "../features/markdown";
@@ -317,14 +318,35 @@
: { ok: false, error: result.error };
}
- // Adapt the store's reasoning-effort result to the chat feature's port.
- async function saveReasoningEffort(
- level: ReasoningEffort,
- ): Promise<ReasoningEffortSaveResult | null> {
- const result = await store.setReasoningEffort(level);
+ // Adapt the store's reasoning-effort + thinking results to the chat
+ // feature's combined selector port. The selector sends ONE selection ("off"
+ // or a level); the adapter fans it out to the right per-axis PUT(s). "off" is
+ // a SEPARATE signal from the effort level: it persists `thinking: false`
+ // (the umans route maps that to `reasoning_effort: "none"`), leaving the
+ // effort level untouched so an off→on toggle restores it. A level ensures
+ // thinking is ON (the level is meaningless while thinking is off) then sets
+ // the effort level.
+ async function saveThinkingSelection(
+ selection: ThinkingSelection,
+ ): Promise<ThinkingSelectionSaveResult | null> {
+ if (selection === "off") {
+ const result = await store.setThinking(false);
+ if (result === null) return null;
+ return result.ok
+ ? { ok: true, selection: "off" }
+ : { ok: false, error: result.error };
+ }
+ // A level: enable thinking first if it is currently off, then set the level.
+ if (store.thinking === false) {
+ const on = await store.setThinking(true);
+ if (on !== null && !on.ok) {
+ return { ok: false, error: on.error };
+ }
+ }
+ const result = await store.setReasoningEffort(selection);
if (result === null) return null;
return result.ok
- ? { ok: true, reasoningEffort: result.reasoningEffort }
+ ? { ok: true, selection: result.reasoningEffort }
: { ok: false, error: result.error };
}
@@ -728,7 +750,11 @@
re-mount per conversation — incl. switching between drafts — and can't
bleed across tabs. Editable for a draft too (cwd + effort apply from turn 1). -->
{#key store.currentConversationId}
- <ReasoningEffortSelector persisted={store.reasoningEffort} save={saveReasoningEffort} />
+ <ReasoningEffortSelector
+ persistedEffort={store.reasoningEffort}
+ persistedThinking={store.thinking}
+ save={saveThinkingSelection}
+ />
<CwdField cwd={store.cwd} canEdit={true} save={saveCwd} />
<ComputerField
computerId={store.computerId}