feat: per-CLI model picker for local agents (closes #8)

Each agent CLI declares its selectable models (and reasoning effort, for
Codex) on the daemon side; the frontend renders a model dropdown in the
avatar menu and the Settings dialog scoped to the currently picked CLI,
persists the choice per-agent in the AppConfig, and threads it through
/api/chat to the spawn argv. "Default" leaves the flag off so the CLI's
own config wins.
This commit is contained in:
pftom
2026-04-28 22:54:18 +08:00
parent 94941f59a9
commit f2d28a1cca
13 changed files with 414 additions and 25 deletions
+17 -1
View File
@@ -690,6 +690,8 @@ export async function startServer({ port = 7456 } = {}) {
imagePaths = [],
projectId,
attachments = [],
model,
reasoning,
} = req.body || {};
const def = getAgentDef(agentId);
if (!def) return res.status(400).json({ error: `unknown agent: ${agentId}` });
@@ -779,7 +781,19 @@ export async function startServer({ port = 7456 } = {}) {
const extraAllowedDirs = [SKILLS_DIR, DESIGN_SYSTEMS_DIR].filter(
(d) => fs.existsSync(d),
);
const args = def.buildArgs(composed, safeImages, extraAllowedDirs);
// Per-agent model + reasoning the user picked in the model menu.
// Validated against the agent's declared options so a stale or hostile
// value can't smuggle arbitrary flags into the spawned argv.
const safeModel =
typeof model === 'string' && Array.isArray(def.models)
? def.models.find((m) => m.id === model)?.id ?? null
: null;
const safeReasoning =
typeof reasoning === 'string' && Array.isArray(def.reasoningOptions)
? def.reasoningOptions.find((r) => r.id === reasoning)?.id ?? null
: null;
const agentOptions = { model: safeModel, reasoning: safeReasoning };
const args = def.buildArgs(composed, safeImages, extraAllowedDirs, agentOptions);
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache, no-transform');
@@ -798,6 +812,8 @@ export async function startServer({ port = 7456 } = {}) {
streamFormat: def.streamFormat ?? 'plain',
projectId: typeof projectId === 'string' ? projectId : null,
cwd,
model: safeModel,
reasoning: safeReasoning,
});
let child;