From 6319bf0fd4599cbcb772c9bfab21447295393ea4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 13:08:44 +0000 Subject: [PATCH] feat(app): model backend picker on the spawn screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fetch the registered model backends (/claude/models) alongside the fleet poll and offer the enabled ones in a Model select on the spawn form, defaulting to the Claude subscription — the mobile counterpart of the web dashboard's per-spawn dropdown. The select is hidden when no backends are registered, and Select itself now takes value/label pairs as well as bare strings. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48 --- app/src/components/Select.tsx | 23 ++++++++++++++++------- app/src/screens/SpawnScreen.tsx | 23 +++++++++++++++++++++-- app/src/state/AppState.tsx | 27 ++++++++++++++++++++++++--- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/app/src/components/Select.tsx b/app/src/components/Select.tsx index fc62a2d..96b3ed8 100644 --- a/app/src/components/Select.tsx +++ b/app/src/components/Select.tsx @@ -14,7 +14,11 @@ import { Icon } from "./Icon"; * Leeworks Select, ported from components/forms/Select.jsx. Native + ) : null} + Task diff --git a/app/src/state/AppState.tsx b/app/src/state/AppState.tsx index 68e187f..659a1e2 100644 --- a/app/src/state/AppState.tsx +++ b/app/src/state/AppState.tsx @@ -14,6 +14,7 @@ import { type ApiClient, type ApiError, type Checkmark, + type ClaudeModel, type LogEntry, type Project, } from "../api/client"; @@ -95,6 +96,8 @@ interface AppStateValue { loading: boolean; error: string | null; projects: Project[]; + /* Registered model backends (the spawn/schedule dropdown next to the subscription). */ + models: ClaudeModel[]; waiting: WaitingItem[]; recent: RecentItem[]; counts: { running: number; waiting: number; done: number }; @@ -108,7 +111,7 @@ interface AppStateValue { // Mutations. sendAnswer: (text: string) => Promise<{ resumed: boolean; note?: string }>; - spawn: (project: string, task: string) => Promise; + spawn: (project: string, task: string, modelId?: number | null) => Promise; kill: (project: string, name: string) => Promise; } @@ -152,6 +155,7 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) { // Fleet data. const [projects, setProjects] = useState([]); + const [models, setModels] = useState([]); const [agentsByProject, setAgentsByProject] = useState>({}); const [checkmarks, setCheckmarks] = useState>({}); const [logsByAgent, setLogsByAgent] = useState>({}); @@ -166,6 +170,7 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) { const resetData = useCallback(() => { setProjects([]); + setModels([]); setAgentsByProject({}); setCheckmarks({}); setLogsByAgent({}); @@ -190,6 +195,15 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) { setError(null); const projs = await client.api("/projects"); + // Model backends feed the spawn dropdown; an older server without the endpoint + // (404) just means "subscription only", so failures leave the list empty. + const modelList = await client + .api("/claude/models") + .catch((e) => { + if (e instanceof AuthError) throw e; + return [] as ClaudeModel[]; + }); + // Per-agent/per-project sub-requests are isolated: one flaky agent (a 500 on // its log, say) must not blank the whole fleet. A rejected AuthError still // propagates via onUnauthorized inside the client; here we just record the @@ -249,6 +263,7 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) { for (const [k, v] of logEntries) logMap[k] = v; setProjects(projs); + setModels(modelList); setAgentsByProject(abp); setCheckmarks(cmMap); setLogsByAgent(logMap); @@ -424,11 +439,15 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) { ); const spawn = useCallback( - async (project: string, task: string) => { + async (project: string, task: string, modelId?: number | null) => { if (!client) throw new Error("not connected"); const name = deriveAgentName(task); await client.api(`/projects/${enc(project)}/agents/spawn`, { - body: { name, ...(task.trim() ? { task: task.trim() } : {}) }, + body: { + name, + ...(task.trim() ? { task: task.trim() } : {}), + ...(modelId != null ? { model_id: modelId } : {}), + }, }); await refresh(); }, @@ -460,6 +479,7 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) { loading, error, projects, + models, waiting, recent, counts, @@ -483,6 +503,7 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) { loading, error, projects, + models, waiting, recent, counts,