summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-23 17:16:33 +0900
committerAdam Malczewski <[email protected]>2026-05-23 17:16:33 +0900
commite0d8663103e8fc0b0cdaf523ca3f48849848ed69 (patch)
treea3a350ceea0596d44d2661234aaacf5d19f73977
parent236beefb708a6cd91b673978ddf4ebf045a9844c (diff)
downloaddispatch-e0d8663103e8fc0b0cdaf523ca3f48849848ed69.tar.gz
dispatch-e0d8663103e8fc0b0cdaf523ca3f48849848ed69.zip
feat: fallback model range slider with live label, model-changed event
- Added model-changed event: backend emits it on fallback, frontend updates tab keyId/modelId - Range slider embedded inside active agent card when >1 model configured - Live label updates on drag (oninput), backend call only on release (onchange) - Slider auto-positions when fallback occurs via model-changed WS event
-rw-r--r--packages/api/src/agent-manager.ts4
-rw-r--r--packages/core/src/types/index.ts1
-rw-r--r--packages/frontend/src/lib/components/ModelSelector.svelte61
-rw-r--r--packages/frontend/src/lib/tabs.svelte.ts9
-rw-r--r--packages/frontend/src/lib/types.ts1
5 files changed, 72 insertions, 4 deletions
diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts
index 828855c..5b19eb3 100644
--- a/packages/api/src/agent-manager.ts
+++ b/packages/api/src/agent-manager.ts
@@ -984,6 +984,10 @@ export class AgentManager {
`Falling back to "${nextEntry.key_id}" (model: ${nextEntry.model_id})...`;
console.warn(`[dispatch] ${fallbackMsg}`);
this.emit({ type: "notice", message: fallbackMsg }, tabId);
+ this.emit(
+ { type: "model-changed", keyId: nextEntry.key_id, modelId: nextEntry.model_id },
+ tabId,
+ );
tabAgent.agent = null;
continue;
}
diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts
index b17a720..1c3090f 100644
--- a/packages/core/src/types/index.ts
+++ b/packages/core/src/types/index.ts
@@ -38,6 +38,7 @@ export type AgentEvent =
| { type: "shell-output"; data: string; stream: "stdout" | "stderr" }
| { type: "error"; error: string; statusCode?: number }
| { type: "notice"; message: string }
+ | { type: "model-changed"; keyId: string; modelId: string }
| { type: "done"; message: ChatMessage }
| { type: "task-list-update"; tasks: TaskItem[] }
| { type: "config-reload" }
diff --git a/packages/frontend/src/lib/components/ModelSelector.svelte b/packages/frontend/src/lib/components/ModelSelector.svelte
index 19ce818..bbbf036 100644
--- a/packages/frontend/src/lib/components/ModelSelector.svelte
+++ b/packages/frontend/src/lib/components/ModelSelector.svelte
@@ -61,6 +61,7 @@ const modelCache = new Map<string, string[]>();
let availableModels = $state<string[]>([]);
let loadingModels = $state(false);
let modelError = $state<string | null>(null);
+ let sliderDragging = $state<number | null>(null);
let cwdExists = $state<boolean | null>(null);
let cwdCheckTimer: ReturnType<typeof setTimeout> | null = null;
@@ -265,28 +266,80 @@ const modelCache = new Map<string, string[]>();
{:else}
<div class="flex flex-col gap-1.5">
{#each visibleAgents as agent (agent.slug + ":" + agent.scope)}
- <button
- class="w-full text-left rounded-lg px-3 py-2 transition-colors {activeAgentSlug === agent.slug ? 'bg-primary text-primary-content' : 'bg-base-300 hover:bg-base-200'}"
+ {@const isActive = activeAgentSlug === agent.slug}
+ {@const hasMultipleModels = agent.models.length > 1}
+ {@const currentIdx = isActive
+ ? agent.models.findIndex(
+ (m) => m.key_id === activeKeyId && m.model_id === activeModelId,
+ )
+ : -1}
+ <div
+ role="button"
+ tabindex="0"
+ class="w-full text-left rounded-lg px-3 py-2 transition-colors {isActive ? 'bg-primary text-primary-content' : 'bg-base-300 hover:bg-base-200'}"
onclick={() => {
+ // Only switch agent — don't reset the slider position
onAgentChange(agent);
const cwdEl = document.getElementById("cwd-input") as HTMLInputElement | null;
if (cwdEl) cwdEl.value = agent.cwd ?? "";
}}
+ onkeydown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onAgentChange(agent);
+ const cwdEl = document.getElementById("cwd-input") as HTMLInputElement | null;
+ if (cwdEl) cwdEl.value = agent.cwd ?? "";
+ }
+ }}
>
<div class="flex items-center justify-between gap-2">
<span class="font-medium text-sm">{agent.name}</span>
<div class="flex gap-1 shrink-0">
- <span class="badge badge-xs">{agent.models.length} model{agent.models.length !== 1 ? 's' : ''}</span>
+ <span class="badge badge-xs">{agent.models.length} model{agent.models.length !== 1 ? "s" : ""}</span>
<span class="badge badge-xs badge-outline">{agent.scope === "global" ? "global" : "project"}</span>
</div>
</div>
{#if agent.description}
<p class="text-xs opacity-60 mt-0.5">{agent.description}</p>
{/if}
- </button>
+ {#if isActive && hasMultipleModels}
+ {@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>
+ <input
+ type="range"
+ min="0"
+ max={agent.models.length - 1}
+ value={currentIdx >= 0 ? currentIdx : 0}
+ class="range range-xs"
+ step="1"
+ oninput={(e) => {
+ sliderDragging = Number(e.currentTarget.value);
+ }}
+ onchange={(e) => {
+ const idx = Number(e.currentTarget.value);
+ const m = agent.models[idx];
+ if (m) onModelChange(m.key_id, m.model_id);
+ sliderDragging = null;
+ }}
+ onclick={(e) => e.stopPropagation()}
+ onkeydown={(e) => e.stopPropagation()}
+ />
+ <div class="flex w-full justify-between px-0.5 text-xs opacity-50 mt-0.5">
+ {#each agent.models as _, i}
+ <span>{i + 1}</span>
+ {/each}
+ </div>
+ </div>
+ {/if}
+ </div>
{/each}
</div>
{/if}
+
<button
type="button"
class="btn btn-outline btn-sm w-full mt-2 hover:bg-base-300 hover:border-base-300 text-base-content/60"
diff --git a/packages/frontend/src/lib/tabs.svelte.ts b/packages/frontend/src/lib/tabs.svelte.ts
index a48be64..5d3ab63 100644
--- a/packages/frontend/src/lib/tabs.svelte.ts
+++ b/packages/frontend/src/lib/tabs.svelte.ts
@@ -431,6 +431,15 @@ function createTabStore() {
}
break;
}
+ case "model-changed": {
+ if (tabId) {
+ updateTab(tabId, {
+ keyId: event.keyId,
+ modelId: event.modelId,
+ });
+ }
+ break;
+ }
case "permission-prompt": {
pendingPermissions = event.pending;
break;
diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts
index 95ffb2b..532c948 100644
--- a/packages/frontend/src/lib/types.ts
+++ b/packages/frontend/src/lib/types.ts
@@ -53,6 +53,7 @@ export type AgentEvent =
}
| { type: "error"; error: string }
| { type: "notice"; message: string }
+ | { type: "model-changed"; keyId: string; modelId: string }
| { type: "task-list-update"; tasks: TaskItem[] }
| { type: "config-reload" }
| {