// ABOUTME: Multi-agent team dispatcher with specialist agents and grid dashboard.
// ABOUTME: Primary agent delegates via dispatch_agent tool; teams defined in .pi/agents/teams.yaml.
/**
 * Agent Team — Dispatcher-only orchestrator with grid dashboard
 *
 * The primary Pi agent has NO codebase tools. It can ONLY delegate work
 * to specialist agents via the `dispatch_agent` tool. Each specialist
 * maintains its own Pi session for cross-invocation memory.
 *
 * Loads agent definitions from agents/*.md, .claude/agents/*.md, .pi/agents/*.md.
 * Teams are defined in .pi/agents/teams.yaml — on boot a select dialog lets
 * you pick which team to work with. Only team members are available for dispatch.
 *
 * Commands:
 *   /agents-team          — switch active team
 *   /agents-list          — list loaded agents
 *   /agents-grid N        — set column count (default 2)
 *   /agents-clear         — clear agent team widget from screen
 *   Alt+G                 — toggle compact/expanded widget view
 *
 * Usage: pi -e extensions/agent-team.ts -e extensions/footer.ts
 */

import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Text, type AutocompleteItem, visibleWidth, truncateToWidth, Container, Spacer, Box, Markdown, matchesKey, Key, type Component } from "@mariozechner/pi-tui";
import { DynamicBorder, getMarkdownTheme as getPiMdTheme } from "@mariozechner/pi-coding-agent";
import { spawn } from "child_process";
import { readdirSync, readFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import { applyExtensionDefaults } from "./lib/themeMap.ts";

import { statusButton } from "./lib/pipeline-render.ts";
import { DEFAULT_SUBAGENT_MODEL } from "./lib/defaults.ts";
import { loadAgentModelsConfig, loadToolkitModelsConfig, resolveAgentModelString, scanToolkitAgentDefs, type AgentModelsConfig } from "./lib/agent-defs.ts";
import { resolveToolkitWorkerModel, isToolkitCliAgent, spawnToolkitWorker } from "./lib/toolkit-cli.ts";
import { padRight, wordWrap, sideBySide } from "./lib/ui-helpers.ts";
import { contextBudgetLevel, isContextLossError } from "./lib/context-budget.ts";
import { buildCommanderPrompt } from "./lib/commander-prompt.ts";
import { preClaimTask, postCompleteTask, postFailTask } from "./lib/commander-lifecycle.ts";
import { renderTaskList, navDown, navUp, navExit, navEnter, type TaskListInfo, type TaskListState } from "./lib/task-list-render.ts";
import { renderSubagentWidget } from "./lib/subagent-render.ts";

// ── Types ────────────────────────────────────────

interface AgentDef {
	name: string;
	description: string;
	tools: string;
	model: string; // full provider/model ID, empty = inherit parent
	systemPrompt: string;
	file: string;
}

interface AgentState {
	def: AgentDef;
	status: "idle" | "running" | "done" | "error";
	task: string;
	toolCount: number;
	elapsed: number;
	lastWork: string;
	contextPct: number;
	sessionFile: string | null;
	runCount: number;
	resolvedModel: string;
	timer?: ReturnType<typeof setInterval>;
	_warnSent?: boolean;
	_criticalWarned?: boolean;
	widgetId: number;           // unique ID for subagent-style widget
	textChunks: string[];       // streaming text for widget summary
	summary?: string;           // short summary shown in widget
	summaryLines?: string[];    // up to 2 recent CLI/output lines for richer widget preview
	proc?: any;                 // ChildProcess ref for escape-cancel
}

// ── Display Name Helper ──────────────────────────

function displayName(name: string): string {
	return name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
}

function abbreviateAgentName(name: string): string {
	const parts = name.split("-");
	if (parts.length > 1) {
		// Multi-word: take first letter of each word and uppercase
		return parts.map(w => w.charAt(0).toUpperCase()).join("");
	} else {
		// Single-word: uppercase the entire name
		return name.toUpperCase();
	}
}

// ── Teams YAML Parser ────────────────────────────

function parseTeamsYaml(raw: string): Record<string, string[]> {
	const teams: Record<string, string[]> = {};
	let current: string | null = null;
	for (const line of raw.split("\n")) {
		const teamMatch = line.match(/^(\S[^:]*):$/);
		if (teamMatch) {
			current = teamMatch[1].trim();
			teams[current] = [];
			continue;
		}
		const itemMatch = line.match(/^\s+-\s+(.+)$/);
		if (itemMatch && current) {
			teams[current].push(itemMatch[1].trim());
		}
	}
	return teams;
}

// ── Frontmatter Parser ───────────────────────────

function parseAgentFile(filePath: string, modelsConfig?: AgentModelsConfig): AgentDef | null {
	try {
		const raw = readFileSync(filePath, "utf-8");
		const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
		if (!match) return null;

		const frontmatter: Record<string, string> = {};
		for (const line of match[1].split("\n")) {
			const idx = line.indexOf(":");
			if (idx > 0) {
				frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
			}
		}

		if (!frontmatter.name) return null;

		// Model resolution: models.json > frontmatter fallback > empty
		let model = "";
		if (modelsConfig) {
			const key = frontmatter.name.toLowerCase();
			const entry = modelsConfig.agents[key];
			if (entry) {
				model = resolveAgentModelString(frontmatter.name, modelsConfig);
			}
		}
		if (!model && frontmatter.model) {
			model = frontmatter.model;
		}

		return {
			name: frontmatter.name,
			description: frontmatter.description || "",
			tools: frontmatter.tools || "read,grep,find,ls",
			model,
			systemPrompt: match[2].trim(),
			file: filePath,
		};
	} catch {
		return null;
	}
}

function scanAgentDirs(cwd: string, extProjectDir?: string, modelsConfig?: AgentModelsConfig): AgentDef[] {
	const dirs = [
		join(cwd, "agents"),
		join(cwd, ".claude", "agents"),
		join(cwd, ".pi", "agents"),
		...(extProjectDir ? [join(extProjectDir, ".pi", "agents"), join(extProjectDir, "agents")] : []),
	];

	const agents: AgentDef[] = [];
	const seen = new Set<string>();

	for (const dir of dirs) {
		if (!existsSync(dir)) continue;
		try {
			const scan = (d: string) => {
				for (const file of readdirSync(d, { withFileTypes: true })) {
					const fullPath = resolve(d, file.name);
					if (file.isDirectory()) {
						scan(fullPath);
					} else if (file.name.endsWith(".md")) {
						const def = parseAgentFile(fullPath, modelsConfig);
						if (def && !seen.has(def.name.toLowerCase())) {
							seen.add(def.name.toLowerCase());
							agents.push(def);
						}
					}
				}
			};
			scan(dir);
		} catch {}
	}

	return agents;
}

// ── Extension ────────────────────────────────────

export default function (pi: ExtensionAPI) {
	const agentStates: Map<string, AgentState> = new Map();
	let allAgentDefs: AgentDef[] = [];
	let teams: Record<string, string[]> = {};
	let activeTeamName = "";
	let gridCols = 2;
	let widgetCtx: any;
	let sessionDir = "";
	let contextWindow = 0;
	let widgetCompact = true;
	let selectedAgentIndex = -1; // -1 = no selection
	let taskListState: TaskListState = { selectedIndex: -1, scrollOffset: 0 };
	let nextWidgetId = 1;
	const agentWidgetBoxes = new Map<number, { invalidate: () => void }>();

	// ── Dark background colors for agent status (matches subagent-widget) ────
	const STATUS_BG: Record<string, string> = {
		running: "\x1b[48;2;26;58;92m",   // dark steel blue
		done:    "\x1b[48;2;35;50;55m",    // dark teal-gray
		error:   "\x1b[48;2;70;35;35m",    // dark muted red
	};
	const RESET_BG = "\x1b[49m";
	const WHITE_BOLD = "\x1b[1;97m";  // bold bright white text
	const RESET_ALL = "\x1b[0m";

	function loadAgents(cwd: string) {
		const extDir = dirname(fileURLToPath(import.meta.url));
		const extProjectDir = resolve(extDir, "..");

		// Create session storage dir
		sessionDir = join(cwd, ".pi", "agent-sessions");
		if (!existsSync(sessionDir)) {
			mkdirSync(sessionDir, { recursive: true });
		}

		// Load standard + toolkit model config, then scan agent .md files
		const modelsConfig = loadAgentModelsConfig(cwd, extProjectDir);
		const toolkitModelsConfig = loadToolkitModelsConfig(cwd, extProjectDir);
		const standardAgentDefs = scanAgentDirs(cwd, extProjectDir, modelsConfig);
		const toolkitAgentDefs = Array.from(scanToolkitAgentDefs(cwd, extProjectDir, toolkitModelsConfig).values());
		const merged = new Map<string, AgentDef>();
		for (const def of [...standardAgentDefs, ...toolkitAgentDefs]) {
			if (!merged.has(def.name.toLowerCase())) merged.set(def.name.toLowerCase(), def);
		}
		allAgentDefs = Array.from(merged.values());

		// Load teams from .pi/agents/teams.yaml (fallback to extension project dir)
		let teamsPath = join(cwd, ".pi", "agents", "teams.yaml");
		if (!existsSync(teamsPath)) {
			teamsPath = join(extProjectDir, ".pi", "agents", "teams.yaml");
		}
		if (!existsSync(teamsPath)) {
			teamsPath = join(extProjectDir, "agents", "teams.yaml");
		}
		if (existsSync(teamsPath)) {
			try {
				teams = parseTeamsYaml(readFileSync(teamsPath, "utf-8"));
			} catch {
				teams = {};
			}
		} else {
			teams = {};
		}

		// If no teams defined, create a default "all" team
		if (Object.keys(teams).length === 0) {
			teams = { all: allAgentDefs.map(d => d.name) };
		}
	}

	function activateTeam(teamName: string) {
		activeTeamName = teamName;
		const members = teams[teamName] || [];
		const defsByName = new Map(allAgentDefs.map(d => [d.name.toLowerCase(), d]));

		agentStates.clear();
		selectedAgentIndex = -1; // Reset selection when team changes
		for (const member of members) {
			const def = defsByName.get(member.toLowerCase());
			if (!def) continue;
			const key = def.name.toLowerCase().replace(/\s+/g, "-");
			const sessionFile = join(sessionDir, `${key}.json`);
			agentStates.set(def.name.toLowerCase(), {
				def,
				status: "idle",
				task: "",
				toolCount: 0,
				elapsed: 0,
				lastWork: "",
				contextPct: 0,
				sessionFile: existsSync(sessionFile) ? sessionFile : null,
				runCount: 0,
				resolvedModel: "",
				widgetId: nextWidgetId++,
				textChunks: [],
				summary: undefined,
				summaryLines: undefined,
			});
		}

		// Auto-size grid columns based on team size
		const size = agentStates.size;
		gridCols = size <= 3 ? size : size === 4 ? 2 : 3;
	}

	// ── Per-Agent Widget Rendering (subagent-style) ──────────────────

	function registerAgentWidget(state: AgentState) {
		if (!widgetCtx) return;
		const key = `agent-${state.widgetId}`;
		widgetCtx.ui.setWidget(key, (_tui: any, theme: any) => {
			const bgFn = (text: string): string => {
				const bg = STATUS_BG[state.status] || STATUS_BG.running;
				return `${bg}${WHITE_BOLD}${text}${RESET_ALL}${RESET_BG}`;
			};

			const box = new Box(1, 1, bgFn);
			const content = new Text("", 0, 0);
			box.addChild(content);
			agentWidgetBoxes.set(state.widgetId, { invalidate: () => box.invalidate() });

			return {
				render(width: number): string[] {
					box.setBgFn((text: string): string => {
						const bg = STATUS_BG[state.status] || STATUS_BG.running;
						return `${bg}${WHITE_BOLD}${text}${RESET_ALL}${RESET_BG}`;
					});

					const renderState = {
						id: state.widgetId,
						status: state.status as "running" | "done" | "error",
						name: state.def.name.toUpperCase(),
						task: state.task,
						toolCount: state.toolCount,
						elapsed: state.elapsed,
						turnCount: state.runCount,
						summary: state.summary,
						summaryLines: state.summaryLines,
						model: state.resolvedModel || state.def.model || undefined,
					};
					const result = renderSubagentWidget(renderState, width, theme);
					content.setText(result.lines.join("\n"));
					return box.render(width);
				},
				invalidate() {
					box.invalidate();
				},
			};
		});
	}

	function invalidateAgentWidget(state: AgentState) {
		agentWidgetBoxes.get(state.widgetId)?.invalidate();
	}

	function removeAgentWidget(state: AgentState) {
		if (!widgetCtx) return;
		widgetCtx.ui.setWidget(`agent-${state.widgetId}`, undefined);
		agentWidgetBoxes.delete(state.widgetId);
	}

	function removeAllAgentWidgets() {
		if (!widgetCtx) return;
		for (const state of agentStates.values()) {
			widgetCtx.ui.setWidget(`agent-${state.widgetId}`, undefined);
		}
		agentWidgetBoxes.clear();
	}

	// ── Combined Widget (task list only) ──────────────────────────────

	function updateWidget() {
		if (!widgetCtx) return;

		// Task list widget (above editor)
		const taskList = (globalThis as any).__piTaskList as TaskListInfo | null;
		if (taskList && taskList.tasks.length > 0) {
			widgetCtx.ui.setWidget("agent-team", (_tui: any, theme: any) => {
				const text = new Text("", 0, 0);

				return {
					render(width: number): string[] {
						const tl = (globalThis as any).__piTaskList as TaskListInfo | null;
						if (!tl || tl.tasks.length === 0) {
							text.setText("");
							return [];
						}

						const termHeight = process.stdout.rows || 24;
						const availableHeight = Math.max(3, Math.min(termHeight - 10, 14));
						const taskLines = renderTaskList(
							tl, taskListState, width, availableHeight,
							{ truncateToWidth, fg: (c: string, t: string) => theme.fg(c, t) },
						);
						const taskBg = "\x1b[48;5;236m";
						const taskReset = "\x1b[0m";
						const emptyPad = taskBg + padRight("", width) + taskReset;
						const allLines: string[] = [];
						allLines.push(emptyPad);
						allLines.push(...taskLines.map(l => taskBg + padRight(l, width) + taskReset));
						allLines.push(emptyPad);

						text.setText(allLines.join("\n"));
						return allLines;
					},
					invalidate() {
						text.invalidate();
					},
				};
			}, { placement: "aboveEditor" });
		} else {
			// No task list — remove the combined widget
			widgetCtx.ui.setWidget("agent-team", undefined);
		}

		// Individual agent widgets are managed separately via registerAgentWidget/invalidateAgentWidget

		// Re-pin mode bar as the last aboveEditor widget so it stays directly above the editor input.
		// Without this, the agent-team widget (tasks) would render between the mode bar and the editor.
		(globalThis as any).__piRefreshModeBlock?.();
	}

	// ── Dispatch Agent (returns Promise) ─────────

	function dispatchAgent(
		agentName: string,
		task: string,
		ctx: any,
	): Promise<{ output: string; exitCode: number; elapsed: number; model: string }> {
		const key = agentName.toLowerCase();
		const state = agentStates.get(key);
		if (!state) {
			return Promise.resolve({
				output: `Agent "${agentName}" not found. Available: ${Array.from(agentStates.values()).map(s => displayName(s.def.name)).join(", ")}`,
				exitCode: 1,
				elapsed: 0,
				model: "",
			});
		}

		if (state.status === "running") {
			return Promise.resolve({
				output: `Agent "${displayName(state.def.name)}" is already running. Wait for it to finish.`,
				exitCode: 1,
				elapsed: 0,
				model: "",
			});
		}

		state.status = "running";
		state.task = task;
		state.toolCount = 0;
		state.elapsed = 0;
		state.lastWork = "";
		state.textChunks = [];
		state.summary = undefined;
		state.summaryLines = undefined;
		state.runCount++;
		registerAgentWidget(state);
		updateWidget();

		const startTime = Date.now();
		state.timer = setInterval(() => {
			state.elapsed = Date.now() - startTime;
			invalidateAgentWidget(state);
		}, 1000);

		// Use agent's defined model or fall back to default subagent model.
		// NOTE: We intentionally do NOT inherit the parent model. Each agent
		// should use its explicitly defined model or the lightweight default.
		const model = resolveToolkitWorkerModel(state.def.name, state.def.model || DEFAULT_SUBAGENT_MODEL);
		state.resolvedModel = model;

		// Session file for this agent
		const agentKey = state.def.name.toLowerCase().replace(/\s+/g, "-");
		const agentSessionFile = join(sessionDir, `${agentKey}.json`);

		// Build args — first run creates session, subsequent runs resume
		const extDir = dirname(fileURLToPath(import.meta.url));
		const tasksExtPath = join(extDir, "tasks.ts");
		const commanderExtPath = join(extDir, "commander-mcp.ts");
		const footerExtPath = join(extDir, "footer.ts");
		const memoryCycleExtPath = join(extDir, "memory-cycle.ts");

		// Resolve tools — append commander tools when Commander is available
		const g = globalThis as any;
		const commanderAvailable = g.__piCommanderGate?.state === "available" && !!g.__piCommanderClient;

		// Commander lifecycle: gate-aware fire-and-forget helper
		function commanderSync(fn: (client: any) => Promise<void>): void {
			const gate = g.__piCommanderGate;
			if (!gate || gate.state !== "available" || !g.__piCommanderClient) return;
			fn(g.__piCommanderClient).catch(() => {});
		}

		// Hoist for use in pre-dispatch claim + post-dispatch reconciliation
		const canonicalName = state.def.name;
		const taskId = commanderAvailable ? g.__piCurrentTask?.commanderTaskId as number | undefined : undefined;

		let tools = state.def.tools;
		// Commander tools are extension-registered (not built-in), so they must NOT
		// go in --tools (which only accepts built-in names and warns on unknowns).
		// Loading the commander-mcp extension via -e is sufficient — pi auto-activates
		// all extension tools via includeAllExtensionTools.

		// Build system prompt — append Commander discipline when available
		let systemPrompt = state.def.systemPrompt;
		if (commanderAvailable) {
			// Gather peer names for inter-agent mailbox communication
			const peerNames: string[] = [];
			for (const [name] of agentStates) {
				if (name !== canonicalName.toLowerCase()) {
					peerNames.push(name);
				}
			}
			systemPrompt += buildCommanderPrompt({
				agentName: canonicalName,
				taskId,
				enableMailboxChat: true,
				peerNames,
			});
		}

		const args = [
			"--mode", "json",
			"-p",
			"--no-extensions",
			"-e", tasksExtPath,
			"-e", footerExtPath,
			"-e", memoryCycleExtPath,
			...(commanderAvailable ? ["-e", commanderExtPath] : []),
			"--model", model,
			"--tools", tools,
			"--thinking", "off",
			"--append-system-prompt", systemPrompt,
			"--session", agentSessionFile,
		];

		// Continue existing session if we have one
		if (state.sessionFile) {
			args.push("-c");
		}

		args.push(task);

		const textChunks: string[] = [];

		return new Promise((resolve) => {
			// Build env — include Commander task ID when available
			const spawnEnv: Record<string, string | undefined> = { ...process.env, PI_SUBAGENT: "1" };
			if (commanderAvailable) {
				const currentTask = g.__piCurrentTask as { commanderTaskId?: number } | null;
				if (currentTask?.commanderTaskId !== undefined) {
					spawnEnv.PI_COMMANDER_TASK_ID = String(currentTask.commanderTaskId);
				}
			}

			// Pre-dispatch: claim task in Commander before spawning
			if (commanderAvailable && taskId !== undefined) {
				commanderSync((client) => preClaimTask(client, taskId, canonicalName));
			}

			const finish = (code: number | null, stderrBuf: string) => {
				state.proc = null;
				clearInterval(state.timer);
				state.elapsed = Date.now() - startTime;
				state.status = code === 0 ? "done" : "error";

				if (code === 0) {
					state.sessionFile = agentSessionFile;
				}

				let full = textChunks.join("");
				if ((code !== 0 && code !== null) && stderrBuf.trim()) {
					if (isContextLossError(stderrBuf)) {
						full = "Context overflow: agent session broke tool_use/tool_result pairing. Clear session and re-dispatch.";
						state.sessionFile = null;
					} else {
						full = full.trim() ? `${full}\n\n--- stderr ---\n${stderrBuf.trim()}` : stderrBuf.trim();
					}
				}
				state.lastWork = full.split("\n").filter((l: string) => l.trim()).pop() || "";
				state.summary = state.lastWork;
				state.summaryLines = full.split("\n").map((l: string) => l.trim()).filter(Boolean).slice(-3);
				invalidateAgentWidget(state);

				setTimeout(() => {
					if (state.status !== "running") removeAgentWidget(state);
				}, 30_000);

				if (commanderAvailable && taskId !== undefined) {
					const summary = textChunks.join("").trim().split("\n").pop() || canonicalName;
					if (state.status === "done") {
						commanderSync((client) => postCompleteTask(client, taskId, canonicalName, summary));
					} else {
						const errMsg = stderrBuf.trim() || summary || "Agent exited with error";
						commanderSync((client) => postFailTask(client, taskId, errMsg));
					}
				}

				ctx.ui.notify(
					`${displayName(state.def.name)} ${state.status} in ${Math.round(state.elapsed / 1000)}s`,
					state.status === "done" ? "success" : "error"
				);

				resolve({ output: full, exitCode: code ?? 1, elapsed: state.elapsed, model });
			};

			const handleStdoutLine = (line: string) => {
				try {
					const event = JSON.parse(line);
					if (event.type === "message_update") {
						const delta = event.assistantMessageEvent;
						if (delta?.type === "text_delta") {
							textChunks.push(delta.delta || "");
							state.textChunks.push(delta.delta || "");
							const full = textChunks.join("");
							const last = full.split("\n").filter((l: string) => l.trim()).pop() || "";
							state.lastWork = last;
							state.summary = last;
							state.summaryLines = full.split("\n").map((l: string) => l.trim()).filter(Boolean).slice(-3);
							invalidateAgentWidget(state);
						}
					} else if (event.type === "tool_execution_start") {
						state.toolCount++;
						invalidateAgentWidget(state);
					} else if (event.type === "message_end") {
						const msg = event.message;
						if (msg?.usage && contextWindow > 0) {
							state.contextPct = ((msg.usage.input || 0) / contextWindow) * 100;
							const level = contextBudgetLevel(state.contextPct);
							if (level === "warn" && !state._warnSent) {
								state._warnSent = true;
								ctx.ui.notify(`${displayName(state.def.name)} Context: ${Math.round(state.contextPct)}%`, "info");
							} else if (level === "critical" && !state._criticalWarned) {
								state._criticalWarned = true;
								ctx.ui.notify(`${displayName(state.def.name)} Context: ${Math.round(state.contextPct)}% — Agent will Cycle-Memory soon`, "info");
							}
							invalidateAgentWidget(state);
						}
					} else if (event.type === "agent_end") {
						const msgs = event.messages || [];
						const last = [...msgs].reverse().find((m: any) => m.role === "assistant");
						if (last?.usage && contextWindow > 0) {
							state.contextPct = ((last.usage.input || 0) / contextWindow) * 100;
							invalidateAgentWidget(state);
						}
					}
				} catch {}
			};

			let stderrBuf = "";
			if (isToolkitCliAgent(state.def.name)) {
				spawnToolkitWorker(state.def, {
					task,
					sessionFile: agentSessionFile,
					cwd: ctx.cwd,
					env: spawnEnv,
					onStdoutLine: handleStdoutLine,
					onStderr: (chunk: string) => { stderrBuf += chunk; },
				}).then(({ exitCode }) => finish(exitCode, stderrBuf));
				return;
			}

			const proc = spawn("pi", args, {
				stdio: ["ignore", "pipe", "pipe"],
				env: spawnEnv,
				cwd: ctx.cwd,
			});
			state.proc = proc;

			let buffer = "";
			proc.stdout!.setEncoding("utf-8");
			proc.stdout!.on("data", (chunk: string) => {
				buffer += chunk;
				const lines = buffer.split("\n");
				buffer = lines.pop() || "";
				for (const line of lines) {
					if (!line.trim()) continue;
					handleStdoutLine(line);
				}
			});

			proc.stderr!.setEncoding("utf-8");
			proc.stderr!.on("data", (chunk: string) => { stderrBuf += chunk; });
			proc.on("close", (code) => {
				if (buffer.trim()) handleStdoutLine(buffer);
				finish(code, stderrBuf);
			});
			proc.on("error", (err) => finish(1, `Error spawning agent: ${err.message}`));
			proc.on("exit", () => { clearInterval(state.timer); });
		});
	}

	// ── dispatch_agent Tool (registered at top level) ──

	pi.registerTool({
		name: "dispatch_agent",
		label: "Dispatch Agent",
		description: "Dispatch a task to a specialist agent. The agent will execute the task and return the result. Use the system prompt to see available agent names.",
		parameters: Type.Object({
			agent: Type.String({ description: "Agent name (case-insensitive)" }),
			task: Type.String({ description: "Task description for the agent to execute" }),
		}),

		async execute(_toolCallId, params, _signal, onUpdate, ctx) {
			const { agent, task } = params as { agent: string; task: string };
			const defModel = agentStates.get(agent.toLowerCase())?.def.model || "";

			try {
				if (onUpdate) {
					onUpdate({
						content: [{ type: "text", text: `Dispatching to ${agent}...` }],
						details: { agent, task, status: "dispatching", model: defModel },
					});
				}

				const result = await dispatchAgent(agent, task, ctx);

				const truncated = result.output.length > 8000
					? result.output.slice(0, 8000) + "\n\n... [truncated]"
					: result.output;

				const status = result.exitCode === 0 ? "done" : "error";
				const summary = `[${agent}] ${status} in ${Math.round(result.elapsed / 1000)}s`;

				return {
					content: [{ type: "text", text: `${summary}\n\n${truncated}` }],
					details: {
						agent,
						task,
						status,
						elapsed: result.elapsed,
						exitCode: result.exitCode,
						fullOutput: result.output,
						model: result.model,
					},
				};
			} catch (err: any) {
				return {
					content: [{ type: "text", text: `Error dispatching to ${agent}: ${err?.message || err}` }],
					details: { agent, task, status: "error", elapsed: 0, exitCode: 1, fullOutput: "", model: defModel },
				};
			}
		},

		renderResult(result, options, theme) {
			const details = result.details as any;
			if (!details) {
				const text = result.content[0];
				return new Text(text?.type === "text" ? text.text : "", 0, 0);
			}

			const agent = (details.agent || "AGENT").toUpperCase();
			const model = details.model || "";
			const elapsed = typeof details.elapsed === "number" ? details.elapsed : 0;
			const rawStatus = details.status || "done";
			const status: "running" | "done" | "error" = rawStatus === "dispatching"
				? "running"
				: rawStatus === "error"
					? "error"
					: "done";

			const renderState = {
				id: 0,
				status,
				name: agent,
				task: details.task || "",
				toolCount: 0,
				elapsed,
				turnCount: 1,
				summary: `dispatching: ${agent.toLowerCase()}${model ? ` @ ${model}` : ""}`,
				model: model || undefined,
			};

			const rendered = renderSubagentWidget(renderState, options.width || 80, theme);
			const bg = STATUS_BG[status] || STATUS_BG.running;
			const bgFn = (text: string): string => `${bg}${WHITE_BOLD}${text}${RESET_ALL}${RESET_BG}`;
			const box = new Box(1, 1, bgFn);
			box.addChild(new Text(rendered.lines.join("\n"), 0, 0));

			if (options.expanded && details.fullOutput) {
				const output = details.fullOutput.length > 4000
					? details.fullOutput.slice(0, 4000) + "\n... [truncated]"
					: details.fullOutput;
				const mdTheme = getPiMdTheme();
				const container = new Container();
				container.addChild(box);
				container.addChild(new Markdown(output, 2, 0, mdTheme));
				return container;
			}

			return box;
		},
	});

	// ── Commands ─────────────────────────────────

	pi.registerCommand("agents-team", {
		description: "Select a team to work with",
		handler: async (_args, ctx) => {
			widgetCtx = ctx;
			const teamNames = Object.keys(teams);
			if (teamNames.length === 0) {
				ctx.ui.notify("No teams defined in .pi/agents/teams.yaml", "warning");
				return;
			}

			const options = teamNames.map(name => {
				const members = teams[name].map(m => displayName(m));
				return `${name} — ${members.join(", ")}`;
			});

			const choice = await ctx.ui.select("Select Team", options);
			if (choice === undefined) return;

			const idx = options.indexOf(choice);
			const name = teamNames[idx];
			activateTeam(name);
			updateWidget();
			ctx.ui.setStatus("agent-team", `Team: ${name} (${agentStates.size})`);
			ctx.ui.notify(`Team: ${name} — ${Array.from(agentStates.values()).map(s => displayName(s.def.name)).join(", ")}`, "info");
		},
	});

	pi.registerCommand("agents-list", {
		description: "List all loaded agents",
		handler: async (_args, _ctx) => {
			widgetCtx = _ctx;
			const names = Array.from(agentStates.values())
				.map(s => {
					const session = s.sessionFile ? "resumed" : "new";
					return `${displayName(s.def.name)} (${s.status}, ${session}, runs: ${s.runCount}): ${s.def.description}`;
				})
				.join("\n");
			_ctx.ui.notify(names || "No agents loaded", "info");
		},
	});

	pi.registerCommand("agents-grid", {
		description: "Set grid columns: /agents-grid <1-6>",
		getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
			const items = ["1", "2", "3", "4", "5", "6"].map(n => ({
				value: n,
				label: `${n} columns`,
			}));
			const filtered = items.filter(i => i.value.startsWith(prefix));
			return filtered.length > 0 ? filtered : items;
		},
		handler: async (args, _ctx) => {
			widgetCtx = _ctx;
			const n = parseInt(args?.trim() || "", 10);
			if (n >= 1 && n <= 6) {
				gridCols = n;
				_ctx.ui.notify(`Grid set to ${gridCols} columns`, "info");
				updateWidget();
			} else {
				_ctx.ui.notify("Usage: /agents-grid <1-6>", "error");
			}
		},
	});

	pi.registerCommand("agents-clear", {
		description: "Clear agent team widget from screen",
		handler: async (_args, ctx) => {
			widgetCtx = ctx;
			ctx.ui.setWidget("agent-team", undefined);

			// Remove all individual agent widgets
			removeAllAgentWidgets();

			// Reset all agent states to idle so the widget can reappear on next dispatch
			for (const state of agentStates.values()) {
				if (state.status === "done" || state.status === "error") {
					state.status = "idle";
					state.task = "";
					state.toolCount = 0;
					state.elapsed = 0;
					state.lastWork = "";
					state.contextPct = 0;
					state.resolvedModel = "";
					state.textChunks = [];
					state.summary = undefined;
				}
			}
			selectedAgentIndex = -1;

			ctx.ui.notify("Agent team widget cleared.", "info");
		},
	});

	// ── Agent Detail Overlay ──────────────────────

	class AgentDetailOverlay {
		private scrollOffset = 0;
		private totalContentLines = 0;

		constructor(
			private agent: AgentState,
			private onDone: () => void,
		) {}

		handleInput(data: string, tui: any): void {
			// Calculate max scroll based on current content
			const height = process.stdout.rows || 24;
			const contentHeight = height - 1; // Reserve 1 line for footer
			const maxScroll = Math.max(0, this.totalContentLines - contentHeight);

			if (matchesKey(data, Key.up)) {
				this.scrollOffset = Math.max(0, this.scrollOffset - 1);
			} else if (matchesKey(data, Key.down)) {
				this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
			} else if (matchesKey(data, Key.pageUp)) {
				this.scrollOffset = Math.max(0, this.scrollOffset - Math.max(1, contentHeight - 1));
			} else if (matchesKey(data, Key.pageDown)) {
				this.scrollOffset = Math.min(maxScroll, this.scrollOffset + Math.max(1, contentHeight - 1));
			} else if (matchesKey(data, Key.home)) {
				this.scrollOffset = 0;
			} else if (matchesKey(data, Key.end)) {
				this.scrollOffset = maxScroll;
			} else if (matchesKey(data, Key.escape)) {
				this.onDone();
				return;
			}
			tui.requestRender();
		}

		render(width: number, height: number, theme: any): string[] {
			const container = new Container();
			const mdTheme = getPiMdTheme();

			// Full width with minimal padding
			const panelW = width - 4; // 2 chars padding each side
			const innerWidth = panelW - 2; // Account for border

			// Header with agent name pill and status
			container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
			const name = displayName(this.agent.def.name);
			const statusBtn = statusButton(this.agent.status, name, theme, false);
			const timeStr = this.agent.status !== "idle" ? ` ${Math.round(this.agent.elapsed / 1000)}s` : "";
			container.addChild(new Text(
				`${statusBtn}${timeStr}`,
				1, 0,
			));
			container.addChild(new Spacer(1));

			// Section header helper - fills width with line characters
			const sectionHeader = (title: string) => {
				const label = ` ─── ${title} `;
				const remaining = Math.max(0, innerWidth - visibleWidth(label));
				return theme.fg("accent", theme.bold(label + "─".repeat(remaining)));
			};

			// Metadata section (full width, vertical list)
			container.addChild(new Text(sectionHeader("METADATA"), 1, 0));
			const formatRow = (label: string, value: string, valueColor: string = "muted") => {
				const labelStr = theme.fg("accent", theme.bold(padRight(label + ":", 14)));
				const valueStr = theme.fg(valueColor, value);
				return labelStr + " " + valueStr;
			};

			// Helper to add wrapped metadata rows
			const addWrappedRow = (label: string, value: string, valueColor: string = "muted") => {
				const labelWidth = 14;
				const valueWidth = innerWidth - labelWidth - 1;
				const wrapped = wordWrap(value, valueWidth);
				for (let i = 0; i < wrapped.length; i++) {
					const displayLabel = i === 0 ? label : "";
					container.addChild(new Text(formatRow(displayLabel, wrapped[i], valueColor), 1, 0));
				}
			};

			// STATUS - color based on state
			const statusColorMap: Record<string, string> = { running: "accent", done: "success", error: "error", idle: "dim" };
			const statusColor = statusColorMap[this.agent.status] || "muted";
			container.addChild(new Text(formatRow("STATUS", this.agent.status.toUpperCase(), statusColor), 1, 0));

			// DESCRIPTION - if present
			if (this.agent.def.description) {
				addWrappedRow("DESCRIPTION", this.agent.def.description, "muted");
			}

			// MODEL - accent color
			addWrappedRow("MODEL", this.agent.resolvedModel || this.agent.def.model || "(unknown)", "accent");

			// TOOLS - success color
			addWrappedRow("TOOLS", this.agent.def.tools, "success");

			// CONTEXT - conditional color based on percentage
			const pct = Math.ceil(this.agent.contextPct);
			const ctxColor = pct > 80 ? "error" : pct > 50 ? "warning" : "success";
			container.addChild(new Text(formatRow("CONTEXT", `${pct}%`, ctxColor), 1, 0));

			// RUNS - accent color
			container.addChild(new Text(formatRow("RUNS", this.agent.runCount.toString(), "accent"), 1, 0));

			// TOOLS USED - accent color
			container.addChild(new Text(formatRow("TOOLS USED", this.agent.toolCount.toString(), "accent"), 1, 0));

			// FILE - dim color (path)
			addWrappedRow("FILE", this.agent.def.file, "dim");

			// SESSION - dim color (path)
			if (this.agent.sessionFile) {
				addWrappedRow("SESSION", this.agent.sessionFile, "dim");
			}
			container.addChild(new Spacer(1));

			// System prompt section (full width)
			container.addChild(new Text(sectionHeader("SYSTEM PROMPT"), 1, 0));
			container.addChild(new Spacer(1));
			// Render system prompt as markdown - it will handle its own wrapping
			const sysPromptMd = new Markdown(this.agent.def.systemPrompt, 1, 0, mdTheme);
			container.addChild(sysPromptMd);
			container.addChild(new Spacer(1));

			// Task section (if present) - render as markdown
			if (this.agent.task) {
				container.addChild(new Text(sectionHeader("CURRENT TASK"), 1, 0));
				container.addChild(new Spacer(1));
				const taskMd = new Markdown(this.agent.task, 1, 0, mdTheme);
				container.addChild(taskMd);
				container.addChild(new Spacer(1));
			}

			// Last work section (if present) - render as markdown
			if (this.agent.lastWork) {
				container.addChild(new Text(sectionHeader("LAST WORK"), 1, 0));
				container.addChild(new Spacer(1));
				const workMd = new Markdown(this.agent.lastWork, 1, 0, mdTheme);
				container.addChild(workMd);
				container.addChild(new Spacer(1));
			}

			// Render all content (without footer)
			const allLines = container.render(panelW);
			this.totalContentLines = allLines.length; // Store for handleInput
			const contentHeight = height - 1; // Reserve 1 line for footer
			const maxScroll = Math.max(0, allLines.length - contentHeight);
			
			// Clamp scroll offset to valid range
			this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll));

			// Apply scrolling - show content lines, footer always at bottom
			const visibleContentLines = allLines.slice(this.scrollOffset, this.scrollOffset + contentHeight);

			// Footer (always visible at bottom, separate from scrollable content)
			const scrollInfo = maxScroll > 0 
				? ` ↑/↓/PgUp/PgDn/Home/End Scroll (${this.scrollOffset + 1}-${Math.min(this.scrollOffset + contentHeight, allLines.length)}/${allLines.length}) • Esc Close`
				: " Esc Close";
			const footer = theme.fg("dim", scrollInfo);
			const footerLine = padRight(footer, panelW);

			// Dark backdrop: full screen from top to bottom
			const dimBg = "\x1b[48;2;10;10;15m";
			const reset = "\x1b[0m";

			const result: string[] = [];
			// Render visible content lines from top
			// Pad each line to panelW before wrapping with background to ensure full coverage
			for (const line of visibleContentLines) {
				result.push(dimBg + "  " + padRight(line, panelW) + "  " + reset);
			}

			// Add footer at bottom (already padded to panelW)
			result.push(dimBg + "  " + footerLine + "  " + reset);

			// Fill remaining height with dark background
			while (result.length < height) {
				result.push(dimBg + " ".repeat(width) + reset);
			}

			return result;
		}
	}

	async function showAgentDetail(ctx: any, agent: AgentState) {
		await ctx.ui.custom((tui, theme, _kb, done) => {
			const overlay = new AgentDetailOverlay(agent, () => done(undefined));
			return {
				render: (w) => overlay.render(w, process.stdout.rows || 24, theme),
				handleInput: (data) => overlay.handleInput(data, tui),
				invalidate: () => {},
			};
		}, {
			overlay: true,
			overlayOptions: { width: "100%" },
		});
	}

	pi.registerShortcut("alt+g", {
		description: "Toggle agent team compact/expanded view",
		handler: async (ctx) => {
			widgetCtx = ctx;
			widgetCompact = !widgetCompact;
			updateWidget();
		},
	});

	const selectNext = async (ctx: any) => {
		if (!ctx.hasUI) return;
		widgetCtx = ctx;
		// Filter out only idle agents - include completed ones
		const active = Array.from(agentStates.values()).filter(
			(a) => a.status !== "idle",
		);
		const count = active.length;
		if (count === 0) {
			selectedAgentIndex = -1;
			return;
		}
		// Auto-expand to expanded view if in compact mode so selection is visible
		if (widgetCompact) {
			widgetCompact = false;
		}
		if (selectedAgentIndex < 0) selectedAgentIndex = 0;
		selectedAgentIndex = (selectedAgentIndex + 1) % count;
		updateWidget();
	};

	const selectPrev = async (ctx: any) => {
		if (!ctx.hasUI) return;
		widgetCtx = ctx;
		// Filter out only idle agents - include completed ones
		const active = Array.from(agentStates.values()).filter(
			(a) => a.status !== "idle",
		);
		const count = active.length;
		if (count === 0) {
			selectedAgentIndex = -1;
			return;
		}
		// Auto-expand to expanded view if in compact mode so selection is visible
		if (widgetCompact) {
			widgetCompact = false;
		}
		if (selectedAgentIndex < 0) selectedAgentIndex = count - 1;
		selectedAgentIndex = (selectedAgentIndex - 1 + count) % count;
		updateWidget();
	};

	const exitSelection = async (ctx: any) => {
		if (!ctx.hasUI) return;
		widgetCtx = ctx;
		selectedAgentIndex = -1;
		updateWidget();
	};

	// ── System Prompt Override ───────────────────

	pi.on("before_agent_start", async (_event, _ctx) => {
		// Mode gate: only fire when mode is TEAM (or unset for backward compat)
		const mode = (globalThis as any).__piCurrentMode;
		if (mode && mode !== "TEAM") return {};

		// Build dynamic agent catalog from active team only
		const agentCatalog = Array.from(agentStates.values())
			.map(s => `### ${displayName(s.def.name)}\n**Dispatch as:** \`${s.def.name}\`\n${s.def.description}\n**Tools:** ${s.def.tools}` + (s.def.model ? `\n**Model:** ${s.def.model}` : ""))
			.join("\n\n");

		const teamMembers = Array.from(agentStates.values()).map(s => displayName(s.def.name)).join(", ");

		const commanderAvailable = (globalThis as any).__piCommanderGate?.state === "available";
		const commanderSection = commanderAvailable ? `

## Commander Integration (REQUIRED)
Commander is connected. ALWAYS use these tools for dashboard visibility:
- \`commander_session { operation: "file:open", file_path: <path> }\` — display key files in Commander's floating viewer
- \`commander_task\` — track tasks in the Commander dashboard
- \`commander_mailbox\` — send status updates to the dashboard

### Mailbox Protocol
- Check your inbox periodically: \`commander_mailbox { operation: "inbox", agent_name: "coordinator" }\`
- Send status at start, milestones, and completion
- Warm, professional, collaborative tone — no emojis anywhere
- Use file:open to show dispatched agent results or task lists` : "";

		// Check if scout is on the team for delegation instructions
		const hasScout = agentStates.has("scout");
		const scoutSection = hasScout ? `

## Scout Agent (ALWAYS use for context gathering)
A scout agent is on your team. **ALWAYS dispatch to the scout** for any context-gathering work instead of doing it yourself.

### What to dispatch to the scout:
- Reading files, exploring directory structures
- Searching for patterns, symbols, or text in the codebase (grep, find)
- Understanding architecture, tracing code paths, mapping dependencies
- Any investigation or information-gathering task

### How to use the scout:
\`\`\`
dispatch_agent { agent: "scout", task: "Read the file at src/index.ts and summarize its exports" }
\`\`\`
The scout runs in the background. When it finishes, its findings are returned. Then you synthesize and respond to the user.

### What YOU still do directly:
- Respond to the user (synthesize scout findings, answer questions)
- Write/edit files, run commands, make code changes
- Plan, create tasks, manage workflow
- Any action that modifies the codebase

### Important:
- Do NOT use Read, grep, find, or ls yourself — dispatch those to the scout
- You CAN still use Bash for running tests, builds, or commands that modify things
- If the scout errors, fall back to doing the work directly` : "";

		return {
			systemPrompt: `You coordinate specialist agents and delegate context-gathering to them.
You dispatch specialist agents for investigation and can work directly for responses and edits.

## Active Team: ${activeTeamName}
Members: ${teamMembers}
You can ONLY dispatch to agents listed below. Do not attempt to dispatch to agents outside this team.
${scoutSection}

## When to Work Directly
- Responding to the user with information gathered by agents
- Writing or editing files, running builds/tests
- Small edits, answering questions you already know the answer to
- Task management, planning, workflow decisions

## When to Dispatch Agents
- ${hasScout ? "ANY context-gathering: reading files, searching code, exploring structure — ALWAYS dispatch scout" : "Simple lookups: reading a file, checking status, listing contents"}
- Significant work: new features, refactors, multi-file changes
- Tasks that benefit from specialist knowledge
- When you want structured, multi-agent collaboration

## Guidelines
- ${hasScout ? "ALWAYS dispatch scout for reads/searches — do NOT read files yourself" : "Use your judgment — if it's quick, just do it; if it's real work, dispatch"}
- You can mix direct work and agent dispatches in the same conversation
- You can chain agents: use scout to explore, then builder to implement
- You can dispatch the same agent multiple times with different tasks
- Keep tasks focused — one clear objective per dispatch

## Asking the User
- You have the ask_user tool to ask the user questions directly
- Use it when you need clarification, decisions, or preferences before dispatching agents
- Three modes: "select" (pick from options with markdown preview), "input" (free text), "confirm" (yes/no)
- If a sub-agent needs user input, it should describe what it needs — then YOU ask the user and relay the answer

## Task Management
- You have direct access to the \`tasks\` tool — use it yourself, do NOT dispatch agents for task management
- Use \`tasks new-list\` to start a themed list, \`tasks add\` to add items, \`tasks toggle\` to cycle status
- Define your plan as tasks BEFORE dispatching agents

## Agents

${agentCatalog}${commanderSection}`,
		};
	});

	// ── Reset helpers ─────────────────────────────────────────────────

	function resetAgentState(state: AgentState) {
		state.status = "idle";
		state.task = "";
		state.toolCount = 0;
		state.elapsed = 0;
		state.lastWork = "";
		state.contextPct = 0;
		state.resolvedModel = "";
		state.textChunks = [];
		state.summary = undefined;
	}

	// ── Reset agent boxes on new message ───────────────────────────────

	pi.on("input", () => {
		// When user sends a new message, reset completed/error agents to idle
		// and remove their individual widgets so boxes display cleanly for the new task
		for (const state of agentStates.values()) {
			if (state.status === "done" || state.status === "error") {
				removeAgentWidget(state);
				resetAgentState(state);
			}
		}
		updateWidget();
	});

	// ── Reset agent boxes on /new ─────────────────────────────────────

	pi.on("session_switch", async (_event, _ctx) => {
		// /new fires session_switch — clear all agent boxes from previous session
		if (widgetCtx) {
			widgetCtx.ui.setWidget("agent-team", undefined);
		}
		removeAllAgentWidgets();
		widgetCtx = _ctx;
		for (const state of agentStates.values()) {
			resetAgentState(state);
		}
		updateWidget();
	});

	// ── Session Start ────────────────────────────

	pi.on("session_start", async (_event, _ctx) => {
		applyExtensionDefaults(import.meta.url, _ctx);
		// Clear widgets from previous session
		if (widgetCtx) {
			widgetCtx.ui.setWidget("agent-team", undefined);
		}
		removeAllAgentWidgets();
		widgetCtx = _ctx;
		contextWindow = _ctx.model?.contextWindow || 0;

		// Wipe old agent session files so subagents start fresh
		const sessDir = join(_ctx.cwd, ".pi", "agent-sessions");
		if (existsSync(sessDir)) {
			for (const f of readdirSync(sessDir)) {
				if (f.endsWith(".json")) {
					try { unlinkSync(join(sessDir, f)); } catch {}
				}
			}
		}

		loadAgents(_ctx.cwd);

		// Default to first team — use /agents-team to switch
		const teamNames = Object.keys(teams);
		if (teamNames.length > 0) {
			activateTeam(teamNames[0]);
		}

		// All tools remain visible — dispatcher can use any registered tool directly

		_ctx.ui.setStatus("agent-team", `Team: ${activeTeamName} (${agentStates.size})`);
		updateWidget();

		// ── Expose global hooks for escape-cancel integration ────────────
		// Team agents also have running subprocesses that should be killed
		// on double-ESC. We reuse __piKillAllSubagents-style hook naming but
		// scoped to team procs. The escape-cancel extension checks these.
		(globalThis as any).__piKillTeamProcs = (): number => {
			let killed = 0;
			for (const [, state] of agentStates) {
				if (state.proc && state.status === "running") {
					try { state.proc.kill("SIGTERM"); } catch {}
					killed++;
				}
			}
			return killed;
		};
		(globalThis as any).__piHasRunningTeam = (): boolean => {
			for (const [, state] of agentStates) {
				if (state.status === "running") return true;
			}
			return false;
		};

		// Use footer.ts for footer — do not overwrite; widget uses placement: belowEditor

		// Register nav providers for F-key navigation
		const providers = ((globalThis as any).__piNavProviders = (globalThis as any).__piNavProviders || []);

		// Task list nav provider (first priority when tasks exist)
		providers.push({
			isActive: () => {
				const tl = (globalThis as any).__piTaskList as TaskListInfo | null;
				return !!(tl && tl.tasks.length > 0);
			},
			selectPrev: (ctx: any) => {
				if (!ctx.hasUI) return;
				widgetCtx = ctx;
				const tl = (globalThis as any).__piTaskList as TaskListInfo | null;
				if (!tl || tl.tasks.length === 0) return;
				if (taskListState.selectedIndex < 0) {
					taskListState = navEnter(taskListState, tl.tasks.length);
				} else {
					taskListState = navUp(taskListState);
				}
				updateWidget();
			},
			selectNext: (ctx: any) => {
				if (!ctx.hasUI) return;
				widgetCtx = ctx;
				const tl = (globalThis as any).__piTaskList as TaskListInfo | null;
				if (!tl || tl.tasks.length === 0) return;
				if (taskListState.selectedIndex < 0) {
					taskListState = navEnter(taskListState, tl.tasks.length);
				} else {
					taskListState = navDown(taskListState, tl.tasks.length);
				}
				updateWidget();
			},
			showDetail: async (_ctx: any) => {
				// Could open /tasks overlay in the future
			},
			exitSelection: (ctx: any) => {
				if (!ctx.hasUI) return;
				widgetCtx = ctx;
				taskListState = navExit(taskListState);
				updateWidget();
			},
		});

		// Agent pills nav provider
		providers.push({
			isActive: () => {
				const active = Array.from(agentStates.values()).filter(a => a.status !== "idle");
				return active.length > 0;
			},
			selectPrev: selectPrev,
			selectNext: selectNext,
			showDetail: async (ctx: any) => {
				if (!ctx.hasUI) return;
				const active = Array.from(agentStates.values()).filter(
					(a) => a.status !== "idle",
				);
				const count = active.length;
				if (count === 0 || selectedAgentIndex < 0 || selectedAgentIndex >= count) return;
				const agent = active[selectedAgentIndex];
				if (!agent) return;
				await showAgentDetail(ctx, agent);
			},
			exitSelection: exitSelection,
		});
	});
}
