diff options
| author | Adam Malczewski <[email protected]> | 2026-06-28 22:21:06 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-28 22:21:06 +0900 |
| commit | 901bd56228ecb105bdbbf18bb9f848192f134791 (patch) | |
| tree | d25ea66950c729387aa92a5e019f7c704a980dbe /src/app/App.svelte | |
| parent | 89956f09a45db8bdd302f49d4028c0acc92a58a2 (diff) | |
| download | dispatch-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.svelte | 44 |
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} |
