diff options
| author | Adam Malczewski <[email protected]> | 2026-06-02 13:02:08 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-02 13:02:08 +0900 |
| commit | 2503eba38b7885ab92a9c0e4f082323d1b3a8679 (patch) | |
| tree | cc9ccc2184510d93c601a98ed08a05fdeb146a6f /packages/frontend/src/lib | |
| parent | 3f629a8469fe483243671e1ca15582a111e96541 (diff) | |
| download | dispatch-2503eba38b7885ab92a9c0e4f082323d1b3a8679.tar.gz dispatch-2503eba38b7885ab92a9c0e4f082323d1b3a8679.zip | |
feat(agents): per-model reasoning effort level
Add a per-model/key reasoning effort setting to agent definitions,
surfaced and editable in the Agent Settings page and displayed at a
glance in the model selector views.
- core: single source of truth for effort levels (REASONING_EFFORTS,
DEFAULT_REASONING_EFFORT='high', labels, isReasoningEffort guard);
add 'xhigh' level; AgentModelEntry.effort; xhigh budget=24000 for
classic-thinking Claude; default floor 'high'. Persist/parse effort
in the agent TOML loader.
- api: thread effort through the fallback chain with per-model -> per-tab
-> default precedence; validate /chat + agentModels effort from the
canonical list.
- frontend: effort <select> per model row in AgentBuilder; effort badges
in ModelSelector (agent + subagent chains); Thinking dropdown sourced
from canonical list; per-tab default raised to 'high'.
- tests: +15 (loader round-trip, agent xhigh budget, canonical list +
guard, api precedence, route validation).
Diffstat (limited to 'packages/frontend/src/lib')
4 files changed, 79 insertions, 24 deletions
diff --git a/packages/frontend/src/lib/components/AgentBuilder.svelte b/packages/frontend/src/lib/components/AgentBuilder.svelte index d8ae530..bbdb83c 100644 --- a/packages/frontend/src/lib/components/AgentBuilder.svelte +++ b/packages/frontend/src/lib/components/AgentBuilder.svelte @@ -4,6 +4,11 @@ const modelCache = new Map(); </script> <script lang="ts"> + import { + DEFAULT_REASONING_EFFORT, + REASONING_EFFORTS, + REASONING_EFFORT_LABELS, + } from "@dispatch/core/src/types/index.js"; import { config } from "../config.js"; import { router } from "../router.svelte.js"; import type { KeyInfo } from "../types.js"; @@ -13,6 +18,7 @@ const modelCache = new Map(); interface AgentModelEntry { key_id: string; model_id: string; + effort?: string; } interface AgentDefinition { @@ -173,6 +179,10 @@ const modelCache = new Map(); formModels = formModels.filter((_, idx) => idx !== i); } + function setEffortEntry(i: number, effort: string) { + formModels = formModels.map((m, idx) => (idx === i ? { ...m, effort } : m)); + } + async function openKeyModal(i: number) { modelModalIndex = i; modelModalType = "key"; @@ -546,6 +556,16 @@ const modelCache = new Map(); > {entry.model_id || "Select Model"} </button> + <select + class="select select-bordered select-sm shrink-0 w-28" + title="Reasoning effort for this model" + value={entry.effort ?? DEFAULT_REASONING_EFFORT} + onchange={(e) => setEffortEntry(i, e.currentTarget.value)} + > + {#each REASONING_EFFORTS as effort} + <option value={effort}>{REASONING_EFFORT_LABELS[effort]}</option> + {/each} + </select> <button type="button" class="btn btn-sm btn-ghost text-error" diff --git a/packages/frontend/src/lib/components/ModelSelector.svelte b/packages/frontend/src/lib/components/ModelSelector.svelte index a752f5d..c328511 100644 --- a/packages/frontend/src/lib/components/ModelSelector.svelte +++ b/packages/frontend/src/lib/components/ModelSelector.svelte @@ -3,6 +3,12 @@ const modelCache = new Map<string, string[]>(); </script> <script lang="ts"> + import { + DEFAULT_REASONING_EFFORT, + isReasoningEffort, + REASONING_EFFORTS, + REASONING_EFFORT_LABELS, + } from "@dispatch/core/src/types/index.js"; import type { KeyInfo } from "../types.js"; import { config } from "../config.js"; import { router } from "../router.svelte.js"; @@ -14,7 +20,7 @@ const modelCache = new Map<string, string[]>(); description: string; skills: string[]; tools: string[]; - models: Array<{ key_id: string; model_id: string }>; + models: Array<{ key_id: string; model_id: string; effort?: string }>; cwd?: string; is_subagent?: boolean; } @@ -30,6 +36,15 @@ const modelCache = new Map<string, string[]>(); }; } + /** + * Human-readable effort label for a (possibly-unset / arbitrary) effort + * string. Falls back to the default level's label when unset/invalid so the + * displayed badge always reflects what will actually be used. + */ + function effortLabel(effort: string | undefined): string { + return REASONING_EFFORT_LABELS[isReasoningEffort(effort) ? effort : DEFAULT_REASONING_EFFORT]; + } + const { keys = [], activeKeyId = null, @@ -37,7 +52,7 @@ const modelCache = new Map<string, string[]>(); reasoningEffort = "max", activeAgentSlug = null, activeTabParentId = null as string | null, - activeAgentModels = null as Array<{ key_id: string; model_id: string }> | null, + activeAgentModels = null as Array<{ key_id: string; model_id: string; effort?: string }> | null, workingDirectory = null, onKeyChange, onModelChange, @@ -51,7 +66,7 @@ const modelCache = new Map<string, string[]>(); reasoningEffort?: string; activeAgentSlug?: string | null; activeTabParentId?: string | null; - activeAgentModels?: Array<{ key_id: string; model_id: string }> | null; + activeAgentModels?: Array<{ key_id: string; model_id: string; effort?: string }> | null; workingDirectory?: string | null; onKeyChange: (keyId: string) => void; onModelChange: (keyId: string, modelId: string) => void; @@ -264,11 +279,9 @@ const modelCache = new Map<string, string[]>(); value={reasoningEffort} onchange={(e) => onReasoningChange(e.currentTarget.value)} > - <option value="none">Off</option> - <option value="low">Low</option> - <option value="medium">Medium</option> - <option value="high">High</option> - <option value="max">Max</option> + {#each REASONING_EFFORTS as effort} + <option value={effort}>{REASONING_EFFORT_LABELS[effort]}</option> + {/each} </select> </div> {/if} @@ -312,8 +325,9 @@ const modelCache = new Map<string, string[]>(); </div> <div class="mt-1 flex flex-col gap-0.5"> {#each subModels as m, i} - <div class="text-xs font-mono truncate {i === displayIdx ? 'opacity-100 font-semibold' : 'opacity-50'}"> - {i + 1}. {m.key_id} / {m.model_id} + <div class="text-xs font-mono truncate flex items-center gap-1 {i === displayIdx ? 'opacity-100 font-semibold' : 'opacity-50'}"> + <span class="truncate">{i + 1}. {m.key_id} / {m.model_id}</span> + <span class="badge badge-xs badge-ghost shrink-0">{effortLabel(m.effort)}</span> </div> {/each} </div> @@ -373,8 +387,9 @@ const modelCache = new Map<string, string[]>(); {@const displayIdx = sliderDragging !== null ? sliderDragging : (currentIdx >= 0 ? currentIdx : 0)} {@const displayModel = agent.models[displayIdx]} <div class="mt-2 pt-2 border-t border-primary-content/20"> - <div class="text-xs font-semibold mb-1 truncate"> - {displayModel ? `${displayModel.key_id} / ${displayModel.model_id}` : `${activeKeyId} / ${activeModelId}`} + <div class="text-xs font-semibold mb-1 truncate flex items-center gap-1"> + <span class="truncate">{displayModel ? `${displayModel.key_id} / ${displayModel.model_id}` : `${activeKeyId} / ${activeModelId}`}</span> + <span class="badge badge-xs shrink-0">{effortLabel(displayModel?.effort)}</span> </div> <input type="range" diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index 491b1bd..3372396 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -34,7 +34,7 @@ const { reasoningEffort = "max", activeAgentSlug = null as string | null, activeTabParentId = null as string | null, - activeAgentModels = null as Array<{ key_id: string; model_id: string }> | null, + activeAgentModels = null as Array<{ key_id: string; model_id: string; effort?: string }> | null, workingDirectory = null as string | null, onKeyChange, onModelChange, @@ -54,7 +54,7 @@ const { reasoningEffort?: string; activeAgentSlug?: string | null; activeTabParentId?: string | null; - activeAgentModels?: Array<{ key_id: string; model_id: string }> | null; + activeAgentModels?: Array<{ key_id: string; model_id: string; effort?: string }> | null; workingDirectory?: string | null; onKeyChange: (keyId: string) => void; onModelChange: (keyId: string, modelId: string) => void; diff --git a/packages/frontend/src/lib/tabs.svelte.ts b/packages/frontend/src/lib/tabs.svelte.ts index 3fd7e5f..d3061c3 100644 --- a/packages/frontend/src/lib/tabs.svelte.ts +++ b/packages/frontend/src/lib/tabs.svelte.ts @@ -12,6 +12,12 @@ import { // source of truth for HISTORY; `groupRowsToMessages` derives render bubbles. import { groupRowsToMessages, type MessageRow } from "@dispatch/core/src/chunks/transform.js"; import type { ChunkRow } from "@dispatch/core/src/types/index.js"; +import { + type AgentModelEntry, + DEFAULT_REASONING_EFFORT, + isReasoningEffort, + type ReasoningEffort, +} from "@dispatch/core/src/types/index.js"; import { config } from "./config.js"; import { appSettings } from "./settings.svelte.js"; import type { @@ -154,7 +160,7 @@ export interface Tab { agentStatus: "idle" | "running" | "error"; keyId: string | null; modelId: string | null; - reasoningEffort: string; + reasoningEffort: ReasoningEffort; currentAssistantId: string | null; tasks: TaskItem[]; injectedSkills: string[]; @@ -162,7 +168,7 @@ export interface Tab { persistent: boolean; agentSlug: string | null; agentScope: string | null; - agentModels: Array<{ key_id: string; model_id: string }> | null; + agentModels: AgentModelEntry[] | null; workingDirectory: string | null; queuedMessages: QueuedMessage[]; chunkLimit: number; @@ -280,7 +286,7 @@ export function createTabStore() { agentStatus: "idle", keyId: null, modelId: null, - reasoningEffort: "max", + reasoningEffort: DEFAULT_REASONING_EFFORT, currentAssistantId: null, tasks: [], injectedSkills: [], @@ -355,7 +361,7 @@ export function createTabStore() { agentStatus: "idle", keyId: tabData.keyId ?? null, modelId: tabData.modelId ?? null, - reasoningEffort: "max", + reasoningEffort: DEFAULT_REASONING_EFFORT, currentAssistantId: null, tasks: [], injectedSkills: [], @@ -829,7 +835,7 @@ export function createTabStore() { agentStatus, keyId: row.keyId ?? null, modelId: row.modelId ?? null, - reasoningEffort: "max", + reasoningEffort: DEFAULT_REASONING_EFFORT, currentAssistantId, tasks: [], injectedSkills: [], @@ -1154,7 +1160,7 @@ export function createTabStore() { parentTabId: string | null; agentSlug?: string | null; workingDirectory: string | null; - agentModels?: Array<{ key_id: string; model_id: string }> | null; + agentModels?: AgentModelEntry[] | null; }; // Only add if we don't already have this tab if (!getTabById(newTabEvent.id)) { @@ -1168,7 +1174,7 @@ export function createTabStore() { agentStatus: "running", keyId: newTabEvent.keyId ?? null, modelId: newTabEvent.modelId ?? null, - reasoningEffort: "max", + reasoningEffort: DEFAULT_REASONING_EFFORT, currentAssistantId: null, tasks: [], injectedSkills: [], @@ -1382,7 +1388,7 @@ export function createTabStore() { name: string; skills: string[]; tools: string[]; - models: Array<{ key_id: string; model_id: string }>; + models: AgentModelEntry[]; cwd?: string; }>; }; @@ -1451,7 +1457,7 @@ export function createTabStore() { agents?: Array<{ slug: string; scope: string; - models: Array<{ key_id: string; model_id: string }>; + models: AgentModelEntry[]; cwd?: string; }>; }; @@ -1733,6 +1739,19 @@ export function createTabStore() { }).catch(() => {}); } + /** + * Update the per-tab reasoning-effort selector. Ignores unrecognised + * values so an out-of-range string from the UI can't corrupt the tab + * state. This is the per-tab effort in the per-model → per-tab → default + * resolution chain. + */ + function setReasoningEffort(effort: string): void { + if (!isReasoningEffort(effort)) return; + const tab = getActiveTab(); + if (!tab) return; + updateTab(tab.id, { reasoningEffort: effort }); + } + function setWorkingDirectory(dir: string | null): void { const tab = getActiveTab(); if (!tab) return; @@ -1745,7 +1764,7 @@ export function createTabStore() { scope: string; skills: string[]; tools: string[]; - models: Array<{ key_id: string; model_id: string }>; + models: AgentModelEntry[]; cwd?: string; } | null, ): void { @@ -2002,6 +2021,7 @@ export function createTabStore() { stopGeneration, changeModel, setKey, + setReasoningEffort, setAgent, replyPermission, copyConversation, |
