summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-02 13:02:08 +0900
committerAdam Malczewski <[email protected]>2026-06-02 13:02:08 +0900
commit2503eba38b7885ab92a9c0e4f082323d1b3a8679 (patch)
treecc9ccc2184510d93c601a98ed08a05fdeb146a6f /packages/frontend/src
parent3f629a8469fe483243671e1ca15582a111e96541 (diff)
downloaddispatch-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')
-rw-r--r--packages/frontend/src/App.svelte11
-rw-r--r--packages/frontend/src/lib/components/AgentBuilder.svelte20
-rw-r--r--packages/frontend/src/lib/components/ModelSelector.svelte39
-rw-r--r--packages/frontend/src/lib/components/SidebarPanel.svelte4
-rw-r--r--packages/frontend/src/lib/tabs.svelte.ts40
5 files changed, 82 insertions, 32 deletions
diff --git a/packages/frontend/src/App.svelte b/packages/frontend/src/App.svelte
index eaa28e8..3f1c500 100644
--- a/packages/frontend/src/App.svelte
+++ b/packages/frontend/src/App.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
+import { DEFAULT_REASONING_EFFORT } from "@dispatch/core/src/types/index.js";
import { onMount } from "svelte";
import AgentBuilder from "./lib/components/AgentBuilder.svelte";
import ChatInput from "./lib/components/ChatInput.svelte";
@@ -141,20 +142,14 @@ onMount(() => {
apiBase={config.apiBase}
activeKeyId={tabStore.activeTab?.keyId ?? null}
activeModelId={tabStore.activeTab?.modelId ?? null}
- reasoningEffort={tabStore.activeTab?.reasoningEffort ?? "max"}
+ reasoningEffort={tabStore.activeTab?.reasoningEffort ?? DEFAULT_REASONING_EFFORT}
activeAgentSlug={tabStore.activeTab?.agentSlug ?? null}
activeTabParentId={tabStore.activeTab?.parentTabId ?? null}
activeAgentModels={tabStore.activeTab?.agentModels ?? null}
workingDirectory={tabStore.activeTab?.workingDirectory ?? null}
onKeyChange={(keyId) => tabStore.setKey(keyId)}
onModelChange={(keyId, modelId) => tabStore.changeModel(keyId, modelId)}
- onReasoningChange={(effort) => {
- const tab = tabStore.activeTab;
- if (tab) {
- // Update reasoning effort for active tab
- tabStore.tabs.find(t => t.id === tab.id)!.reasoningEffort = effort;
- }
- }}
+ onReasoningChange={(effort) => tabStore.setReasoningEffort(effort)}
onAgentChange={(agent) => tabStore.setAgent(agent)}
onWorkingDirectoryChange={(dir) => tabStore.setWorkingDirectory(dir)}
onAddKey={() => { showAddKeyModal = true; addKeyId = ""; addKeyProvider = "anthropic"; addKeyError = null; }}
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,