/** * Interactive configuration wizard for /subagents-setup. * * The top-level menu exposes enabled roles, per-agent model and thinking choices, * and a full setup pass. Everything else (scope, concurrency, timeout, result lines) * is config-file-only. */ import { stat } from "node:fs/promises"; import type { Api, Model } from "@earendil-works/pi-ai"; import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import { discoverAgents } from "../delegation/agents.ts"; import { AGENT_PROFILES, BUILTIN_AGENT_NAMES, DEFAULT_CONFIG, DEFAULT_ENABLED_AGENTS, agentProfile, errorMessage, getConfigPath, loadConfig, roleThinkingLevel, saveConfig, type SubagentsConfig, type ThinkingLevel, } from "./config.ts"; import { CURRENT_MAIN_MODEL, applyAgentModelChoice, availableModelsInScope, buildModelPickerItems, currentModelRef, findModelByRef, modelRef, resolveThinkingLevel, supportedThinkingLevels, } from "./models.ts"; import { promptSelectMany, promptSelectOne } from "./ui.ts"; const THINKING_LEVEL_HINTS: Record = { off: "no reasoning tokens", minimal: "minimal reasoning", low: "light reasoning", medium: "balanced reasoning", high: "deep reasoning", xhigh: "extra-deep reasoning", max: "strongest reasoning", }; function setupAgentNames(ctx: ExtensionCommandContext, config: SubagentsConfig): string[] { const { agents } = discoverAgents(ctx.cwd, { scope: config.agentScope, projectTrusted: ctx.isProjectTrusted?.() === true, }); const available = new Set(agents.map((agent) => agent.name)); return [...new Set([...BUILTIN_AGENT_NAMES, ...available])].filter((name) => available.has(name)); } function agentPickerItems(names: readonly string[]): Array<{ value: string; label: string; description: string }> { return names.map((name) => { const profile = agentProfile(name); return { value: name, label: profile ? name : `${name} (custom)`, description: profile ? `${profile.summary} — ${profile.remark}` : "custom agent", }; }); } function moduleLabel(name: string): string { const profile = agentProfile(name); return profile ? `${name} — ${profile.summary}` : `${name} (custom)`; } async function configExists(configPath: string): Promise { try { await stat(configPath); return true; } catch { return false; } } async function pickEnabledAgents( ctx: ExtensionCommandContext, config: SubagentsConfig, ): Promise { const names = setupAgentNames(ctx, config); return promptSelectMany( ctx, "Which roles should be available?", "Each line is a role and its job. Space toggles • Enter confirms • Esc back", agentPickerItems(names), config.enabledAgents.filter((name) => names.includes(name)), ); } async function pickConfiguredModel( ctx: ExtensionCommandContext, title: string, configuredRef: string | undefined, escNote: string, ): Promise { const models = availableModelsInScope(ctx); const items = buildModelPickerItems({ models, configuredRef, mainRef: currentModelRef(ctx), }); return promptSelectOne( ctx, title, `Type to filter by provider, model, or capability • ↑/↓ • Enter selects • Esc ${escNote}`, items, configuredRef ?? CURRENT_MAIN_MODEL, ); } async function pickAgentModel( ctx: ExtensionCommandContext, agentName: string, agentModels: Readonly>, escNote = "cancels this setup pass", ): Promise { const profile = agentProfile(agentName); const duty = profile ? ` — ${profile.summary}` : ""; return pickConfiguredModel( ctx, `Model for ${agentName}${duty}?`, agentModels[agentName], escNote, ); } function effectiveModelForChoice( ctx: ExtensionCommandContext, agentName: string, choice: string, agentModels: Readonly>, ): Model | undefined { const selectedModels = applyAgentModelChoice({ ...agentModels }, agentName, choice); const ref = selectedModels[agentName]; return findModelByRef(availableModelsInScope(ctx), ref) ?? ctx.model; } /** The role default is marked; picking it clears a stored override. */ async function pickAgentStrength( ctx: ExtensionCommandContext, agentName: string, model: Model | undefined, current: ThinkingLevel | undefined, escNote = "cancels this setup pass", ): Promise { const supported = supportedThinkingLevels(model); const roleDefault = resolveThinkingLevel(model, roleThinkingLevel(agentName)); if (supported.length <= 1) return roleDefault; const currentEffective = current ? resolveThinkingLevel(model, current) : roleDefault; const modelName = model ? modelRef(model) : "current main model"; const options = supported.map((level) => { const tags = [ level === roleDefault ? "role default" : "", current !== undefined && currentEffective === level ? "current" : "", ].filter(Boolean); return { value: level, label: `${level} — ${THINKING_LEVEL_HINTS[level]}${tags.length ? ` (${tags.join(", ")})` : ""}`, }; }); return promptSelectOne( ctx, `Thinking for ${agentName}?`, `${agentName} defaults to ${roleDefault} on ${modelName} • Enter selects • Esc ${escNote}`, options, currentEffective, ) as Promise; } async function pickAgentToConfigure( ctx: ExtensionCommandContext, enabledAgents: readonly string[], ): Promise { if (enabledAgents.length === 0) { ctx.ui.notify("No agents are enabled. Enable agents first.", "warning"); return undefined; } return promptSelectOne( ctx, "Configure which agent?", "Name, then what it owns • ↑/↓ • Enter selects • Esc returns to settings", enabledAgents.map((name) => { const profile = agentProfile(name); return { value: name, label: moduleLabel(name), description: profile?.remark, }; }), ); } interface ConfiguredAgentChoice { name: string; model: string; strength: ThinkingLevel; } async function configureOneAgent( ctx: ExtensionCommandContext, config: SubagentsConfig, ): Promise { const available = setupAgentNames(ctx, config); const enabled = config.enabledAgents.filter((name) => available.includes(name)); while (true) { const name = await pickAgentToConfigure(ctx, enabled); if (name === undefined) return undefined; const profile = agentProfile(name); if (profile) ctx.ui.notify(`${name}: ${profile.remark}`, "info"); while (true) { const modelChoice = await pickAgentModel( ctx, name, config.agentModels, "returns to agent selection", ); if (modelChoice === undefined) break; const model = effectiveModelForChoice(ctx, name, modelChoice, config.agentModels); const strength = await pickAgentStrength( ctx, name, model, config.agentThinkingLevels[name], "returns to model selection", ); if (strength === undefined) continue; return { name, model: modelChoice, strength }; } } } function keepAgentEntries(record: Record, enabled: readonly string[]): Record { const keep = new Set(enabled); return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name))); } function applyThinkingChoice( levels: Record, agentName: string, strength: ThinkingLevel, model: Model | undefined, ): Record { const next = { ...levels }; const roleDefault = resolveThinkingLevel(model, roleThinkingLevel(agentName)); if (strength === roleDefault) delete next[agentName]; else next[agentName] = resolveThinkingLevel(model, strength); return next; } async function introduceSetup(ctx: ExtensionCommandContext): Promise { const lines = BUILTIN_AGENT_NAMES.map((name) => { const profile = AGENT_PROFILES[name]; return `${name} — ${profile.summary}. ${profile.remark}`; }); const defaults = BUILTIN_AGENT_NAMES.map((name) => `${name} ${roleThinkingLevel(name)}`).join(", "); ctx.ui.notify( `pi-subagents: ${lines.join(" ")} Pick a model for each role next. Thinking defaults per role (${defaults}); change it on a role when you want.`, "info", ); const choice = await ctx.ui.select("How to configure pi-subagents", [ "Continue — pick a model for each role (thinking has a role default you can change later)", ]); return choice !== undefined; } async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise { if (!(await introduceSetup(ctx))) return false; const enabled = await pickEnabledAgents(ctx, base); if (enabled === undefined) return false; let agentModels = keepAgentEntries(base.agentModels, enabled); for (const agentName of enabled) { const profile = agentProfile(agentName); if (profile) ctx.ui.notify(`${agentName}: ${profile.remark}`, "info"); const choice = await pickAgentModel(ctx, agentName, agentModels); if (choice === undefined) return false; agentModels = applyAgentModelChoice(agentModels, agentName, choice); } const next: SubagentsConfig = { enabledAgents: enabled, knownAgents: setupAgentNames(ctx, base), agentModels, agentThinkingLevels: keepAgentEntries(base.agentThinkingLevels, enabled), maxResultLines: base.maxResultLines, maxConcurrentAgents: base.maxConcurrentAgents, agentScope: base.agentScope, idleTimeoutSec: base.idleTimeoutSec, }; await saveConfig(next, configPath); ctx.ui.notify( `pi-subagents saved to ${configPath}. Role thinking defaults apply; open Configure an agent to change one.`, "info", ); return true; } async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise { while (true) { const choice = await ctx.ui.select("pi-subagents settings", [ "Enable/disable agents — choose which roles are available", "Configure an agent — model and thinking, with its job on the row", "Full re-setup — walk through the team and pick models again", ]); if (choice === undefined) return; if (choice.startsWith("Full")) { if (await runFullSetup(ctx, configPath, config)) return; continue; } let next: SubagentsConfig = { ...config, agentModels: { ...config.agentModels }, agentThinkingLevels: { ...config.agentThinkingLevels }, }; if (choice.startsWith("Enable")) { const enabled = await pickEnabledAgents(ctx, config); if (enabled === undefined) continue; next.enabledAgents = enabled; next.knownAgents = setupAgentNames(ctx, config); next.agentModels = keepAgentEntries(next.agentModels, enabled); next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled); } else { let configuredAny = false; while (true) { const picked = await configureOneAgent(ctx, next); if (!picked) break; configuredAny = true; next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model); const model = effectiveModelForChoice(ctx, picked.name, picked.model, next.agentModels); next.agentThinkingLevels = applyThinkingChoice( next.agentThinkingLevels, picked.name, picked.strength, model, ); } if (!configuredAny) continue; await saveConfig(next, configPath); ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info"); config = next; continue; } await saveConfig(next, configPath); ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info"); return; } } /** Entry point for the /subagents-setup command. */ export async function runSetup(ctx: ExtensionCommandContext, configPath: string = getConfigPath()): Promise { if (ctx.mode !== "tui") { ctx.ui.notify("/subagents-setup requires Pi's interactive TUI.", "error"); return; } try { const exists = await configExists(configPath); const config = await loadConfig(configPath); if (exists) await runMenu(ctx, configPath, config); else if (!(await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] }))) { ctx.ui.notify("pi-subagents setup cancelled.", "info"); } } catch (error) { ctx.ui.notify(`pi-subagents setup failed: ${errorMessage(error)}`, "error"); } }