diff options
| author | Adam Malczewski <[email protected]> | 2026-05-23 17:16:33 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-23 17:16:33 +0900 |
| commit | e0d8663103e8fc0b0cdaf523ca3f48849848ed69 (patch) | |
| tree | a3a350ceea0596d44d2661234aaacf5d19f73977 | |
| parent | 236beefb708a6cd91b673978ddf4ebf045a9844c (diff) | |
| download | dispatch-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.ts | 4 | ||||
| -rw-r--r-- | packages/core/src/types/index.ts | 1 | ||||
| -rw-r--r-- | packages/frontend/src/lib/components/ModelSelector.svelte | 61 | ||||
| -rw-r--r-- | packages/frontend/src/lib/tabs.svelte.ts | 9 | ||||
| -rw-r--r-- | packages/frontend/src/lib/types.ts | 1 |
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" } | { |
