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 | c1082294dd98cadfecda58d854174610659cadec (patch) | |
| tree | 00dbc2ebd40260d1ae82820d56f9ab8f62cf343b /src/features/chat/ui | |
| parent | 1cdb866fbb708a1f6c5e802a075789cece85800d (diff) | |
| download | dispatch-web-c1082294dd98cadfecda58d854174610659cadec.tar.gz dispatch-web-c1082294dd98cadfecda58d854174610659cadec.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/features/chat/ui')
| -rw-r--r-- | src/features/chat/ui/ReasoningEffortSelector.svelte | 43 |
1 files changed, 27 insertions, 16 deletions
diff --git a/src/features/chat/ui/ReasoningEffortSelector.svelte b/src/features/chat/ui/ReasoningEffortSelector.svelte index d982905..6858779 100644 --- a/src/features/chat/ui/ReasoningEffortSelector.svelte +++ b/src/features/chat/ui/ReasoningEffortSelector.svelte @@ -1,34 +1,45 @@ <script lang="ts"> import type { ReasoningEffort } from "@dispatch/transport-contract"; import { - effectiveEffort, - effortOptions, - isReasoningEffort, - type SaveReasoningEffort, + effectiveSelection, + isThinkingSelection, + selectionOptions, + type SaveThinkingSelection, + type ThinkingSelection, } from "../reasoning-effort"; let { - persisted, + persistedEffort, + persistedThinking, save, }: { - /** The conversation's persisted level, or null when never set (default applies). */ - persisted: ReasoningEffort | null; - save: SaveReasoningEffort; + /** The conversation's persisted effort level, or null when never set (default applies). */ + persistedEffort: ReasoningEffort | null; + /** + * The conversation's persisted thinking flag, or null when never set + * (thinking ON — the default). `false` ⇒ thinking disabled (the separate + * "off" axis); the effort level is preserved across an off→on toggle. + */ + persistedThinking: boolean | null; + /** Persist a thinking selection (off or a level). */ + save: SaveThinkingSelection; } = $props(); - const options = effortOptions(); + const options = selectionOptions(); - // The user's in-flight choice; null = mirror the (async-loaded) persisted prop. - // Re-mounted per conversation, so there is no cross-tab bleed. - let chosen = $state<ReasoningEffort | null>(null); + // The user's in-flight choice; null = mirror the (async-loaded) persisted + // selection. Re-mounted per conversation, so there is no cross-tab bleed. + let chosen = $state<ThinkingSelection | null>(null); let saving = $state(false); let error = $state<string | null>(null); let justSaved = $state(false); - const selected = $derived(chosen ?? effectiveEffort(persisted)); + const selected = $derived( + chosen ?? effectiveSelection(persistedEffort, persistedThinking), + ); async function handleChange(value: string) { - if (!isReasoningEffort(value) || saving) return; + if (!isThinkingSelection(value) || saving) return; chosen = value; saving = true; error = null; @@ -40,7 +51,7 @@ justSaved = true; } else { error = result.error; - chosen = null; // revert to the persisted value + chosen = null; // revert to the persisted selection } } </script> @@ -69,7 +80,7 @@ <p class="text-xs text-success">Saved — applies from the next turn.</p> {:else} <p class="text-xs opacity-50"> - How long the model thinks before answering. Changing it can re-prefill the prompt cache once. + How long the model thinks before answering. “Off” disables thinking entirely. Changing it can re-prefill the prompt cache once. </p> {/if} </div> |
