import { spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { getAgentDir, getSelectListTheme, getSettingsListTheme, parseFrontmatter, renderDiff, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Container, Input, SelectList, SettingsList, Spacer, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui"; import { Type } from "typebox"; type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; type StatusView = "minimal" | "default" | "detailed"; type ExecutionMode = "parallel" | "sequential"; type AgentName = string; type Role = string; type BillingOverride = "api" | "subscription"; type AuthType = "oauth" | "api_key"; interface RoleTotals { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; requests: number; lastModel?: string; } interface UsageDelta { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; requests?: number; provider?: string; model?: string; } interface UsageSnapshot { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; } interface OrchestratorRoleConfig { provider: string; model: string; thinking: ThinkingLevel; } interface OrchestratorConfig { orchestrator?: OrchestratorRoleConfig; enabled?: boolean; statusWidget?: boolean; statusView?: StatusView; replaceFooter?: boolean; } interface AgentConfig { name: string; description: string; provider: string; model: string; thinking: ThinkingLevel; execution: ExecutionMode; tools: string[]; systemPrompt: string; billing?: BillingOverride; enabled: boolean; } interface ActivityItem { kind: "tool" | "text"; toolCallId?: string; tool?: string; summary: string; detail?: string; isError?: boolean; pending?: boolean; } interface DelegationDetails { agent: string; provider: string; model: string; thinking: ThinkingLevel; cwd: string; sessionId: string; resume: "new" | "resumed" | "not_found"; turns: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cost: number; exitCode: number; stderr: string; activity: ActivityItem[]; status: "running" | "done" | "failed"; report?: string; droppedCount: number; } const delegateParams = Type.Object({ task: Type.String({ description: "Specific task to delegate." }), cwd: Type.Optional(Type.String({ description: "Working directory for the delegated Pi process." })), sessionId: Type.Optional( Type.String({ description: "Session id from a previous delegation to continue that agent's session with retained context." }), ), thinking: Type.Optional( Type.Union( [ Type.Literal("off"), Type.Literal("minimal"), Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh"), ], { description: "Override the agent's default thinking level for this delegation." }, ), ), }); const diffParams = Type.Object({ cwd: Type.Optional(Type.String({ description: "Working directory to inspect. Defaults to current project." })), paths: Type.Optional(Type.Array(Type.String(), { description: "Limit the diff to these pathspecs." })), maxBytes: Type.Optional(Type.Number({ description: "Cap on total diff output bytes. Default 80000." })), }); const MAX_ACTIVITY_ITEMS = 200; const MAX_ACTIVITY_DETAIL_BYTES = 120_000; const MAX_EDIT_DETAIL_BYTES = 6_000; const MAX_WRITE_DETAIL_BYTES = 2_000; const MAX_BASH_DETAIL_BYTES = 1_500; const UPDATE_THROTTLE_MS = 80; const BUILTIN_AGENT_NAMES: readonly string[] = ["researcher", "implementor", "design"]; const AGENT_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/; const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; const STATUS_VIEWS: StatusView[] = ["minimal", "default", "detailed"]; const EXECUTION_MODES: ExecutionMode[] = ["parallel", "sequential"]; function isThinkingLevel(value: unknown): value is ThinkingLevel { return typeof value === "string" && THINKING_LEVELS.includes(value as ThinkingLevel); } function isStatusView(value: unknown): value is StatusView { return typeof value === "string" && STATUS_VIEWS.includes(value as StatusView); } function isExecutionMode(value: unknown): value is ExecutionMode { return typeof value === "string" && EXECUTION_MODES.includes(value as ExecutionMode); } function defaultExecutionMode(role: AgentName): ExecutionMode { return role === "researcher" ? "parallel" : "sequential"; } function isBuiltinAgent(name: string): boolean { return BUILTIN_AGENT_NAMES.includes(name); } function agentSortKey(name: string): number { const index = BUILTIN_AGENT_NAMES.indexOf(name); return index >= 0 ? index : BUILTIN_AGENT_NAMES.length; } function delegateToolName(name: AgentName): string { return `delegate_${name}`; } function agentNameFromToolName(toolName: unknown): AgentName | undefined { if (typeof toolName !== "string" || !toolName.startsWith("delegate_")) return undefined; const name = toolName.slice("delegate_".length); return name.length > 0 ? name : undefined; } function isBillingOverride(value: unknown): value is BillingOverride { return value === "api" || value === "subscription"; } function zeroRoleTotals(): RoleTotals { return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, requests: 0 }; } function zeroTotalsRecord(): Record { return { orchestrator: zeroRoleTotals() }; } function resetTotals(totals: Record) { for (const role of Object.keys(totals)) delete totals[role]; totals.orchestrator = zeroRoleTotals(); } function safeNumber(value: unknown): number { return typeof value === "number" && Number.isFinite(value) ? value : 0; } function modelLabel(provider: unknown, model: unknown): string | undefined { return typeof provider === "string" && provider.length > 0 && typeof model === "string" && model.length > 0 ? `${provider}/${model}` : undefined; } function shortModelLabel(lastModel: string | undefined, dropProvider: boolean): string { if (!lastModel) return "unknown"; if (!dropProvider) return lastModel; const slash = lastModel.indexOf("/"); return slash >= 0 ? lastModel.slice(slash + 1) : lastModel; } function cachedPctLabel(cacheRead: number, cacheWrite: number, submitted: number): string { const cached = cacheRead + cacheWrite; if (!cached || !submitted) return ""; return `${Math.round((cached / submitted) * 100)}% cached`; } const extensionDir = path.dirname(fileURLToPath(import.meta.url)); function bundledPath(...segments: string[]): string { return path.join(extensionDir, "..", ...segments); } function getUserAgentPath(name: AgentName): string { return path.join(getAgentDir(), "agents", `${name}.md`); } function getBundledAgentPath(name: AgentName): string { return bundledPath("agents", `${name}.md`); } function resolveAgentPath(name: AgentName): string { const userPath = getUserAgentPath(name); if (fs.existsSync(userPath)) return userPath; return getBundledAgentPath(name); } function getUserAppendSystemPath(): string { return path.join(getAgentDir(), "APPEND_SYSTEM.md"); } function getBundledOrchestratorPromptPath(): string { return bundledPath("prompts", "orchestrator.md"); } function getUserOrchestratorPromptPath(): string { return path.join(getAgentDir(), "prompts", "orchestrator.md"); } function ensureUserOrchestratorPrompt(): string { const filePath = getUserOrchestratorPromptPath(); if (fs.existsSync(filePath)) return filePath; fs.mkdirSync(path.dirname(filePath), { recursive: true }); const bundled = getBundledOrchestratorPromptPath(); fs.writeFileSync(filePath, fs.existsSync(bundled) ? fs.readFileSync(bundled, "utf8") : "", "utf8"); return filePath; } function resetUserOrchestratorPrompt(): string | undefined { const filePath = getUserOrchestratorPromptPath(); if (!fs.existsSync(filePath)) return undefined; const backupPath = `${filePath}.bak`; fs.rmSync(backupPath, { force: true }); fs.renameSync(filePath, backupPath); return backupPath; } function discoverAgentNames(): AgentName[] { const names = new Set(BUILTIN_AGENT_NAMES); for (const dir of [bundledPath("agents"), path.join(getAgentDir(), "agents")]) { try { for (const entry of fs.readdirSync(dir)) { if (!entry.endsWith(".md")) continue; const name = entry.slice(0, -3); if (AGENT_NAME_RE.test(name)) names.add(name); } } catch { // Missing directory is fine; builtins remain. } } return [...names].sort((a, b) => agentSortKey(a) - agentSortKey(b) || a.localeCompare(b)); } function zeroUsageSnapshot(): UsageSnapshot { return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 }; } function usageSnapshotFromUsage(usage: any): UsageSnapshot { return { input: safeNumber(usage?.input), output: safeNumber(usage?.output), cacheRead: safeNumber(usage?.cacheRead), cacheWrite: safeNumber(usage?.cacheWrite), cost: safeNumber(usage?.cost?.total), }; } function usageDeltaFromSnapshots( previous: UsageSnapshot | undefined, next: UsageSnapshot | undefined, requests = 0, provider?: string, model?: string, ): UsageDelta { const before = previous ?? zeroUsageSnapshot(); const after = next ?? zeroUsageSnapshot(); return { input: Math.max(0, after.input - before.input), output: Math.max(0, after.output - before.output), cacheRead: Math.max(0, after.cacheRead - before.cacheRead), cacheWrite: Math.max(0, after.cacheWrite - before.cacheWrite), cost: Math.max(0, after.cost - before.cost), requests, provider, model, }; } function usageDeltaFromUsage(usage: any, requests = 0, provider?: string, model?: string): UsageDelta { return { ...usageSnapshotFromUsage(usage), requests, provider, model }; } function usageSnapshotsEqual(left: UsageSnapshot | undefined, right: UsageSnapshot | undefined): boolean { if (!left || !right) return left === right; return ( left.input === right.input && left.output === right.output && left.cacheRead === right.cacheRead && left.cacheWrite === right.cacheWrite && left.cost === right.cost ); } function hasUsageDeltaValues(delta: UsageDelta): boolean { return Boolean(delta.input || delta.output || delta.cacheRead || delta.cacheWrite || delta.cost || (delta.requests ?? 0)); } function submittedInputCount(input: number, cacheRead: number, cacheWrite: number): number { return input + cacheRead + cacheWrite; } function addUsageDeltaToTotals(totals: Record, role: Role, delta: UsageDelta) { const target = totals[role] ?? (totals[role] = zeroRoleTotals()); target.input += delta.input; target.output += delta.output; target.cacheRead += delta.cacheRead; target.cacheWrite += delta.cacheWrite; target.cost += delta.cost; target.requests += delta.requests ?? 0; const label = modelLabel(delta.provider, delta.model); if (label) target.lastModel = label; } function hasRoleActivity(total: RoleTotals): boolean { return Boolean(total.requests || total.input || total.output || total.cacheRead || total.cacheWrite || total.cost); } function compactTokenCount(value: number): string { const sign = value < 0 ? "-" : ""; const abs = Math.abs(value); if (abs < 10_000) return `${sign}${Math.round(abs)}`; if (abs < 100_000) return `${sign}${(abs / 1_000).toFixed(1)}k`; if (abs < 1_000_000) return `${sign}${Math.round(abs / 1_000)}k`; if (abs < 10_000_000) return `${sign}${(abs / 1_000_000).toFixed(1)}M`; return `${sign}${Math.round(abs / 1_000_000)}M`; } function getOrchestratorConfigPath(): string { return path.join(getAgentDir(), "orchestrator-config.json"); } function normalizeOrchestratorConfig(value: any): OrchestratorConfig { const config: OrchestratorConfig = {}; if (!value || typeof value !== "object") return config; const orchestrator = value.orchestrator; if ( orchestrator && typeof orchestrator === "object" && typeof orchestrator.provider === "string" && typeof orchestrator.model === "string" ) { config.orchestrator = { provider: orchestrator.provider, model: orchestrator.model, thinking: isThinkingLevel(orchestrator.thinking) ? orchestrator.thinking : "medium", }; } if (typeof value.enabled === "boolean") config.enabled = value.enabled; if (typeof value.statusWidget === "boolean") config.statusWidget = value.statusWidget; if (isStatusView(value.statusView)) config.statusView = value.statusView; if (typeof value.replaceFooter === "boolean") config.replaceFooter = value.replaceFooter; return config; } function loadOrchestratorConfig(): OrchestratorConfig { try { const filePath = getOrchestratorConfigPath(); if (!fs.existsSync(filePath)) return {}; return normalizeOrchestratorConfig(JSON.parse(fs.readFileSync(filePath, "utf8"))); } catch { return {}; } } function isSystemEnabled(config = loadOrchestratorConfig()): boolean { return config.enabled !== false; } function saveOrchestratorConfig(patch: Partial): OrchestratorConfig { const current = loadOrchestratorConfig(); const next: OrchestratorConfig = { ...current }; if (patch.orchestrator) next.orchestrator = patch.orchestrator; if (typeof patch.enabled === "boolean") next.enabled = patch.enabled; if (typeof patch.statusWidget === "boolean") next.statusWidget = patch.statusWidget; if (isStatusView(patch.statusView)) next.statusView = patch.statusView; if (typeof patch.replaceFooter === "boolean") next.replaceFooter = patch.replaceFooter; fs.mkdirSync(path.dirname(getOrchestratorConfigPath()), { recursive: true }); fs.writeFileSync(getOrchestratorConfigPath(), `${JSON.stringify(next, null, 2)}\n`, "utf8"); return next; } function customAgentTemplate(name: AgentName): string { const title = `${name[0].toUpperCase()}${name.slice(1)}`; return `--- name: ${name} description: ${title} subagent. Edit this description so the orchestrator knows exactly when to delegate to it. provider: anthropic model: claude-sonnet-5 thinking: medium execution: sequential tools: read,bash,grep,find,ls enabled: true --- # ${title} You are the ${name} subagent. Describe this agent's job here. The orchestrator sends a task brief; only this system prompt and that brief are your context. ## Responsibilities - Replace with the agent's concrete responsibilities. - Stay within the task given by the orchestrator; report ambiguity instead of guessing. - Do not perform final acceptance review. Return the result to the orchestrator. ## Output Budget Keep the report under ~80 lines; cite \`path:line\` instead of pasting whole files. ## Output Format 1. Summary 2. Findings Or Changes 3. Validation Run 4. Remaining Risks Or Questions `; } function scaffoldCustomAgent(name: AgentName): string { const filePath = getUserAgentPath(name); if (fs.existsSync(filePath)) throw new Error(`Agent file already exists: ${filePath}`); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, customAgentTemplate(name), "utf8"); return filePath; } function ensureUserAgentFile(role: AgentName): string { const filePath = getUserAgentPath(role); if (fs.existsSync(filePath)) return filePath; const fallbackPath = getBundledAgentPath(role); if (!fs.existsSync(fallbackPath)) { throw new Error(`Agent ${role} not found in ${filePath} or bundled fallback ${fallbackPath}`); } fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.copyFileSync(fallbackPath, filePath); return filePath; } function updateAgentFrontmatter(role: AgentName, updates: Record) { const filePath = ensureUserAgentFile(role); const content = fs.readFileSync(filePath, "utf8"); const match = content.match(/^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/); if (!match) throw new Error(`Agent ${role} is missing a leading frontmatter block in ${filePath}`); const newline = match[1].includes("\r\n") || match[3].includes("\r\n") ? "\r\n" : "\n"; const lines = match[2].length > 0 ? match[2].split(/\r?\n/) : []; for (const [key, value] of Object.entries(updates)) { const line = `${key}: ${value}`; const index = lines.findIndex((candidate) => new RegExp(`^\\s*${key}\\s*:`).test(candidate)); if (index >= 0) lines[index] = line; else lines.push(line); } const next = `${match[1]}${lines.join(newline)}${match[3]}${content.slice(match[0].length)}`; fs.writeFileSync(filePath, next, "utf8"); } function loadAgent(name: AgentName): AgentConfig { const filePath = resolveAgentPath(name); if (!fs.existsSync(filePath)) throw new Error(`Agent ${name} not found at ${filePath}`); const content = fs.readFileSync(filePath, "utf8"); const { frontmatter, body } = parseFrontmatter>(content); const provider = frontmatter.provider?.trim(); const model = frontmatter.model?.trim(); const thinking = (frontmatter.thinking?.trim() || "medium") as ThinkingLevel; const executionValue = frontmatter.execution?.trim(); const execution = isExecutionMode(executionValue) ? executionValue : defaultExecutionMode(name); const billingValue = frontmatter.billing?.trim(); const billing = isBillingOverride(billingValue) ? billingValue : undefined; const enabled = frontmatter.enabled?.trim() !== "false"; const tools = (frontmatter.tools || "") .split(",") .map((tool) => tool.trim()) .filter(Boolean); if (!provider || !model) { throw new Error(`Agent ${name} must define provider and model in ${filePath}`); } return { name: frontmatter.name || name, description: frontmatter.description || name, provider, model, thinking, execution, tools, systemPrompt: body.trim(), billing, enabled, }; } function loadAgentSafe(name: AgentName): AgentConfig | undefined { try { return loadAgent(name); } catch { return undefined; } } let sequentialChain: Promise = Promise.resolve(); function abortError(): Error { return new Error("Tool call was aborted before queued execution started."); } function throwIfAborted(signal: AbortSignal | undefined) { if (signal?.aborted) throw abortError(); } function runExclusive(fn: () => Promise, signal?: AbortSignal): Promise { const previous = sequentialChain; let started = false; const run = previous.catch(() => undefined).then(async () => { throwIfAborted(signal); started = true; return fn(); }); sequentialChain = run.then( () => undefined, () => undefined, ); if (!signal) return run; if (signal.aborted && !started) return Promise.reject(abortError()); let abortListener: (() => void) | undefined; const abortWhileQueued = new Promise((_resolve, reject) => { abortListener = () => { if (!started) reject(abortError()); }; signal.addEventListener("abort", abortListener, { once: true }); if (signal.aborted) abortListener(); }); return Promise.race([run, abortWhileQueued]).finally(() => { if (abortListener) signal.removeEventListener("abort", abortListener); }); } const AGENT_BLOCK_RE = /[ \t]*[ \t]*\r?\n?([\s\S]*?)[ \t]*[ \t]*\r?\n?/g; /** * Assemble the orchestrator system prompt for the current team: * - Base text is the user's editable copy (~/.pi/agent/prompts/orchestrator.md) when present, else the bundled default. * - `` blocks are kept only while that agent exists and is enabled. * - Enabled custom agents get an auto-generated roster section; disabled agents get an explicit do-not-use note. */ function buildOrchestratorPrompt(agentNames: AgentName[]): string | undefined { if (fs.existsSync(getUserAppendSystemPath())) return undefined; const userPath = getUserOrchestratorPromptPath(); const basePath = fs.existsSync(userPath) ? userPath : getBundledOrchestratorPromptPath(); if (!fs.existsSync(basePath)) return undefined; const agents = new Map(); for (const name of agentNames) agents.set(name, loadAgentSafe(name)); const isEnabled = (name: AgentName) => agents.get(name)?.enabled === true; let text = fs.readFileSync(basePath, "utf8"); text = text .replace(AGENT_BLOCK_RE, (_match, name: string, body: string) => (isEnabled(name) ? body : "")) .replace(/[ \t]*[ \t]*\r?\n?/g, "") .replace(/\n{3,}/g, "\n\n") .trim(); const customAgents = agentNames.filter((name) => !isBuiltinAgent(name) && isEnabled(name)); if (customAgents.length > 0) { const lines = customAgents.map((name) => { const agent = agents.get(name)!; return `- ${delegateToolName(name)} (${agent.execution}): ${agent.description}`; }); text += `\n\n## Custom Agents\n\nThese additional delegate tools are available. Brief them with the same Goal/Context/Constraints/Acceptance criteria format, and remember they start with zero context:\n\n${lines.join("\n")}`; } const disabledAgents = agentNames.filter((name) => agents.get(name) && !isEnabled(name)); if (disabledAgents.length > 0) { const toolList = disabledAgents.map((name) => delegateToolName(name)).join(", "); text += `\n\n## Disabled Agents\n\nThese delegate tools are disabled and unavailable: ${toolList}. Do not reference or wait for them; handle that work directly or route it to an enabled agent.`; } return text.length > 0 ? text : undefined; } function delegatePromptSnippet(role: AgentName, purpose: string, suffix = ""): string { const toolName = `delegate_${role}`; try { const agent = loadAgent(role); return `${toolName}: ${purpose} via ${agent.provider}/${agent.model}:${agent.thinking} (reconfigure with /team).${suffix}`; } catch { return `${toolName}: ${purpose} via the configured ${role} model — see /team.${suffix}`; } } function textFromMessage(message: any): string { if (!message || !Array.isArray(message.content)) return ""; return message.content .filter((part: any) => part?.type === "text" && typeof part.text === "string") .map((part: any) => part.text) .join("\n"); } function textFromContent(content: any): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .filter((part: any) => part?.type === "text" && typeof part.text === "string") .map((part: any) => part.text) .join("\n"); } function oneLine(text: string, maxChars: number): string { const collapsed = text.replace(/\s+/g, " ").trim(); if (collapsed.length <= maxChars) return collapsed; if (maxChars <= 1) return "…"; return `${collapsed.slice(0, maxChars - 1)}…`; } function compactJson(value: unknown, maxChars: number): string { let text: string; try { text = JSON.stringify(value ?? {}); } catch { text = String(value); } return oneLine(text, maxChars); } function capBytes(text: string, maxBytes: number, marker = "\n… truncated"): string { if (Buffer.byteLength(text, "utf8") <= maxBytes) return text; const markerBytes = Buffer.byteLength(marker, "utf8"); const bodyMaxBytes = Math.max(0, maxBytes - markerBytes); let capped = text.slice(0, bodyMaxBytes); while (Buffer.byteLength(capped, "utf8") > bodyMaxBytes) capped = capped.slice(0, -1); return `${capped}${marker}`; } function tailBytes(text: string, maxBytes: number): string { if (Buffer.byteLength(text, "utf8") <= maxBytes) return text; const marker = "… truncated\n"; const tailMaxBytes = Math.max(0, maxBytes - Buffer.byteLength(marker, "utf8")); let tail = text.slice(Math.max(0, text.length - tailMaxBytes)); while (Buffer.byteLength(tail, "utf8") > tailMaxBytes) tail = tail.slice(1); return `${marker}${tail}`; } function lineCount(text: string): number { return text.length > 0 ? text.split(/\r?\n/).length : 0; } function firstLines(text: string, maxLines: number): { text: string; truncated: boolean } { const lines = text.split(/\r?\n/); if (lines.length <= maxLines) return { text, truncated: false }; return { text: lines.slice(0, maxLines).join("\n"), truncated: true }; } function capLines(text: string, maxLines: number, fromEnd = false): { text: string; truncated: boolean } { const lines = text.split(/\r?\n/); if (lines.length <= maxLines) return { text, truncated: false }; const kept = fromEnd ? lines.slice(-maxLines) : lines.slice(0, maxLines); return { text: kept.join("\n"), truncated: true }; } function relPathForDisplay(value: unknown, cwd: string): string { if (typeof value !== "string" || value.length === 0) return "..."; if (!path.isAbsolute(value)) return value; const relative = path.relative(cwd, value); if (relative === "") return "."; if (!relative.startsWith("..") && !path.isAbsolute(relative)) return relative; return value; } function pathArg(args: any): unknown { return args?.path ?? args?.file_path ?? args?.filePath; } function summarizeToolCall(toolName: string, args: any, cwd: string): string { switch (toolName) { case "edit": return `edit ${relPathForDisplay(pathArg(args), cwd)}`; case "write": { const content = typeof args?.content === "string" ? args.content : ""; return `write ${relPathForDisplay(pathArg(args), cwd)} (${lineCount(content)} lines)`; } case "bash": return `bash ${oneLine(String(args?.command ?? "..."), 80)}`; case "read": { let summary = `read ${relPathForDisplay(pathArg(args), cwd)}`; const options: string[] = []; if (args?.offset !== undefined) options.push(`offset=${args.offset}`); if (args?.limit !== undefined) options.push(`limit=${args.limit}`); if (options.length > 0) summary += ` (${options.join(", ")})`; return summary; } case "grep": { const pattern = args?.pattern ?? args?.query; const target = pattern !== undefined && pattern !== "" ? pattern : (pathArg(args) ?? args?.path ?? "."); return `grep ${oneLine(String(target), 80)}`; } case "find": { const target = args?.pattern ?? pathArg(args) ?? "."; return `find ${oneLine(String(target), 80)}`; } case "ls": return `ls ${relPathForDisplay(pathArg(args) ?? ".", cwd)}`; default: return `${toolName} ${compactJson(args, 120)}`; } } function finishedToolDetail(toolName: string, args: any, result: any, cwd: string): { summary: string; detail?: string } { switch (toolName) { case "edit": { const diff = typeof result?.details?.diff === "string" ? result.details.diff : ""; return { summary: `edit ${relPathForDisplay(pathArg(args), cwd)}`, detail: diff ? capBytes(diff, MAX_EDIT_DETAIL_BYTES) : undefined, }; } case "write": { const content = typeof args?.content === "string" ? args.content : ""; const lines = content.split(/\r?\n/); let preview = lines.slice(0, 30).join("\n"); if (lines.length > 30) preview += `\n… ${lines.length - 30} more lines`; return { summary: `write ${relPathForDisplay(pathArg(args), cwd)} (${lineCount(content)} lines)`, detail: preview ? capBytes(preview, MAX_WRITE_DETAIL_BYTES) : undefined, }; } case "bash": { const output = textFromContent(result?.content); return { summary: `bash ${oneLine(String(args?.command ?? "..."), 80)}`, detail: output ? tailBytes(output, MAX_BASH_DETAIL_BYTES) : undefined, }; } case "read": case "grep": case "find": case "ls": return { summary: summarizeToolCall(toolName, args, cwd) }; default: return { summary: summarizeToolCall(toolName, args, cwd) }; } } function cloneActivity(activity: ActivityItem[]): ActivityItem[] { return activity.map((item) => ({ ...item })); } function getPiInvocation(args: string[]): { command: string; args: string[] } { const currentScript = process.argv[1]; const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/"); if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) { return { command: process.execPath, args: [currentScript, ...args] }; } return { command: "pi", args }; } function findSubagentSessionFile(sessionDir: string, sessionId: string): string | undefined { try { let newest: { file: string; mtimeMs: number } | undefined; for (const entry of fs.readdirSync(sessionDir)) { if (!entry.endsWith(`_${sessionId}.jsonl`)) continue; const file = path.join(sessionDir, entry); const stat = fs.statSync(file); if (!stat.isFile()) continue; if (!newest || stat.mtimeMs > newest.mtimeMs) newest = { file: path.resolve(file), mtimeMs: stat.mtimeMs }; } return newest?.file; } catch { return undefined; } } async function runDelegatedAgent( agentName: AgentName, task: string, cwd: string, signal: AbortSignal | undefined, onUpdate: ((partial: AgentToolResult) => void) | undefined, onUsageDelta: ((delta: UsageDelta) => void) | undefined, providedSessionId?: string, thinkingOverride?: ThinkingLevel, ): Promise> { const agent = loadAgent(agentName); const sessionDir = path.join(getAgentDir(), "subagent-sessions"); const effectiveThinking = thinkingOverride ?? agent.thinking; if (providedSessionId !== undefined && !/^[0-9a-zA-Z-]{8,64}$/.test(providedSessionId)) { const message = `Invalid sessionId: ${providedSessionId}. sessionId must match /^[0-9a-zA-Z-]{8,64}$/.`; return { content: [{ type: "text", text: message }], details: { agent: agent.name, provider: agent.provider, model: agent.model, thinking: effectiveThinking, cwd, sessionId: providedSessionId, resume: "not_found", turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cost: 0, exitCode: 1, stderr: message, activity: [], status: "failed", report: message, droppedCount: 0, }, isError: true, } as AgentToolResult; } const sessionId = providedSessionId || randomUUID(); const resumeSessionFile = providedSessionId ? findSubagentSessionFile(sessionDir, providedSessionId) : undefined; const resume: DelegationDetails["resume"] = providedSessionId ? (resumeSessionFile ? "resumed" : "not_found") : "new"; const sessionArgs = resumeSessionFile ? ["--session", resumeSessionFile] : ["--session-id", sessionId]; const args = [ "--mode", "json", "-p", "--session-dir", sessionDir, ...sessionArgs, "--no-extensions", "--provider", agent.provider, "--model", agent.model, "--thinking", effectiveThinking, "--append-system-prompt", agent.systemPrompt, ]; if (agent.tools.length > 0) { args.push("--tools", agent.tools.join(",")); } args.push(`Task from orchestrator:\n\n${task}`); const invocation = getPiInvocation(args); let stdoutBuffer = ""; let stderr = ""; let finalText = ""; let stopReason = ""; let errorMessage = ""; let aborted = false; let turns = 0; let inputTokens = 0; let outputTokens = 0; let cacheReadTokens = 0; let cacheWriteTokens = 0; let cost = 0; let usageSnapshot: UsageSnapshot | undefined; const activity: ActivityItem[] = []; const toolArgsById = new Map(); let droppedCount = 0; let storedDetailBytes = 0; let lastUpdateAt = 0; const dropOldActivity = () => { while (activity.length > MAX_ACTIVITY_ITEMS) { const dropped = activity.shift(); if (!dropped) return; if (dropped.detail) storedDetailBytes -= Buffer.byteLength(dropped.detail, "utf8"); droppedCount += 1; } }; const pushActivity = (item: ActivityItem): ActivityItem => { activity.push(item); dropOldActivity(); return item; }; const setActivityDetail = (item: ActivityItem, detail: string | undefined) => { if (item.detail) storedDetailBytes -= Buffer.byteLength(item.detail, "utf8"); delete item.detail; if (!detail) return; const detailBytes = Buffer.byteLength(detail, "utf8"); if (storedDetailBytes + detailBytes > MAX_ACTIVITY_DETAIL_BYTES) return; item.detail = detail; storedDetailBytes += detailBytes; }; const details = (exitCode: number, status: DelegationDetails["status"] = "running", report?: string): DelegationDetails => ({ agent: agent.name, provider: agent.provider, model: agent.model, thinking: effectiveThinking, cwd, sessionId, resume, turns, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cost, exitCode, stderr, activity: cloneActivity(activity), status, report: report ?? (finalText || undefined), droppedCount, }); const emitUpdate = (force = false) => { if (!onUpdate) return; const now = Date.now(); if (!force && now - lastUpdateAt < UPDATE_THROTTLE_MS) return; lastUpdateAt = now; onUpdate({ content: [{ type: "text", text: finalText || `Running ${agent.name}...` }], details: details(-1, "running"), }); }; const applyUsageDelta = (delta: UsageDelta) => { inputTokens += delta.input; outputTokens += delta.output; cacheReadTokens += delta.cacheRead; cacheWriteTokens += delta.cacheWrite; cost += delta.cost; onUsageDelta?.(delta); }; const processLine = (line: string) => { if (!line.trim()) return; let event: any; try { event = JSON.parse(line); } catch { return; } if (event.type === "tool_execution_start") { const toolName = String(event.toolName ?? "tool"); const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined; const toolArgs = event.args ?? {}; if (toolCallId) toolArgsById.set(toolCallId, toolArgs); pushActivity({ kind: "tool", toolCallId, tool: toolName, summary: summarizeToolCall(toolName, toolArgs, cwd), pending: true, }); emitUpdate(false); } if (event.type === "tool_execution_end") { const toolName = String(event.toolName ?? "tool"); const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined; const toolArgs = (toolCallId ? toolArgsById.get(toolCallId) : undefined) ?? event.args ?? {}; let item = toolCallId ? activity.find((candidate) => candidate.kind === "tool" && candidate.toolCallId === toolCallId && candidate.pending) : undefined; if (!item) { item = pushActivity({ kind: "tool", toolCallId, tool: toolName, summary: summarizeToolCall(toolName, toolArgs, cwd), }); } const refined = finishedToolDetail(toolName, toolArgs, event.result, cwd); item.tool = toolName; item.summary = refined.summary; item.pending = false; item.isError = Boolean(event.isError); setActivityDetail(item, refined.detail); if (toolCallId) toolArgsById.delete(toolCallId); emitUpdate(true); } if (event.type === "message_update" && event.message?.role === "assistant" && event.message.usage) { const nextSnapshot = usageSnapshotFromUsage(event.message.usage); if (!usageSnapshotsEqual(usageSnapshot, nextSnapshot)) { const delta = usageDeltaFromSnapshots(usageSnapshot, nextSnapshot, 0, agent.provider, agent.model); usageSnapshot = nextSnapshot; if (hasUsageDeltaValues(delta)) { applyUsageDelta(delta); emitUpdate(false); } } } if (event.type === "message_end" && event.message?.role === "assistant") { const finalSnapshot = event.message.usage ? usageSnapshotFromUsage(event.message.usage) : undefined; const delta = usageDeltaFromSnapshots(usageSnapshot, finalSnapshot, 1, agent.provider, agent.model); usageSnapshot = undefined; turns += 1; applyUsageDelta(delta); const text = textFromMessage(event.message); if (text) { finalText = text; pushActivity({ kind: "text", summary: oneLine(text, 200) }); } if (event.message.stopReason) stopReason = event.message.stopReason; if (event.message.errorMessage) errorMessage = event.message.errorMessage; emitUpdate(true); } }; const exitCode = await new Promise((resolve) => { const proc = spawn(invocation.command, invocation.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, PI_TELEMETRY: process.env.PI_TELEMETRY || "0", PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK || "1", PI_CACHE_RETENTION: process.env.PI_CACHE_RETENTION || "long", }, }); proc.stdout.on("data", (data) => { stdoutBuffer += data.toString(); const lines = stdoutBuffer.split("\n"); stdoutBuffer = lines.pop() || ""; for (const line of lines) processLine(line); }); proc.stderr.on("data", (data) => { stderr += data.toString(); }); proc.on("close", (code) => { if (stdoutBuffer.trim()) processLine(stdoutBuffer); resolve(code ?? 0); }); proc.on("error", () => resolve(1)); if (signal) { const abort = () => { aborted = true; proc.kill("SIGTERM"); setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 5000); }; if (signal.aborted) abort(); else signal.addEventListener("abort", abort, { once: true }); } }); const failed = aborted || exitCode !== 0 || stopReason === "error" || Boolean(errorMessage); const fallback = errorMessage || stderr.trim() || (aborted ? `${agent.name} was aborted.` : "No output returned."); const resultText = finalText || fallback; const stderrTailBlock = failed && stderr.trim() ? `\n\nStderr (tail):\n\`\`\`\n${stderr.slice(-2000)}\n\`\`\`` : ""; const resultWarning = resume === "not_found" ? `Warning: resume miss — session ${sessionId} was not found in the subagent session dir; the agent started without prior context.\n\n` : ""; const resultBody = stderrTailBlock ? `${resultWarning}${capOutput(resultText, Math.max(0, 32_000 - Buffer.byteLength(stderrTailBlock, "utf8") - Buffer.byteLength(resultWarning, "utf8")), "Delegation result")}${stderrTailBlock}` : `${resultWarning}${capOutput(resultText, Math.max(0, 32_000 - Buffer.byteLength(resultWarning, "utf8")), "Delegation result")}`; return { content: [ { type: "text", text: `${resultBody}${delegationFooter(agent.name, sessionId, resume, turns, inputTokens, outputTokens, cacheReadTokens, cost)}`, }, ], details: details(exitCode, failed ? "failed" : "done", finalText ? `${resultWarning}${finalText}` : undefined), isError: failed, } as AgentToolResult; } async function runGit(args: string[], cwd: string): Promise<{ code: number; stdout: string; stderr: string }> { return new Promise((resolve) => { const proc = spawn("git", args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; proc.stdout.on("data", (data) => { stdout += data.toString(); }); proc.stderr.on("data", (data) => { stderr += data.toString(); }); proc.on("close", (code) => resolve({ code: code ?? 0, stdout, stderr })); proc.on("error", (error) => resolve({ code: 1, stdout, stderr: String(error) })); }); } function formatCount(value: number): string { return value.toLocaleString("en-US"); } function delegationFooter( agentName: string, sessionId: string, resume: DelegationDetails["resume"], turns: number, inputTokens: number, outputTokens: number, cacheReadTokens: number, cost: number, ): string { const costSegment = cost > 0 ? ` · $${cost.toFixed(4)}` : ""; const resumeSegment = resume === "resumed" ? " · resumed" : resume === "not_found" ? " · ⚠ resume miss — started fresh" : ""; const turnLabel = `${formatCount(turns)} ${turns === 1 ? "turn" : "turns"}`; return `\n\n---\n[${agentName} · session ${sessionId}${resumeSegment} · ${turnLabel} · ${formatCount(inputTokens)} in / ${formatCount(outputTokens)} out · cache ${formatCount(cacheReadTokens)} read${costSegment}]`; } function capOutput(text: string, maxBytes: number, label = "Output"): string { if (Buffer.byteLength(text, "utf8") <= maxBytes) return text; const note = `\n\n[${label} truncated to ${formatCount(maxBytes)} bytes.]`; const noteBytes = Buffer.byteLength(note, "utf8"); if (noteBytes >= maxBytes) { let cappedNote = note.slice(0, maxBytes); while (Buffer.byteLength(cappedNote, "utf8") > maxBytes) cappedNote = cappedNote.slice(0, -1); return cappedNote; } const bodyMaxBytes = maxBytes - noteBytes; let capped = text.slice(0, bodyMaxBytes); while (Buffer.byteLength(capped, "utf8") > bodyMaxBytes) capped = capped.slice(0, -1); return `${capped}${note}`; } function isDelegationDetails(details: any): details is DelegationDetails { return Boolean(details && typeof details === "object" && Array.isArray(details.activity) && typeof details.status === "string"); } function formatDelegationStats(details: DelegationDetails): string { const turnLabel = `${formatCount(details.turns)} ${details.turns === 1 ? "turn" : "turns"}`; const submittedInputTokens = submittedInputCount(details.inputTokens, details.cacheReadTokens, details.cacheWriteTokens); const parts = [turnLabel, `${compactTokenCount(submittedInputTokens)}→${compactTokenCount(details.outputTokens)}`]; const cachedPct = cachedPctLabel(details.cacheReadTokens, details.cacheWriteTokens, submittedInputTokens); if (cachedPct) parts.push(cachedPct); if (details.cost > 0) parts.push(`$${details.cost.toFixed(4)}`); return parts.join(" · "); } function delegateHeader(agentLabel: string, details: DelegationDetails, theme: any): string { const agent = theme.fg("accent", theme.bold(agentLabel)); const resumeMiss = details.resume === "not_found" ? theme.fg("warning", " ⚠ resume miss") : ""; if (details.status === "running") { const toolCalls = details.activity.filter((item) => item.kind === "tool").length; const stats = `${formatCount(details.turns)} turns · ${formatCount(toolCalls)} tool calls`; return `${theme.fg("warning", "⏳")} ${agent} ${theme.fg("warning", "working…")}${resumeMiss} ${theme.fg("dim", `· ${stats}`)}`; } const stats = formatDelegationStats(details); const statsText = stats ? theme.fg("dim", ` · ${stats}`) : ""; if (details.status === "failed") return `${theme.fg("error", "✗")} ${agent} ${theme.fg("error", "failed")}${resumeMiss}${statsText}`; return `${theme.fg("success", "✓")} ${agent} ${theme.fg("success", "done")}${resumeMiss}${statsText}`; } function activityLine(item: ActivityItem, theme: any): string { if (item.kind === "text") return theme.fg("muted", `✎ ${item.summary}`); const prefix = item.isError ? "✗ " : "→ "; const suffix = item.pending ? "…" : ""; const line = `${prefix}${item.summary}${suffix}`; return theme.fg(item.isError ? "error" : "dim", line); } function addActivitySummaries(container: Container, details: DelegationDetails, theme: any, limit?: number) { const items = details.activity; const skippedInMemory = limit ? Math.max(0, items.length - limit) : 0; const earlier = details.droppedCount + skippedInMemory; if (earlier > 0) container.addChild(new Text(theme.fg("muted", `… ${formatCount(earlier)} earlier items`), 0, 0)); const toShow = limit ? items.slice(-limit) : items; if (toShow.length === 0 && earlier === 0) { container.addChild(new Text(theme.fg("muted", "(no subagent activity yet)"), 0, 0)); return; } for (const item of toShow) container.addChild(new Text(activityLine(item, theme), 0, 0)); } function detailBlockText(item: ActivityItem, theme: any, maxLines = 40, fromEnd = false): string | null { if (!item.detail) return null; const capped = capLines(item.detail, maxLines, fromEnd); const truncatedLine = capped.truncated ? `\n${theme.fg("dim", "… truncated")}` : ""; if (item.tool === "edit") return `${renderDiff(capped.text)}${truncatedLine}`; const indented = capped.text .split(/\r?\n/) .map((line) => theme.fg("dim", ` ${line}`)) .join("\n"); return `${indented}${truncatedLine}`; } function addToolDetail(container: Container, item: ActivityItem, theme: any, maxLines = 40, fromEnd = false) { const detail = detailBlockText(item, theme, maxLines, fromEnd); if (detail) container.addChild(new Text(detail, 0, 0)); } function fullReportText(result: any, details?: DelegationDetails): string { return details?.report || textFromContent(result?.content) || "(no output)"; } function makeDelegateRenderers(agentLabel: string) { return { renderCall(args: any, theme: any, _context: any) { const task = oneLine(String(args?.task ?? "..."), 100) || "..."; let text = `${theme.fg("toolTitle", theme.bold(agentLabel))} ${theme.fg("accent", task)}`; if (args?.sessionId) text += theme.fg("dim", ` (resume ${String(args.sessionId).slice(0, 8)})`); if (args?.cwd) text += theme.fg("dim", ` cwd=${args.cwd}`); return new Text(text, 0, 0); }, renderResult(result: any, { expanded, isPartial }: { expanded: boolean; isPartial: boolean }, theme: any, _context: any) { const details = result.details; if (!isDelegationDetails(details)) return new Text(textFromContent(result?.content) || "(no output)", 0, 0); const container = new Container(); container.addChild(new Text(delegateHeader(agentLabel, details, theme), 0, 0)); if (isPartial) { if (expanded) { addActivitySummaries(container, details, theme, 20); const detailItems = details.activity.filter((item) => item.kind === "tool" && item.detail).slice(-2); for (const item of detailItems) { container.addChild(new Text(activityLine(item, theme), 0, 0)); addToolDetail(container, item, theme, 24); } return container; } addActivitySummaries(container, details, theme, 5); const lastFinished = [...details.activity] .reverse() .find((item) => item.kind === "tool" && !item.pending); if (lastFinished?.tool === "edit" && lastFinished.detail) addToolDetail(container, lastFinished, theme, 12, true); return container; } if (!expanded) { const report = textFromContent(result?.content) || details.report || "(no output)"; const cappedReport = firstLines(report, 12); container.addChild(new Text(cappedReport.text, 0, 0)); if (cappedReport.truncated) container.addChild(new Text(theme.fg("dim", "… report truncated"), 0, 0)); container.addChild(new Text(theme.fg("dim", "ctrl+o to expand subagent activity"), 0, 0)); return container; } container.addChild(new Text(theme.fg("muted", "── activity ──"), 0, 0)); if (details.droppedCount > 0) { container.addChild(new Text(theme.fg("muted", `… ${formatCount(details.droppedCount)} earlier items`), 0, 0)); } if (details.activity.length === 0) { container.addChild(new Text(theme.fg("muted", "(no subagent activity captured)"), 0, 0)); } for (const item of details.activity) { container.addChild(new Text(activityLine(item, theme), 0, 0)); if (item.kind === "tool") addToolDetail(container, item, theme, 40); } container.addChild(new Text(theme.fg("muted", "── report ──"), 0, 0)); container.addChild(new Text(fullReportText(result, details), 0, 0)); return container; }, }; } function capReviewSections( sections: { status: string; stat: string; diff: string; untracked: string }, maxBytes: number, ): { status: string; stat: string; diff: string; untracked: string } { let remaining = maxBytes; const take = (text: string, label: string) => { if (!text || remaining <= 0) return ""; const capped = capOutput(text, remaining, label); remaining -= Buffer.byteLength(capped, "utf8"); return capped; }; return { status: take(sections.status, "Git status"), stat: take(sections.stat, "Diff stats"), diff: take(sections.diff, "Diff"), untracked: take(sections.untracked, "Untracked diff"), }; } function addReviewText(container: Container, text: string, theme: any, diff = false, spacer = true) { if (!text) return; container.addChild(new Text(diff ? renderDiff(text) : text, 0, 0)); if (spacer) container.addChild(new Spacer(1)); } function formatCwdForTeamFooter(cwd: string): string { const resolved = path.resolve(cwd); const home = process.env.HOME ? path.resolve(process.env.HOME) : ""; if (!home) return resolved; if (resolved === home) return "~"; if (resolved.startsWith(`${home}${path.sep}`)) return `~/${path.relative(home, resolved).split(path.sep).join("/")}`; return resolved; } function allToolNames(pi: any): string[] { const tools = pi.getAllTools?.() ?? []; return tools .map((tool: any) => (typeof tool === "string" ? tool : typeof tool?.name === "string" ? tool.name : undefined)) .filter((name: any): name is string => typeof name === "string"); } function makeDirectoryFooterFactory(ctx: any, getStatusView: () => StatusView) { return (tui: any, theme: any, footerData: any) => { const component = { render(width: number): string[] { const cwd = formatCwdForTeamFooter(ctx.sessionManager.getCwd()); const branch = footerData.getGitBranch?.(); const sessionName = ctx.sessionManager.getSessionName?.(); const leftParts = [`${cwd}${branch ? ` (${branch})` : ""}`]; if (sessionName) leftParts.push(sessionName); const rightPlain = `● team · ${getStatusView()}`; const right = `${theme.fg("accent", "● team")}${theme.fg("dim", ` · ${getStatusView()}`)}`; // Prioritize the team indicator: truncate/drop the cwd side first when space is tight. if (visibleWidth(rightPlain) >= width) return [truncateToWidth(right, width)]; const leftBudget = width - visibleWidth(rightPlain) - 1; const leftPlain = truncateToWidth(leftParts.join(" · "), leftBudget); const left = theme.fg("dim", leftPlain); const gap = width - visibleWidth(leftPlain) - visibleWidth(rightPlain); const line = gap >= 1 ? `${left}${" ".repeat(gap)}${right}` : right; return [truncateToWidth(line, width)]; }, invalidate() {}, dispose: footerData.onBranchChange?.(() => tui.requestRender?.()), }; return component; }; } class TeamSettingsList extends SettingsList { private readonly teamOnChange: (id: string, newValue: string) => void; constructor( items: any[], maxVisible: number, theme: any, onChange: (id: string, newValue: string) => void, onCancel: () => void, options?: { enableSearch?: boolean }, ) { super(items, maxVisible, theme, onChange, onCancel, options); this.teamOnChange = onChange; this.ensureSelectableSelection(1); } /** Whether a submenu (e.g. the model picker) is currently open over the main list. */ hasSubmenu(): boolean { return Boolean((this as any).submenuComponent); } private isHeaderItem(item: any): boolean { return Boolean(item?.header); } private displayItems(): any[] { const searchEnabled = Boolean((this as any).searchEnabled); const filteredItems = (this as any).filteredItems; const items = (this as any).items; return searchEnabled && Array.isArray(filteredItems) ? filteredItems : Array.isArray(items) ? items : []; } private ensureSelectableSelection(direction: 1 | -1 = 1) { const items = this.displayItems(); if (items.length === 0) return; let index = Math.max(0, Math.min((this as any).selectedIndex ?? 0, items.length - 1)); if (!this.isHeaderItem(items[index])) { (this as any).selectedIndex = index; return; } for (let offset = 1; offset <= items.length; offset += 1) { const candidate = (index + offset * direction + items.length) % items.length; if (!this.isHeaderItem(items[candidate])) { (this as any).selectedIndex = candidate; return; } } (this as any).selectedIndex = 0; } private moveSelection(direction: 1 | -1) { const items = this.displayItems(); if (items.length === 0) return; let index = (this as any).selectedIndex ?? 0; for (let step = 0; step < items.length; step += 1) { index = (index + direction + items.length) % items.length; if (!this.isHeaderItem(items[index])) { (this as any).selectedIndex = index; return; } } } private addTeamHintLine(lines: string[], width: number) { const theme = (this as any).theme; const searchEnabled = Boolean((this as any).searchEnabled); lines.push(""); lines.push( truncateToWidth( theme.hint(searchEnabled ? " Type to search · Enter/Space to change · Esc to cancel" : " Enter/Space to change · Esc to cancel"), width, ), ); } override render(width: number): string[] { if ((this as any).submenuComponent) return super.render(width); const theme = (this as any).theme; const lines: string[] = []; const searchEnabled = Boolean((this as any).searchEnabled); const searchInput = (this as any).searchInput; const allItems = Array.isArray((this as any).items) ? (this as any).items : []; if (searchEnabled && searchInput) { lines.push(...searchInput.render(width)); lines.push(""); } if (allItems.length === 0) { lines.push(theme.hint(" No settings available")); if (searchEnabled) this.addTeamHintLine(lines, width); return lines; } const displayItems = this.displayItems(); if (displayItems.length === 0) { lines.push(truncateToWidth(theme.hint(" No matching settings"), width)); this.addTeamHintLine(lines, width); return lines; } this.ensureSelectableSelection(1); const selectedIndex = (this as any).selectedIndex ?? 0; const maxVisible = (this as any).maxVisible ?? displayItems.length; let startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), displayItems.length - maxVisible)); // Keep a group header attached to its first member instead of scrolling it out of view. if (startIndex > 0 && !this.isHeaderItem(displayItems[startIndex]) && this.isHeaderItem(displayItems[startIndex - 1])) { startIndex -= 1; } let endIndex = Math.min(Math.max(startIndex + maxVisible, selectedIndex + 1), displayItems.length); // Don't end the window on a dangling header with none of its members visible. while (endIndex > startIndex + 1 && endIndex < displayItems.length && this.isHeaderItem(displayItems[endIndex - 1])) { endIndex -= 1; } const selectableLabels = allItems.filter((item: any) => !this.isHeaderItem(item)).map((item: any) => visibleWidth(item.label)); const maxLabelWidth = Math.min(30, Math.max(...(selectableLabels.length > 0 ? selectableLabels : [0]))); let renderedAny = false; for (let i = startIndex; i < endIndex; i += 1) { const item = displayItems[i]; if (!item) continue; if (this.isHeaderItem(item)) { if (renderedAny) lines.push(""); lines.push(truncateToWidth(theme.description(` ${item.label}`), width)); renderedAny = true; continue; } const isSelected = i === selectedIndex; const prefix = isSelected ? theme.cursor : " "; const prefixWidth = visibleWidth(prefix); const labelPadded = item.label + " ".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label))); const labelText = theme.label(labelPadded, isSelected); const separator = " "; const usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator); const valueMaxWidth = width - usedWidth - 2; const valueText = theme.value(truncateToWidth(item.currentValue, valueMaxWidth, ""), isSelected); lines.push(truncateToWidth(prefix + labelText + separator + valueText, width)); renderedAny = true; } if (startIndex > 0 || endIndex < displayItems.length) { const scrollText = ` (${selectedIndex + 1}/${displayItems.length})`; lines.push(theme.hint(truncateToWidth(scrollText, width - 2, ""))); } const selectedItem = displayItems[selectedIndex]; if (selectedItem?.description && !this.isHeaderItem(selectedItem)) { lines.push(""); const wrappedDesc = wrapTextWithAnsi(selectedItem.description, width - 4); for (const line of wrappedDesc) { lines.push(theme.description(` ${line}`)); } } this.addTeamHintLine(lines, width); return lines; } override handleInput(data: string) { if ((this as any).submenuComponent) { super.handleInput(data); return; } if (matchesKey(data, "up")) { this.moveSelection(-1); return; } if (matchesKey(data, "down")) { this.moveSelection(1); return; } this.ensureSelectableSelection(1); const items = this.displayItems(); const selectedIndex = (this as any).selectedIndex ?? 0; const item = items[selectedIndex]; if (this.isHeaderItem(item)) { if (matchesKey(data, "right") || matchesKey(data, "left") || matchesKey(data, "enter") || matchesKey(data, "return") || data === " ") return; } const direction = matchesKey(data, "right") ? 1 : matchesKey(data, "left") ? -1 : 0; if (direction !== 0) { if (item?.values?.length > 0) { const currentIndex = Math.max(0, item.values.indexOf(item.currentValue)); const nextIndex = (currentIndex + direction + item.values.length) % item.values.length; const nextValue = item.values[nextIndex]; item.currentValue = nextValue; this.teamOnChange(item.id, nextValue); } return; } super.handleInput(data); this.ensureSelectableSelection(1); } } function makeReviewDiffRenderers() { return { renderCall(args: any, theme: any, _context: any) { const cwd = args?.cwd || "."; let text = `${theme.fg("toolTitle", theme.bold("review_diff"))} ${theme.fg("accent", cwd)}`; if (Array.isArray(args?.paths) && args.paths.length > 0) text += theme.fg("dim", ` ${args.paths.join(" ")}`); return new Text(text, 0, 0); }, renderResult(result: any, { expanded }: { expanded: boolean }, theme: any, _context: any) { const details = result.details as any; const content = textFromContent(result?.content) || "(no output)"; const sections = details?.sections; if (!sections) return new Text(result.isError ? theme.fg("error", content) : content, 0, 0); const container = new Container(); if (!expanded) { addReviewText(container, sections.status, theme, false, false); addReviewText(container, sections.stat, theme, false, false); container.addChild(new Text(theme.fg("dim", "ctrl+o for full colored diff"), 0, 0)); return container; } addReviewText(container, sections.status, theme); addReviewText(container, sections.stat, theme); addReviewText(container, sections.diff, theme, true); addReviewText(container, sections.untracked, theme, true); if (details.truncated) container.addChild(new Text(theme.fg("warning", "Diff output truncated by maxBytes."), 0, 0)); return container; }, }; } export default function orchestratorWorkflow(pi: ExtensionAPI) { const totals = zeroTotalsRecord(); let config = loadOrchestratorConfig(); let agentNames = discoverAgentNames(); const registeredDelegates = new Set(); let authTypes: Record | undefined; let orchestratorUsageSnapshot: UsageSnapshot | undefined; let statusWidgetTimer: ReturnType | undefined; let pendingStatusWidgetCtx: any; let lastStatusWidgetRenderAt = 0; const getAuthTypes = (): Record => { if (authTypes) return authTypes; authTypes = {}; try { const authPath = path.join(getAgentDir(), "auth.json"); if (!fs.existsSync(authPath)) return authTypes; const parsed = JSON.parse(fs.readFileSync(authPath, "utf8")); if (!parsed || typeof parsed !== "object") return authTypes; for (const [provider, value] of Object.entries(parsed as Record)) { if (value?.type === "oauth" || value?.type === "api_key") authTypes[provider] = value.type; } } catch { authTypes = {}; } return authTypes; }; const getBillingOverride = (role: Role): BillingOverride | undefined => { if (role === "orchestrator") return undefined; try { return loadAgent(role).billing; } catch { return undefined; } }; const getBillingSegment = (role: Role, provider: string | undefined, cost: number): { label: string; dollarCost: number } => { const override = getBillingOverride(role); if (override === "subscription") return { label: "sub", dollarCost: 0 }; if (override === "api") return { label: `$${cost.toFixed(4)} api`, dollarCost: cost }; const authType = provider ? getAuthTypes()[provider] : undefined; if (authType === "oauth") return { label: "sub", dollarCost: 0 }; if (authType === "api_key") return { label: `$${cost.toFixed(4)} api`, dollarCost: cost }; if (cost > 0) return { label: `$${cost.toFixed(4)}`, dollarCost: cost }; return { label: "", dollarCost: 0 }; }; const getStatusView = (): StatusView => config.statusView ?? "default"; const orderedRoles = (): Role[] => { const roles: Role[] = ["orchestrator", ...agentNames]; for (const role of Object.keys(totals)) if (!roles.includes(role)) roles.push(role); return roles; }; const getActiveStatusRoles = (): Role[] => orderedRoles().filter((role) => totals[role] && hasRoleActivity(totals[role])); const statusRowPrefix = (index: number): string => (index === 0 ? "team " : " "); const aggregateStatusTotals = (activeRoles: Role[]) => { let requests = 0; let input = 0; let output = 0; let cacheRead = 0; let cacheWrite = 0; let totalDollarCost = 0; let hasSubscription = false; for (const role of activeRoles) { const total = totals[role]; const provider = total.lastModel?.split("/")[0]; const billing = getBillingSegment(role, provider, total.cost); requests += total.requests; input += total.input; output += total.output; cacheRead += total.cacheRead; cacheWrite += total.cacheWrite; totalDollarCost += billing.dollarCost; if (billing.label === "sub") hasSubscription = true; } return { requests, input, output, cacheRead, cacheWrite, totalDollarCost, hasSubscription }; }; const compactAggregateParts = (activeRoles: Role[], aggregate: ReturnType): string[] => { const submitted = submittedInputCount(aggregate.input, aggregate.cacheRead, aggregate.cacheWrite); const parts = [ `${activeRoles.length} ${activeRoles.length === 1 ? "role" : "roles"}`, `${formatCount(aggregate.requests)} req`, `${compactTokenCount(submitted)}→${compactTokenCount(aggregate.output)}`, ]; const cachedPct = cachedPctLabel(aggregate.cacheRead, aggregate.cacheWrite, submitted); if (cachedPct) parts.push(cachedPct); if (aggregate.totalDollarCost > 0) parts.push(`$${aggregate.totalDollarCost.toFixed(4)} api`); if (aggregate.hasSubscription) parts.push("sub"); return parts; }; const compactCacheDetail = (cacheRead: number, cacheWrite: number, submitted: number): string => { const cachedPct = cachedPctLabel(cacheRead, cacheWrite, submitted).replace(" cached", ""); return `cache ${compactTokenCount(cacheRead)}r/${compactTokenCount(cacheWrite)}w${cachedPct ? ` (${cachedPct})` : ""}`; }; const refreshStatusWidgetNow = (ctx: any) => { if (!isSystemEnabled(config) || config.statusWidget === false) { ctx.ui.setWidget("agent-status", undefined); return; } const activeRoles = getActiveStatusRoles(); if (activeRoles.length === 0) { ctx.ui.setWidget("agent-status", undefined); return; } const view = getStatusView(); const lines: string[] = []; if (view === "minimal") { lines.push(["team", ...compactAggregateParts(activeRoles, aggregateStatusTotals(activeRoles))].join(" · ")); } else if (view === "detailed") { activeRoles.forEach((role, index) => { const total = totals[role]; const provider = total.lastModel?.split("/")[0]; const billing = getBillingSegment(role, provider, total.cost); const submitted = submittedInputCount(total.input, total.cacheRead, total.cacheWrite); const parts = [ role, total.lastModel || "unknown", `${formatCount(total.requests)} req`, `in ${compactTokenCount(submitted)}→out ${compactTokenCount(total.output)}`, compactCacheDetail(total.cacheRead, total.cacheWrite, submitted), ]; if (billing.label) parts.push(billing.label); lines.push(`${statusRowPrefix(index)}${parts.join(" · ")}`); }); } else { const providers = new Set( activeRoles.map((role) => totals[role].lastModel?.split("/")[0]).filter((value): value is string => Boolean(value)), ); const dropProvider = providers.size <= 1; let totalDollarCost = 0; let payingRoles = 0; activeRoles.forEach((role, index) => { const total = totals[role]; const provider = total.lastModel?.split("/")[0]; const billing = getBillingSegment(role, provider, total.cost); totalDollarCost += billing.dollarCost; if (billing.dollarCost > 0) payingRoles += 1; const submitted = submittedInputCount(total.input, total.cacheRead, total.cacheWrite); const parts = [ role, shortModelLabel(total.lastModel, dropProvider), `${formatCount(total.requests)} req`, `${compactTokenCount(submitted)}→${compactTokenCount(total.output)}`, ]; const cachedPct = cachedPctLabel(total.cacheRead, total.cacheWrite, submitted); if (cachedPct) parts.push(cachedPct); if (billing.label) parts.push(billing.label); lines.push(`${statusRowPrefix(index)}${parts.join(" · ")}`); }); if (payingRoles >= 2) lines.push(` total · $${totalDollarCost.toFixed(4)}`); } ctx.ui.setWidget("agent-status", lines, { placement: "belowEditor" }); }; const refreshStatusWidget = (ctx: any, force = false) => { pendingStatusWidgetCtx = ctx; if (force) { if (statusWidgetTimer) clearTimeout(statusWidgetTimer); statusWidgetTimer = undefined; pendingStatusWidgetCtx = undefined; lastStatusWidgetRenderAt = Date.now(); refreshStatusWidgetNow(ctx); return; } const now = Date.now(); const elapsed = now - lastStatusWidgetRenderAt; if (elapsed >= 150 && !statusWidgetTimer) { pendingStatusWidgetCtx = undefined; lastStatusWidgetRenderAt = now; refreshStatusWidgetNow(ctx); return; } if (!statusWidgetTimer) { statusWidgetTimer = setTimeout(() => { const queuedCtx = pendingStatusWidgetCtx; pendingStatusWidgetCtx = undefined; statusWidgetTimer = undefined; lastStatusWidgetRenderAt = Date.now(); if (queuedCtx) refreshStatusWidgetNow(queuedCtx); }, Math.max(0, 150 - elapsed)); } }; const allOurToolNames = (): string[] => { const names = new Set(["review_diff"]); for (const name of agentNames) names.add(delegateToolName(name)); for (const name of registeredDelegates) names.add(delegateToolName(name)); return [...names]; }; const enabledOurToolNames = (): string[] => { const names = ["review_diff"]; for (const name of agentNames) { if (loadAgentSafe(name)?.enabled) names.push(delegateToolName(name)); } return names; }; const applySystemState = (ctx: any) => { const enabled = isSystemEnabled(config); const active = pi.getActiveTools?.() ?? []; const activeNames = Array.isArray(active) ? active.filter((name: any): name is string => typeof name === "string") : []; const ourNames = allOurToolNames(); if (enabled) { const available = new Set(allToolNames(pi)); const ours = enabledOurToolNames().filter((name) => available.has(name)); const kept = activeNames.filter((name) => !ourNames.includes(name) || ours.includes(name)); pi.setActiveTools?.([...new Set([...kept, ...ours])]); ctx.ui.setStatus("workflow", ctx.ui.theme.fg("accent", "● team")); } else { pi.setActiveTools?.(activeNames.filter((name) => !ourNames.includes(name))); ctx.ui.setStatus("workflow", ctx.ui.theme.fg("dim", "○ team off")); } refreshStatusWidget(ctx, true); if (enabled && config.statusWidget !== false && config.replaceFooter !== false) { ctx.ui.setFooter?.(makeDirectoryFooterFactory(ctx, getStatusView)); } else { ctx.ui.setFooter?.(undefined); } }; const recordUsageDelta = (role: Role, delta: UsageDelta, ctx: any, force = false) => { addUsageDeltaToTotals(totals, role, delta); refreshStatusWidget(ctx, force || Boolean(delta.requests)); }; const rebuildTotalsFromSession = (ctx: any) => { resetTotals(totals); for (const entry of ctx.sessionManager.getEntries()) { const message = entry?.message; if (!message) continue; if (message.role === "assistant") { addUsageDeltaToTotals( totals, "orchestrator", usageDeltaFromUsage(message.usage, 1, message.provider, message.model), ); continue; } if (message.role !== "toolResult") continue; const role = agentNameFromToolName(message.toolName); if (!role) continue; const details = message.details; if (!details || typeof details !== "object") continue; const hasDelegationStats = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "cost"].some( (key) => typeof details[key] === "number", ) || (typeof details.provider === "string" && typeof details.model === "string"); if (!hasDelegationStats) continue; addUsageDeltaToTotals(totals, role, { input: safeNumber(details.inputTokens), output: safeNumber(details.outputTokens), cacheRead: safeNumber(details.cacheReadTokens), cacheWrite: safeNumber(details.cacheWriteTokens), cost: safeNumber(details.cost), requests: safeNumber(details.turns) || 1, provider: typeof details.provider === "string" ? details.provider : undefined, model: typeof details.model === "string" ? details.model : undefined, }); } }; const applyConfiguredOrchestrator = async (ctx: any) => { if (!isSystemEnabled(config)) return; const configured = config.orchestrator; if (!configured) return; const model = ctx.modelRegistry.find(configured.provider, configured.model); if (!model) { ctx.ui.notify(`Configured orchestrator model not found: ${configured.provider}/${configured.model}`, "warning"); return; } const ok = await pi.setModel(model); if (!ok) { ctx.ui.notify(`Configured orchestrator model is not available: ${configured.provider}/${configured.model}`, "warning"); return; } pi.setThinkingLevel(configured.thinking); }; const getModelOptions = (ctx: any) => { const preferred = ctx.modelRegistry.getAvailable(); const models = preferred.length > 0 ? preferred : ctx.modelRegistry.getAll(); const byKey = new Map(); for (const model of models) { if (typeof model?.provider !== "string" || typeof model?.id !== "string") continue; byKey.set(`${model.provider}/${model.id}`, model); } return [...byKey.keys()].sort((a, b) => a.localeCompare(b)); }; const selectModel = async (ctx: any): Promise<{ provider: string; model: string } | undefined> => { const options = getModelOptions(ctx); if (options.length === 0) { ctx.ui.notify("No models found in the model registry.", "error"); return undefined; } let choice: string | undefined; if (options.length > 60) { const providers = [...new Set(options.map((option) => option.slice(0, option.indexOf("/"))))].sort((a, b) => a.localeCompare(b), ); const provider = await ctx.ui.select("Choose provider", providers); if (!provider) return undefined; choice = await ctx.ui.select( `Choose model for ${provider}`, options.filter((option) => option.startsWith(`${provider}/`)), ); } else { choice = await ctx.ui.select("Choose model", options); } if (!choice) return undefined; const slash = choice.indexOf("/"); if (slash <= 0) return undefined; return { provider: choice.slice(0, slash), model: choice.slice(slash + 1) }; }; const parseModelValue = (value: string): { provider: string; model: string } | undefined => { const slash = value.indexOf("/"); if (slash <= 0) return undefined; return { provider: value.slice(0, slash), model: value.slice(slash + 1) }; }; const currentOrchestratorModel = (ctx: any): { provider: string; model: string; thinking: ThinkingLevel } => { const configured = config.orchestrator; return { provider: ctx.model?.provider ?? configured?.provider ?? "unset", model: ctx.model?.id ?? configured?.model ?? "unset", thinking: (ctx.model ? pi.getThinkingLevel() : (configured?.thinking ?? pi.getThinkingLevel())) as ThinkingLevel, }; }; const makeModelPicker = (ctx: any, currentValue: string) => { const options = getModelOptions(ctx); const input = new Input(); const selectList = new SelectList( options.map((value) => ({ value, label: value })), Math.min(12, Math.max(1, options.length)), getSelectListTheme(), ); const currentIndex = options.indexOf(currentValue); if (currentIndex >= 0) selectList.setSelectedIndex(currentIndex); return (done: (selectedValue?: string) => void) => { selectList.onSelect = (item) => done(item.value); selectList.onCancel = () => done(undefined); return { render(width: number): string[] { return [ ...input.render(width), ctx.ui.theme.fg("dim", " Type to filter · ↑/↓ select · Enter choose · Esc cancel"), "", ...selectList.render(width), ]; }, invalidate() {}, handleInput(data: string) { if ( matchesKey(data, "up") || matchesKey(data, "down") || matchesKey(data, "enter") || matchesKey(data, "return") || matchesKey(data, "escape") ) { selectList.handleInput(data); return; } input.handleInput(data); selectList.setFilter(input.getValue()); }, }; }; }; const runLegacyTeamFlow = async (ctx: any) => { const role = (await ctx.ui.select("Configure which role?", ["orchestrator", ...agentNames])) as Role | undefined; if (!role) return; const selected = await selectModel(ctx); if (!selected) return; const thinking = (await ctx.ui.select("Thinking level", THINKING_LEVELS)) as ThinkingLevel | undefined; if (!thinking) return; if (role === "orchestrator") { const model = ctx.modelRegistry.find(selected.provider, selected.model); if (!model) { ctx.ui.notify(`Model not found: ${selected.provider}/${selected.model}`, "error"); return; } const ok = await pi.setModel(model); if (!ok) { ctx.ui.notify(`No configured auth for ${selected.provider}/${selected.model}; model not changed.`, "error"); return; } pi.setThinkingLevel(thinking); config = saveOrchestratorConfig({ orchestrator: { provider: selected.provider, model: selected.model, thinking } }); ctx.ui.notify(`orchestrator set to ${selected.provider}/${selected.model}:${thinking}`, "info"); return; } const agentRole = role as AgentName; const current = loadAgent(agentRole); const execution = ((await ctx.ui.select("Execution mode", EXECUTION_MODES)) as ExecutionMode | undefined) ?? current.execution; updateAgentFrontmatter(agentRole, { provider: selected.provider, model: selected.model, thinking, execution }); ctx.ui.notify(`${role} set to ${selected.provider}/${selected.model}:${thinking}·${execution}; applies to the next delegation.`, "info"); }; const openTeamPanel = async (ctx: any) => { const orchestrator = currentOrchestratorModel(ctx); const roleHeader = (label: string) => ({ id: `header:${label.toLowerCase()}`, header: true, label, currentValue: "" }); const roleItems = (role: AgentName, agent: AgentConfig) => [ roleHeader(`${role[0].toUpperCase()}${role.slice(1)}${isBuiltinAgent(role) ? "" : " (custom)"}`), { id: `${role}.enabled`, label: "Enabled", description: `off = deactivate ${delegateToolName(role)} and drop this agent from the orchestrator prompt`, currentValue: agent.enabled ? "on" : "off", values: ["on", "off"], }, { id: `${role}.model`, label: "Model", currentValue: `${agent.provider}/${agent.model}`, submenu: (currentValue: string, done: (selectedValue?: string) => void) => makeModelPicker(ctx, currentValue)(done), }, { id: `${role}.thinking`, label: "Thinking", currentValue: agent.thinking, values: THINKING_LEVELS, }, { id: `${role}.execution`, label: "Execution", currentValue: agent.execution, values: EXECUTION_MODES, }, ]; const agentGroups: any[] = []; for (const name of agentNames) { const agent = loadAgentSafe(name); if (!agent) { ctx.ui.notify(`Skipping ${name}: invalid agent file (needs provider and model in frontmatter).`, "warning"); continue; } agentGroups.push(...roleItems(name, agent)); } const items: any[] = [ { id: "system", label: "System", description: "Master switch: tools, prompt injection, widget, footer. Add agents with /team add .", currentValue: isSystemEnabled(config) ? "on" : "off", values: ["on", "off"], }, roleHeader("Orchestrator"), { id: "orchestrator.model", label: "Model", currentValue: `${orchestrator.provider}/${orchestrator.model}`, submenu: (currentValue: string, done: (selectedValue?: string) => void) => makeModelPicker(ctx, currentValue)(done), }, { id: "orchestrator.thinking", label: "Thinking", currentValue: orchestrator.thinking, values: THINKING_LEVELS, }, { id: "orchestrator.prompt", label: "Prompt", description: `custom = editable copy at ${getUserOrchestratorPromptPath()}; default = optimized bundled prompt`, currentValue: fs.existsSync(getUserOrchestratorPromptPath()) ? "custom" : "default", values: ["default", "custom"], }, ...agentGroups, roleHeader("Display"), { id: "statusWidget", label: "Status widget", currentValue: config.statusWidget === false ? "off" : "on", values: ["on", "off"] }, { id: "statusView", label: "Status detail", currentValue: getStatusView(), values: STATUS_VIEWS }, { id: "footer", label: "Footer", description: "directory = replace pi footer with cwd-only line while status widget is on", currentValue: config.replaceFooter === false ? "built-in" : "directory", values: ["directory", "built-in"], }, ]; await ctx.ui.custom((tui: any, theme: any, _keybindings: any, done: (value?: unknown) => void) => { let list: TeamSettingsList; const revert = (id: string, value: string) => list.updateValue(id, value); const onChange = (id: string, value: string) => { if (id === "system") { config = saveOrchestratorConfig({ enabled: value === "on" }); applySystemState(ctx); if (value === "on") void applyConfiguredOrchestrator(ctx); return; } if (id === "orchestrator.model") { const previous = currentOrchestratorModel(ctx); const selected = parseModelValue(value); if (!selected) { ctx.ui.notify(`Invalid model: ${value}`, "error"); revert(id, `${previous.provider}/${previous.model}`); return; } const model = ctx.modelRegistry.find(selected.provider, selected.model); if (!model) { ctx.ui.notify(`Model not found: ${selected.provider}/${selected.model}`, "error"); revert(id, `${previous.provider}/${previous.model}`); return; } pi.setModel(model).then((ok: boolean) => { if (!ok) { ctx.ui.notify(`No configured auth for ${selected.provider}/${selected.model}; model not changed.`, "error"); revert(id, `${previous.provider}/${previous.model}`); tui.requestRender(); return; } config = saveOrchestratorConfig({ orchestrator: { provider: selected.provider, model: selected.model, thinking: pi.getThinkingLevel() } }); tui.requestRender(); }); return; } if (id === "orchestrator.thinking") { const current = currentOrchestratorModel(ctx); pi.setThinkingLevel(value as ThinkingLevel); config = saveOrchestratorConfig({ orchestrator: { provider: current.provider, model: current.model, thinking: value as ThinkingLevel } }); return; } if (id === "orchestrator.prompt") { if (value === "custom") { const filePath = ensureUserOrchestratorPrompt(); ctx.ui.notify(`Editable orchestrator prompt: ${filePath}`, "info"); } else { const backupPath = resetUserOrchestratorPrompt(); ctx.ui.notify( backupPath ? `Reverted to bundled prompt; your copy was kept at ${backupPath}` : "Already using the bundled prompt.", "info", ); } return; } for (const role of agentNames) { if (id === `${role}.enabled`) { updateAgentFrontmatter(role, { enabled: value === "on" ? "true" : "false" }); applySystemState(ctx); ctx.ui.notify(`${role} ${value === "on" ? "enabled" : "disabled"}.`, "info"); return; } if (id === `${role}.model`) { const previous = loadAgent(role); const selected = parseModelValue(value); if (!selected) { ctx.ui.notify(`Invalid model: ${value}`, "error"); revert(id, `${previous.provider}/${previous.model}`); return; } updateAgentFrontmatter(role, { provider: selected.provider, model: selected.model, thinking: previous.thinking, execution: previous.execution, }); return; } if (id === `${role}.thinking`) { const agent = loadAgent(role); updateAgentFrontmatter(role, { provider: agent.provider, model: agent.model, thinking: value as ThinkingLevel, execution: agent.execution }); return; } if (id === `${role}.execution`) { const agent = loadAgent(role); const execution = isExecutionMode(value) ? value : agent.execution; updateAgentFrontmatter(role, { provider: agent.provider, model: agent.model, thinking: agent.thinking, execution }); ctx.ui.notify(`${role} execution set to ${execution}; applies to the next delegation.`, "info"); return; } } if (id === "statusWidget") { config = saveOrchestratorConfig({ statusWidget: value === "on" }); applySystemState(ctx); return; } if (id === "statusView") { config = saveOrchestratorConfig({ statusView: value as StatusView }); applySystemState(ctx); return; } if (id === "footer") { config = saveOrchestratorConfig({ replaceFooter: value === "directory" }); applySystemState(ctx); } }; list = new TeamSettingsList(items, Math.min(18, items.length + 2), getSettingsListTheme(), onChange, () => done(undefined), { enableSearch: false, }); return { render(width: number): string[] { const hint = list.hasSubmenu() ? "" : ` ${theme.fg("dim", "↑/↓ select · ←/→ cycle value")}`; return [`${theme.fg("accent", theme.bold("Team"))}${hint}`, "", ...list.render(width)]; }, invalidate() { list.invalidate(); }, handleInput(data: string) { list.handleInput(data); tui.requestRender(); }, }; }); }; const formatTeamShow = (ctx: any): string => { const configured = config.orchestrator; const orchestratorProvider = ctx.model?.provider ?? configured?.provider ?? "unset"; const orchestratorModel = ctx.model?.id ?? configured?.model ?? "unset"; const orchestratorThinking = ctx.model ? pi.getThinkingLevel() : (configured?.thinking ?? pi.getThinkingLevel()); const widget = config.statusWidget === false || !isSystemEnabled(config) ? `off (${getStatusView()})` : getStatusView(); const footer = isSystemEnabled(config) && config.statusWidget !== false && config.replaceFooter !== false ? "directory" : "built-in"; const prompt = fs.existsSync(getUserOrchestratorPromptPath()) ? "custom" : "default"; const agentParts = agentNames.map((name) => { const agent = loadAgentSafe(name); if (!agent) return `${name} (invalid)`; const state = agent.enabled ? "" : " (off)"; return `${name} ${agent.provider}/${agent.model}:${agent.thinking}·${agent.execution}${state}`; }); return [ `team ${isSystemEnabled(config) ? "on" : "off"}`, `status ${widget}`, `footer ${footer}`, `orchestrator ${orchestratorProvider}/${orchestratorModel}:${orchestratorThinking} (prompt ${prompt})`, ...agentParts, ].join(" · "); }; const executeDelegation = async ( role: AgentName, params: any, signal: AbortSignal | undefined, onUpdate: ((partial: AgentToolResult) => void) | undefined, ctx: any, ) => { let agent: AgentConfig; try { agent = loadAgent(role); } catch (error) { return { content: [{ type: "text", text: `Cannot delegate to ${role}: ${error instanceof Error ? error.message : String(error)}` }], isError: true, } as AgentToolResult; } if (!agent.enabled) { return { content: [{ type: "text", text: `The ${role} agent is disabled. Do not retry; handle the work another way (the user can re-enable it via /team).` }], isError: true, } as AgentToolResult; } const run = async () => { throwIfAborted(signal); const result = await runDelegatedAgent( role, params.task, params.cwd || ctx.cwd, signal, onUpdate, (delta) => recordUsageDelta(role, delta, ctx), params.sessionId, params.thinking as ThinkingLevel | undefined, ); refreshStatusWidget(ctx, true); return result; }; return agent.execution === "sequential" ? runExclusive(run, signal) : run(); }; const builtinDelegateMeta: Record = { researcher: { label: "Researcher", description: "Delegate read-only codebase research to the researcher subagent. Use for context gathering, relevance mapping, use-case tracing, and architecture questions before implementation. Fire independent research questions as parallel calls.", promptSnippet: delegatePromptSnippet( "researcher", "read-only context and relevance research", " Fire independent questions as parallel calls.", ), }, implementor: { label: "Implementor", description: "Delegate implementation to the implementor subagent. Use this for all file edits, code generation, migrations, and targeted validation when useful.", promptSnippet: delegatePromptSnippet("implementor", "implementation and targeted validation"), promptGuidelines: [ "All implementation work should be delegated to delegate_implementor and reviewed afterward with review_diff.", "For follow-up fixes or the next milestone of the same work, pass the previous delegation's sessionId to delegate_implementor to continue with retained context.", ], }, design: { label: "Design", description: "Delegate a design/UI/UX review-and-fix pass after UI-affecting implementation is complete. It inspects the changed surface, fixes visual/interaction/accessibility issues, and reports deferred UX concerns.", promptSnippet: delegatePromptSnippet("design", "design/UI/UX review-and-fix pass"), promptGuidelines: [ "After UI-affecting implementation passes review_diff, run delegate_design scoped to the changed surface, then review_diff again. Skip it for changes with no UI impact.", ], }, }; const customDelegateMeta = (name: AgentName): { label: string; description: string; promptSnippet: string; promptGuidelines?: string[] } => { const label = `${name[0].toUpperCase()}${name.slice(1)}`; const agent = loadAgentSafe(name); const purpose = agent?.description || `tasks for the custom ${name} agent`; return { label, description: `Delegate a task to the custom ${name} subagent. ${purpose}`, promptSnippet: delegatePromptSnippet(name, purpose), }; }; const registerDelegateTool = (name: AgentName) => { if (registeredDelegates.has(name)) return; registeredDelegates.add(name); const meta = builtinDelegateMeta[name] ?? customDelegateMeta(name); pi.registerTool({ name: delegateToolName(name), label: meta.label, description: meta.description, promptSnippet: meta.promptSnippet, ...(meta.promptGuidelines ? { promptGuidelines: meta.promptGuidelines } : {}), parameters: delegateParams, executionMode: "parallel", async execute(_toolCallId, params, signal, onUpdate, ctx) { return executeDelegation(name, params, signal, onUpdate, ctx); }, ...makeDelegateRenderers(meta.label), }); }; for (const name of agentNames) registerDelegateTool(name); pi.registerTool({ name: "review_diff", label: "Review Diff", description: "Collect git status plus a combined staged+unstaged diff against HEAD and untracked file contents for orchestrator review, with an optional paths filter.", promptSnippet: "review_diff: inspect git status and combined diff before final orchestrator review.", parameters: diffParams, executionMode: "parallel", async execute(_toolCallId, params, signal, _onUpdate, ctx) { return runExclusive(async () => { throwIfAborted(signal); const cwd = params.cwd || ctx.cwd; const paths = Array.isArray(params.paths) ? params.paths.filter((pathspec: unknown): pathspec is string => typeof pathspec === "string" && pathspec.length > 0) : []; const maxBytes = params.maxBytes ?? 80_000; const withPaths = (args: string[]) => (paths.length > 0 ? [...args, "--", ...paths] : args); const section = (title: string, body: string, empty = "(none)") => `# ${title}\n\n${body || empty}`; const inside = await runGit(["rev-parse", "--is-inside-work-tree"], cwd); if (inside.code !== 0 || inside.stdout.trim() !== "true") { return { content: [{ type: "text", text: `Not a git worktree: ${cwd}\n${inside.stderr}`.trim() }], details: { cwd, status: "not_git" }, isError: true, }; } const status = await runGit(withPaths(["status", "--short"]), cwd); const head = await runGit(["rev-parse", "--verify", "HEAD"], cwd); const statusSection = section("Git Status", status.stdout, "(clean)"); const statSections: string[] = []; const diffSections: string[] = []; const untrackedSections: string[] = []; const sections = [statusSection]; if (head.code === 0) { const stat = await runGit(withPaths(["diff", "HEAD", "--stat"]), cwd); const diff = await runGit(withPaths(["diff", "HEAD"]), cwd); const statSection = section("Diff Stat (HEAD)", stat.stdout); const diffSection = section("Diff (HEAD)", diff.stdout); statSections.push(statSection); diffSections.push(diffSection); sections.push(statSection, diffSection); } else { const stat = await runGit(withPaths(["diff", "--stat"]), cwd); const diff = await runGit(withPaths(["diff"]), cwd); const stagedStat = await runGit(withPaths(["diff", "--staged", "--stat"]), cwd); const stagedDiff = await runGit(withPaths(["diff", "--staged"]), cwd); const unstagedStatSection = section("Unstaged Diff Stat", stat.stdout); const unstagedDiffSection = section("Unstaged Diff", diff.stdout); const stagedStatSection = section("Staged Diff Stat", stagedStat.stdout); const stagedDiffSection = section("Staged Diff", stagedDiff.stdout); statSections.push(unstagedStatSection, stagedStatSection); diffSections.push(unstagedDiffSection, stagedDiffSection); sections.push(unstagedStatSection, unstagedDiffSection, stagedStatSection, stagedDiffSection); } const untracked = await runGit(withPaths(["ls-files", "--others", "--exclude-standard"]), cwd); const untrackedFiles = untracked.stdout .split(/\r?\n/) .filter((file) => file.length > 0); const diffableUntrackedFiles = untrackedFiles.slice(0, 20); const untrackedList = untrackedFiles.length > 20 ? `${diffableUntrackedFiles.join("\n")}\n\n(${untrackedFiles.length - 20} more listed after diff sections.)` : untrackedFiles.join("\n"); const untrackedSection = section("Untracked Files", untrackedList); untrackedSections.push(untrackedSection); sections.push(untrackedSection); if (diffableUntrackedFiles.length > 0) { const untrackedDiffSections: string[] = []; for (const file of diffableUntrackedFiles) { const diff = await runGit(["diff", "--no-index", "--", "/dev/null", file], cwd); const diffOutput = diff.stdout || (diff.stderr ? `Stderr:\n${diff.stderr}` : "(no diff output)"); untrackedDiffSections.push(`## ${file}\n\n${capOutput(diffOutput, 20_000, "Untracked file diff")}`); } const untrackedDiffSection = section( untrackedFiles.length > 20 ? "Untracked File Diffs (first 20)" : "Untracked File Diffs", untrackedDiffSections.join("\n\n"), ); untrackedSections.push(untrackedDiffSection); sections.push(untrackedDiffSection); if (untrackedFiles.length > 20) { const remainingUntrackedSection = section("Remaining Untracked Files", untrackedFiles.slice(20).join("\n")); untrackedSections.push(remainingUntrackedSection); sections.push(remainingUntrackedSection); } } const output = sections.join("\n\n"); const cappedOutput = capOutput(output, maxBytes, "Diff output"); const sectionDetails = capReviewSections( { status: statusSection, stat: statSections.join("\n\n"), diff: diffSections.join("\n\n"), untracked: untrackedSections.join("\n\n"), }, maxBytes, ); return { content: [{ type: "text", text: cappedOutput }], details: { cwd, status: status.stdout, truncated: Buffer.byteLength(output, "utf8") > maxBytes, output: cappedOutput, sections: sectionDetails, }, }; }, signal); }, ...makeReviewDiffRenderers(), }); pi.registerCommand("team", { description: "Team settings: agents on/off, custom agents, models, thinking, execution, prompt, status widget", getArgumentCompletions: (prefix) => { const trimmed = (prefix || "").trimStart(); const values = [ "show", "on", "off", "reset", ...STATUS_VIEWS, "prompt", "prompt reset", "add ", ...agentNames.flatMap((name) => [`enable ${name}`, `disable ${name}`]), ...agentNames.filter((name) => !isBuiltinAgent(name)).map((name) => `remove ${name}`), ]; const filtered = values.filter((value) => value.startsWith(trimmed)); return filtered.length > 0 ? filtered.map((value) => ({ value, label: value.trim() })) : null; }, handler: async (args, ctx) => { const action = (args || "").trim(); const [verb, ...rest] = action.split(/\s+/); const target = rest.join(" ").trim(); if (action === "show") { ctx.ui.notify(formatTeamShow(ctx), "info"); return; } if (verb === "prompt") { if (target === "reset") { const backupPath = resetUserOrchestratorPrompt(); ctx.ui.notify( backupPath ? `Reverted to the bundled orchestrator prompt; your copy was kept at ${backupPath}` : "Already using the bundled orchestrator prompt.", "info", ); return; } if (target) { ctx.ui.notify(`Unknown /team prompt argument: ${target} (use /team prompt or /team prompt reset)`, "error"); return; } const filePath = ensureUserOrchestratorPrompt(); ctx.ui.notify(`Editable orchestrator prompt: ${filePath} — edit it there; changes apply on the next turn. Revert with /team prompt reset.`, "info"); return; } if (verb === "add") { if (!AGENT_NAME_RE.test(target)) { ctx.ui.notify(`Invalid agent name: "${target}". Use lowercase letters, digits, - or _ (max 32 chars, starts with a letter).`, "error"); return; } if (agentNames.includes(target) || fs.existsSync(getUserAgentPath(target))) { ctx.ui.notify(`Agent ${target} already exists.`, "error"); return; } const filePath = scaffoldCustomAgent(target); agentNames = discoverAgentNames(); try { registerDelegateTool(target); applySystemState(ctx); ctx.ui.notify(`Created ${target} at ${filePath}. Edit its prompt, model, and tools there; it is live as ${delegateToolName(target)}.`, "info"); } catch { ctx.ui.notify(`Created ${target} at ${filePath}. Restart Pi to register ${delegateToolName(target)}.`, "info"); } return; } if (verb === "remove") { if (!agentNames.includes(target)) { ctx.ui.notify(`Unknown agent: ${target}`, "error"); return; } if (isBuiltinAgent(target)) { ctx.ui.notify(`${target} is a built-in agent and cannot be removed; disable it with /team disable ${target}.`, "error"); return; } const filePath = getUserAgentPath(target); const backupPath = `${filePath}.bak`; fs.rmSync(backupPath, { force: true }); if (fs.existsSync(filePath)) fs.renameSync(filePath, backupPath); agentNames = discoverAgentNames(); applySystemState(ctx); ctx.ui.notify(`Removed ${target} (kept a backup at ${backupPath}). The ${delegateToolName(target)} tool is deactivated; restart Pi to drop it entirely.`, "info"); return; } if (verb === "enable" || verb === "disable") { if (!agentNames.includes(target)) { ctx.ui.notify(`Unknown agent: ${target}`, "error"); return; } try { updateAgentFrontmatter(target, { enabled: verb === "enable" ? "true" : "false" }); } catch (error) { ctx.ui.notify(`Cannot update ${target}: ${error instanceof Error ? error.message : String(error)}`, "error"); return; } applySystemState(ctx); ctx.ui.notify(`${target} ${verb}d.`, "info"); return; } if (action === "on") { config = saveOrchestratorConfig({ enabled: true }); applySystemState(ctx); await applyConfiguredOrchestrator(ctx); ctx.ui.notify("Team orchestration enabled.", "info"); return; } if (action === "off") { config = saveOrchestratorConfig({ enabled: false }); applySystemState(ctx); ctx.ui.notify("Team orchestration disabled.", "info"); return; } if (isStatusView(action)) { config = saveOrchestratorConfig({ statusView: action, statusWidget: true }); applySystemState(ctx); ctx.ui.notify(`Team status view set to ${action}.`, "info"); return; } if (action === "reset") { resetTotals(totals); refreshStatusWidget(ctx, true); ctx.ui.notify("Team status totals reset.", "info"); return; } if (action) { ctx.ui.notify(`Unknown /team argument: ${action}`, "error"); return; } if (ctx.mode !== "tui") { await runLegacyTeamFlow(ctx); return; } await openTeamPanel(ctx); }, }); pi.on("before_agent_start", async (event) => { if (!isSystemEnabled(config)) return; const prompt = buildOrchestratorPrompt(agentNames); if (!prompt) return; return { systemPrompt: `${event.systemPrompt}\n\n${prompt}` }; }); pi.on("message_update", async (event, ctx) => { const message = event.message; if (message?.role !== "assistant" || !message.usage) return; const nextSnapshot = usageSnapshotFromUsage(message.usage); if (usageSnapshotsEqual(orchestratorUsageSnapshot, nextSnapshot)) return; const delta = usageDeltaFromSnapshots(orchestratorUsageSnapshot, nextSnapshot, 0, message.provider, message.model); orchestratorUsageSnapshot = nextSnapshot; if (hasUsageDeltaValues(delta)) recordUsageDelta("orchestrator", delta, ctx); }); pi.on("message_end", async (event, ctx) => { const message = event.message; if (message?.role !== "assistant") return; const finalSnapshot = message.usage ? usageSnapshotFromUsage(message.usage) : undefined; const delta = usageDeltaFromSnapshots(orchestratorUsageSnapshot, finalSnapshot, 1, message.provider, message.model); orchestratorUsageSnapshot = undefined; recordUsageDelta("orchestrator", delta, ctx, true); }); pi.on("session_start", async (_event, ctx) => { config = loadOrchestratorConfig(); agentNames = discoverAgentNames(); for (const name of agentNames) { try { registerDelegateTool(name); } catch { // Late registration may be unsupported; the agent activates on next restart. } } orchestratorUsageSnapshot = undefined; rebuildTotalsFromSession(ctx); applySystemState(ctx); await applyConfiguredOrchestrator(ctx); }); pi.on("session_shutdown", async () => { if (statusWidgetTimer) clearTimeout(statusWidgetTimer); statusWidgetTimer = undefined; pendingStatusWidgetCtx = undefined; orchestratorUsageSnapshot = undefined; }); }