/**
 * Subagent Tool - Delegate tasks to specialized agents
 *
 * Spawns a separate `pi` process for each subagent invocation,
 * giving it an isolated context window.
 *
 * Supports three modes:
 *   - Single: { agent: "name", task: "..." }
 *   - Parallel: { tasks: [{ agent: "name", task: "..." }, ...] }
 *   - Chain: { chain: [{ agent: "name", task: "... {previous} ..." }, ...] }
 *
 * Uses JSON mode to capture structured output from subagents.
 */

import { spawn, type ChildProcess } from "node:child_process";
import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import type { AgentToolResult } from "@singularity-forge/pi-agent-core";
import type { Message } from "@singularity-forge/pi-ai";
import { StringEnum } from "@singularity-forge/pi-ai";
import { type ExtensionAPI, getMarkdownTheme } from "@singularity-forge/pi-coding-agent";
import { Container, Markdown, Spacer, Text } from "@singularity-forge/pi-tui";
import { Static, Type } from "@sinclair/typebox";
import { formatTokenCount } from "../shared/mod.js";
import { getCurrentPhase } from "../shared/sf-phase-state.js";
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
import {
	type IsolationEnvironment,
	type IsolationMode,
	type MergeResult,
	createIsolation,
	mergeDeltaPatches,
	readIsolationMode,
} from "./isolation.js";
import { registerWorker, updateWorker } from "./worker-registry.js";
import { loadEffectiveSFPreferences } from "../sf/preferences.js";
import { CmuxClient, shellEscape } from "../cmux/index.js";
import { SubagentBackgroundJobManager } from "./background-jobs.js";

const MAX_PARALLEL_TASKS = 8;
const MAX_CONCURRENCY = 4;
const COLLAPSED_ITEM_COUNT = 10;
const liveSubagentProcesses = new Set<ChildProcess>();

async function stopLiveSubagents(): Promise<void> {
	const active = Array.from(liveSubagentProcesses);
	if (active.length === 0) return;

	for (const proc of active) {
		try {
			proc.kill("SIGTERM");
		} catch {
			/* ignore */
		}
	}

	await Promise.all(
		active.map(
			(proc) =>
				new Promise<void>((resolve) => {
					const done = () => resolve();
					const timer = setTimeout(done, 500);
					proc.once("exit", () => {
						clearTimeout(timer);
						resolve();
					});
				}),
		),
	);

	for (const proc of active) {
		if (proc.exitCode === null) {
			try {
				proc.kill("SIGKILL");
			} catch {
				/* ignore */
			}
		}
	}
}

function formatUsageStats(
	usage: {
		input: number;
		output: number;
		cacheRead: number;
		cacheWrite: number;
		cost: number;
		contextTokens?: number;
		turns?: number;
	},
	model?: string,
): string {
	const parts: string[] = [];
	if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
	if (usage.input) parts.push(`↑${formatTokenCount(usage.input)}`);
	if (usage.output) parts.push(`↓${formatTokenCount(usage.output)}`);
	if (usage.cacheRead) parts.push(`R${formatTokenCount(usage.cacheRead)}`);
	if (usage.cacheWrite) parts.push(`W${formatTokenCount(usage.cacheWrite)}`);
	if (usage.cost) parts.push(`$${(Number(usage.cost) || 0).toFixed(4)}`);
	if (usage.contextTokens && usage.contextTokens > 0) {
		parts.push(`ctx:${formatTokenCount(usage.contextTokens)}`);
	}
	if (model) parts.push(model);
	return parts.join(" ");
}

function formatToolCall(
	toolName: string,
	args: Record<string, unknown>,
	themeFg: (color: any, text: string) => string,
): string {
	const shortenPath = (p: string) => {
		const home = os.homedir();
		return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
	};

	switch (toolName) {
		case "bash": {
			const command = (args.command as string) || "...";
			const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
			return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
		}
		case "read": {
			const rawPath = (args.file_path || args.path || "...") as string;
			const filePath = shortenPath(rawPath);
			const offset = args.offset as number | undefined;
			const limit = args.limit as number | undefined;
			let text = themeFg("accent", filePath);
			if (offset !== undefined || limit !== undefined) {
				const startLine = offset ?? 1;
				const endLine = limit !== undefined ? startLine + limit - 1 : "";
				text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
			}
			return themeFg("muted", "read ") + text;
		}
		case "write": {
			const rawPath = (args.file_path || args.path || "...") as string;
			const filePath = shortenPath(rawPath);
			const content = (args.content || "") as string;
			const lines = content.split("\n").length;
			let text = themeFg("muted", "write ") + themeFg("accent", filePath);
			if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
			return text;
		}
		case "edit": {
			const rawPath = (args.file_path || args.path || "...") as string;
			return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
		}
		case "ls": {
			const rawPath = (args.path || ".") as string;
			return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
		}
		case "find": {
			const pattern = (args.pattern || "*") as string;
			const rawPath = (args.path || ".") as string;
			return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`);
		}
		case "grep": {
			const pattern = (args.pattern || "") as string;
			const rawPath = (args.path || ".") as string;
			return (
				themeFg("muted", "grep ") +
				themeFg("accent", `/${pattern}/`) +
				themeFg("dim", ` in ${shortenPath(rawPath)}`)
			);
		}
		default: {
			const argsStr = JSON.stringify(args);
			const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
			return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
		}
	}
}

interface UsageStats {
	input: number;
	output: number;
	cacheRead: number;
	cacheWrite: number;
	cost: number;
	contextTokens: number;
	turns: number;
}

interface SingleResult {
	agent: string;
	agentSource: "user" | "project" | "unknown";
	task: string;
	exitCode: number;
	messages: Message[];
	stderr: string;
	usage: UsageStats;
	model?: string;
	stopReason?: string;
	errorMessage?: string;
	step?: number;
}

interface SubagentDetails {
	mode: "single" | "parallel" | "chain";
	agentScope: AgentScope;
	projectAgentsDir: string | null;
	results: SingleResult[];
}

interface SubagentToolResult extends AgentToolResult<SubagentDetails> {
	isError?: boolean;
}

interface SubagentExecutionContext {
	defaultCwd: string;
	agents: AgentConfig[];
	agentScope: AgentScope;
	projectAgentsDir: string | null;
	params: Static<typeof SubagentParams>;
	signal: AbortSignal | undefined;
	onUpdate: OnUpdateCallback | undefined;
	cmuxClient: CmuxClient;
	cmuxSplitsEnabled: boolean;
	useIsolation: boolean;
}

function getPrimaryTextContent(result: AgentToolResult<SubagentDetails>): string {
	const first = result.content.find((item) => item.type === "text");
	return first?.type === "text" ? first.text : "(no output)";
}

function summarizeBackgroundInvocation(params: Static<typeof SubagentParams>): string {
	if (params.chain && params.chain.length > 0) return `chain:${params.chain.map((step) => step.agent).join("→")}`;
	if (params.tasks && params.tasks.length > 0) return `parallel:${params.tasks.map((task) => task.agent).join(",")}`;
	if (params.agent) return `single:${params.agent}`;
	return "subagent";
}

async function executeSubagentInvocation({
	defaultCwd,
	agents,
	agentScope,
	projectAgentsDir,
	params,
	signal,
	onUpdate,
	cmuxClient,
	cmuxSplitsEnabled,
	useIsolation,
}: SubagentExecutionContext): Promise<SubagentToolResult> {
	const makeDetails =
		(mode: "single" | "parallel" | "chain") =>
		(results: SingleResult[]): SubagentDetails => ({
			mode,
			agentScope,
			projectAgentsDir,
			results,
		});

	if (params.chain && params.chain.length > 0) {
		const results: SingleResult[] = [];
		let previousOutput = "";

		for (let i = 0; i < params.chain.length; i++) {
			const step = params.chain[i];
			const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
			const chainUpdate: OnUpdateCallback | undefined = onUpdate
				? (partial) => {
						const currentResult = partial.details?.results[0];
						if (!currentResult) return;
						onUpdate({
							content: partial.content,
							details: makeDetails("chain")([...results, currentResult]),
						});
					}
				: undefined;

			const result = await runSingleAgent(
				defaultCwd,
				agents,
				step.agent,
				taskWithContext,
				step.cwd,
				i + 1,
				signal,
				chainUpdate,
				makeDetails("chain"),
				step.model ?? params.model,
			);
			results.push(result);

			const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
			if (isError) {
				const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
				return {
					content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
					details: makeDetails("chain")(results),
					isError: true,
				};
			}
			previousOutput = getFinalOutput(result.messages);
		}

		return {
			content: [{ type: "text", text: getFinalOutput(results[results.length - 1].messages) || "(no output)" }],
			details: makeDetails("chain")(results),
		};
	}

	if (params.tasks && params.tasks.length > 0) {
		if (params.tasks.length > MAX_PARALLEL_TASKS) {
			return {
				content: [{ type: "text", text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` }],
				details: makeDetails("parallel")([]),
				isError: true,
			};
		}

		const allResults: SingleResult[] = new Array(params.tasks.length);
		for (let i = 0; i < params.tasks.length; i++) {
			allResults[i] = {
				agent: params.tasks[i].agent,
				agentSource: "unknown",
				task: params.tasks[i].task,
				exitCode: -1,
				messages: [],
				stderr: "",
				usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
			};
		}

		const emitParallelUpdate = () => {
			if (!onUpdate) return;
			const running = allResults.filter((r) => r.exitCode === -1).length;
			const done = allResults.filter((r) => r.exitCode !== -1).length;
			onUpdate({
				content: [{ type: "text", text: `Parallel: ${done}/${allResults.length} done, ${running} running...` }],
				details: makeDetails("parallel")([...allResults]),
			});
		};

		const MAX_RETRIES = 1;
		const batchId = crypto.randomUUID();
		const batchSize = params.tasks.length;
		const gridSurfaces = cmuxSplitsEnabled
			? await cmuxClient.createGridLayout(Math.min(batchSize, MAX_CONCURRENCY))
			: [];
		const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
			const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
			const taskModelOverride = t.model ?? params.model;
			const runTask = () =>
				cmuxSplitsEnabled
					? runSingleAgentInCmuxSplit(
						cmuxClient,
						gridSurfaces[index] ?? (index % 2 === 0 ? "right" : "down"),
						defaultCwd,
						agents,
						t.agent,
						t.task,
						t.cwd,
						undefined,
						signal,
						(partial) => {
							if (partial.details?.results[0]) {
								allResults[index] = partial.details.results[0];
								emitParallelUpdate();
							}
						},
						makeDetails("parallel"),
						taskModelOverride,
					)
					: runSingleAgent(
						defaultCwd,
						agents,
						t.agent,
						t.task,
						t.cwd,
						undefined,
						signal,
						(partial) => {
							if (partial.details?.results[0]) {
								allResults[index] = partial.details.results[0];
								emitParallelUpdate();
							}
						},
						makeDetails("parallel"),
						taskModelOverride,
					);
			let result = await runTask();
			const isFailed = result.exitCode !== 0 || (result.messages.length === 0 && !signal?.aborted);
			if (isFailed && MAX_RETRIES > 0 && !signal?.aborted) {
				result = await runTask();
			}

			updateWorker(workerId, result.exitCode === 0 ? "completed" : "failed");
			allResults[index] = result;
			emitParallelUpdate();
			return result;
		});

		const successCount = results.filter((r) => r.exitCode === 0).length;
		const summaries = results.map((r) => {
			const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
			const output = isError
				? (r.errorMessage || r.stderr || getFinalOutput(r.messages) || "(no output)")
				: getFinalOutput(r.messages);
			return `[${r.agent}] ${r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`}: ${output || "(no output)"}`;
		});
		return {
			content: [{ type: "text", text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}` }],
			details: makeDetails("parallel")(results),
		};
	}

	if (params.agent && params.task) {
		let isolation: IsolationEnvironment | null = null;
		let mergeResult: MergeResult | undefined;
		try {
			const effectiveCwd = params.cwd ?? defaultCwd;
			if (useIsolation) {
				const taskId = crypto.randomUUID();
				isolation = await createIsolation(effectiveCwd, taskId, readIsolationMode());
			}

			const result = cmuxSplitsEnabled
				? await runSingleAgentInCmuxSplit(
					cmuxClient,
					"right",
					defaultCwd,
					agents,
					params.agent,
					params.task,
					isolation ? isolation.workDir : params.cwd,
					undefined,
					signal,
					onUpdate,
					makeDetails("single"),
					params.model,
				)
				: await runSingleAgent(
					defaultCwd,
					agents,
					params.agent,
					params.task,
					isolation ? isolation.workDir : params.cwd,
					undefined,
					signal,
					onUpdate,
					makeDetails("single"),
					params.model,
				);

			if (isolation) {
				const patches = await isolation.captureDelta();
				if (patches.length > 0) mergeResult = await mergeDeltaPatches(effectiveCwd, patches);
			}

			const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
			if (isError) {
				const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
				return {
					content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
					details: makeDetails("single")([result]),
					isError: true,
				};
			}

			let outputText = getFinalOutput(result.messages) || "(no output)";
			if (mergeResult && !mergeResult.success) {
				outputText += `\n\n⚠ Patch merge failed: ${mergeResult.error || "unknown error"}`;
			}
			return {
				content: [{ type: "text", text: outputText }],
				details: makeDetails("single")([result]),
			};
		} finally {
			if (isolation) await isolation.cleanup();
		}
	}

	const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
	return {
		content: [{ type: "text", text: `Invalid parameters. Available agents: ${available}` }],
		details: makeDetails("single")([]),
		isError: true,
	};
}

function getFinalOutput(messages: Message[]): string {
	for (let i = messages.length - 1; i >= 0; i--) {
		const msg = messages[i];
		if (msg.role === "assistant") {
			for (const part of msg.content) {
				if (part.type === "text") return part.text;
			}
		}
	}
	return "";
}

type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, any> };

function getDisplayItems(messages: Message[]): DisplayItem[] {
	const items: DisplayItem[] = [];
	for (const msg of messages) {
		if (msg.role === "assistant") {
			for (const part of msg.content) {
				if (part.type === "text") items.push({ type: "text", text: part.text });
				else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
			}
		}
	}
	return items;
}

async function mapWithConcurrencyLimit<TIn, TOut>(
	items: TIn[],
	concurrency: number,
	fn: (item: TIn, index: number) => Promise<TOut>,
): Promise<TOut[]> {
	if (items.length === 0) return [];
	const limit = Math.max(1, Math.min(concurrency, items.length));
	const results: TOut[] = new Array(items.length);
	let nextIndex = 0;
	const workers = new Array(limit).fill(null).map(async () => {
		while (true) {
			const current = nextIndex++;
			if (current >= items.length) return;
			results[current] = await fn(items[current], current);
		}
	});
	await Promise.all(workers);
	return results;
}

function writePromptToTempFile(agentName: string, prompt: string): { dir: string; filePath: string } {
	const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
	const safeName = agentName.replace(/[^\w.-]+/g, "_");
	const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
	fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
	return { dir: tmpDir, filePath };
}

function buildSubagentProcessArgs(
	agent: AgentConfig,
	task: string,
	tmpPromptPath: string | null,
	modelOverride?: string,
): string[] {
	const args: string[] = ["--mode", "json", "-p", "--no-session"];
	const modelToUse = modelOverride ?? agent.model;
	if (modelToUse) args.push("--model", modelToUse);
	if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
	if (tmpPromptPath) args.push("--append-system-prompt", tmpPromptPath);
	args.push(`Task: ${task}`);
	return args;
}

function processSubagentEventLine(
	line: string,
	currentResult: SingleResult,
	emitUpdate: () => void,
): void {
	if (!line.trim()) return;
	let event: any;
	try {
		event = JSON.parse(line);
	} catch {
		return;
	}

	if (event.type === "message_end" && event.message) {
		const msg = event.message as Message;
		currentResult.messages.push(msg);

		if (msg.role === "assistant") {
			currentResult.usage.turns++;
			const usage = msg.usage;
			if (usage) {
				currentResult.usage.input += usage.input || 0;
				currentResult.usage.output += usage.output || 0;
				currentResult.usage.cacheRead += usage.cacheRead || 0;
				currentResult.usage.cacheWrite += usage.cacheWrite || 0;
				currentResult.usage.cost += usage.cost?.total || 0;
				currentResult.usage.contextTokens = usage.totalTokens || 0;
			}
			if (!currentResult.model && msg.model) currentResult.model = msg.model;
			if (msg.stopReason) currentResult.stopReason = msg.stopReason;
			if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
		}
		emitUpdate();
	}

	if (event.type === "tool_result_end" && event.message) {
		currentResult.messages.push(event.message as Message);
		emitUpdate();
	}
}

async function waitForFile(filePath: string, signal: AbortSignal | undefined, timeoutMs = 30 * 60 * 1000): Promise<boolean> {
	const started = Date.now();
	while (Date.now() - started < timeoutMs) {
		if (signal?.aborted) return false;
		if (fs.existsSync(filePath)) return true;
		await new Promise((resolve) => setTimeout(resolve, 150));
	}
	return false;
}

type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;

async function runSingleAgent(
	defaultCwd: string,
	agents: AgentConfig[],
	agentName: string,
	task: string,
	cwd: string | undefined,
	step: number | undefined,
	signal: AbortSignal | undefined,
	onUpdate: OnUpdateCallback | undefined,
	makeDetails: (results: SingleResult[]) => SubagentDetails,
	modelOverride?: string,
): Promise<SingleResult> {
	const agent = agents.find((a) => a.name === agentName);

	if (!agent) {
		const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
		return {
			agent: agentName,
			agentSource: "unknown",
			task,
			exitCode: 1,
			messages: [],
			stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
			usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
			step,
		};
	}

	// SF phase guard: block agents that conflict with the active SF phase
	if (agent.conflictsWith && agent.conflictsWith.length > 0) {
		const activePhase = getCurrentPhase();
		if (activePhase && agent.conflictsWith.includes(activePhase)) {
			return {
				agent: agentName,
				agentSource: agent.source,
				task,
				exitCode: 1,
				messages: [],
				stderr: `Agent "${agentName}" is blocked: it conflicts with the active SF phase "${activePhase}". Use the built-in SF workflow instead.`,
				usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
				step,
			};
		}
	}

	let tmpPromptDir: string | null = null;
	let tmpPromptPath: string | null = null;

	const currentResult: SingleResult = {
		agent: agentName,
		agentSource: agent.source,
		task,
		exitCode: 0,
		messages: [],
		stderr: "",
		usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
		model: modelOverride ?? agent.model,
		step,
	};

	const emitUpdate = () => {
		if (onUpdate) {
			onUpdate({
				content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
				details: makeDetails([currentResult]),
			});
		}
	};

	try {
		if (agent.systemPrompt.trim()) {
			const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
			tmpPromptDir = tmp.dir;
			tmpPromptPath = tmp.filePath;
		}
		const args = buildSubagentProcessArgs(agent, task, tmpPromptPath, modelOverride);
		let wasAborted = false;

		const exitCode = await new Promise<number>((resolve) => {
			const bundledPaths = (process.env.SF_BUNDLED_EXTENSION_PATHS ?? "").split(path.delimiter).map(s => s.trim()).filter(Boolean);
			const extensionArgs = bundledPaths.flatMap(p => ["--extension", p]);
			const proc = spawn(
				process.execPath,
				[process.env.SF_BIN_PATH!, ...extensionArgs, ...args],
				{ cwd: cwd ?? defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] },
			);
			liveSubagentProcesses.add(proc);
			let buffer = "";

			proc.stdout.on("data", (data) => {
				buffer += data.toString();
				const lines = buffer.split("\n");
				buffer = lines.pop() || "";
				for (const line of lines) processSubagentEventLine(line, currentResult, emitUpdate);
			});

			proc.stderr.on("data", (data) => {
				currentResult.stderr += data.toString();
			});

			proc.on("close", (code) => {
				liveSubagentProcesses.delete(proc);
				if (buffer.trim()) processSubagentEventLine(buffer, currentResult, emitUpdate);
				resolve(code ?? 0);
			});

			proc.on("error", () => {
				liveSubagentProcesses.delete(proc);
				resolve(1);
			});

			if (signal) {
				const killProc = () => {
					wasAborted = true;
					proc.kill("SIGTERM");
					setTimeout(() => {
						if (!proc.killed) proc.kill("SIGKILL");
					}, 5000);
				};
				if (signal.aborted) killProc();
				else signal.addEventListener("abort", killProc, { once: true });
			}
		});

		currentResult.exitCode = exitCode;
		if (wasAborted) throw new Error("Subagent was aborted");
		return currentResult;
	} finally {
		if (tmpPromptPath)
			try {
				fs.unlinkSync(tmpPromptPath);
			} catch {
				/* ignore */
			}
		if (tmpPromptDir)
			try {
				fs.rmdirSync(tmpPromptDir);
			} catch {
				/* ignore */
			}
	}
}

async function runSingleAgentInCmuxSplit(
	cmuxClient: CmuxClient,
	directionOrSurfaceId: "right" | "down" | string,
	defaultCwd: string,
	agents: AgentConfig[],
	agentName: string,
	task: string,
	cwd: string | undefined,
	step: number | undefined,
	signal: AbortSignal | undefined,
	onUpdate: OnUpdateCallback | undefined,
	makeDetails: (results: SingleResult[]) => SubagentDetails,
	modelOverride?: string,
): Promise<SingleResult> {
	const agent = agents.find((a) => a.name === agentName);
	if (!agent) {
		return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, modelOverride);
	}

	let tmpPromptDir: string | null = null;
	let tmpPromptPath: string | null = null;
	let tmpOutputDir: string | null = null;

	const currentResult: SingleResult = {
		agent: agentName,
		agentSource: agent.source,
		task,
		exitCode: 0,
		messages: [],
		stderr: "",
		usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
		model: modelOverride ?? agent.model,
		step,
	};

	const emitUpdate = () => {
		if (onUpdate) {
			onUpdate({
				content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
				details: makeDetails([currentResult]),
			});
		}
	};

	try {
		if (agent.systemPrompt.trim()) {
			const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
			tmpPromptDir = tmp.dir;
			tmpPromptPath = tmp.filePath;
		}
		tmpOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-cmux-"));
		const stdoutPath = path.join(tmpOutputDir, "stdout.jsonl");
		const stderrPath = path.join(tmpOutputDir, "stderr.log");
		const exitPath = path.join(tmpOutputDir, "exit.code");
		// Accept either a pre-created surface ID or a direction to create a new split
		const isDirection = directionOrSurfaceId === "right" || directionOrSurfaceId === "down"
			|| directionOrSurfaceId === "left" || directionOrSurfaceId === "up";
		const cmuxSurfaceId = isDirection
			? await cmuxClient.createSplit(directionOrSurfaceId as "right" | "down" | "left" | "up")
			: directionOrSurfaceId;
		if (!cmuxSurfaceId) {
			return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, modelOverride);
		}

		const bundledPaths = (process.env.SF_BUNDLED_EXTENSION_PATHS ?? "").split(path.delimiter).map((s) => s.trim()).filter(Boolean);
		const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
		const processArgs = [process.env.SF_BIN_PATH!, ...extensionArgs, ...buildSubagentProcessArgs(agent, task, tmpPromptPath, modelOverride)];
		// Normalize all paths to forward slashes before embedding in bash strings.
		// On Windows, backslashes are interpreted as escape characters by bash,
		// mangling paths like C:\Users\user into C:Useruser (#1436).
		const bashPath = (p: string) => shellEscape(p.replaceAll("\\", "/"));
		const innerScript = [
			`cd ${bashPath(cwd ?? defaultCwd)}`,
			"set -o pipefail",
			`${bashPath(process.execPath)} ${processArgs.map(a => bashPath(a)).join(" ")} 2> >(tee ${bashPath(stderrPath)} >&2) | tee ${bashPath(stdoutPath)}`,
			"status=${PIPESTATUS[0]}",
			`printf '%s' "$status" > ${bashPath(exitPath)}`,
		].join("; ");

		const sent = await cmuxClient.sendSurface(cmuxSurfaceId, `bash -lc ${shellEscape(innerScript)}`);
		if (!sent) {
			return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, modelOverride);
		}

		const finished = await waitForFile(exitPath, signal);
		if (!finished) {
			currentResult.exitCode = 1;
			currentResult.stderr = "cmux split execution timed out or was aborted";
			return currentResult;
		}

		if (fs.existsSync(stdoutPath)) {
			const stdout = fs.readFileSync(stdoutPath, "utf-8");
			for (const line of stdout.split("\n")) {
				processSubagentEventLine(line, currentResult, emitUpdate);
			}
		}
		if (fs.existsSync(stderrPath)) {
			currentResult.stderr = fs.readFileSync(stderrPath, "utf-8");
		}
		currentResult.exitCode = Number.parseInt(fs.readFileSync(exitPath, "utf-8").trim() || "1", 10) || 0;
		return currentResult;
	} finally {
		if (tmpPromptPath)
			try {
				fs.unlinkSync(tmpPromptPath);
			} catch {
				/* ignore */
			}
		if (tmpPromptDir)
			try {
				fs.rmdirSync(tmpPromptDir);
			} catch {
				/* ignore */
			}
		if (tmpOutputDir)
			try {
				fs.rmSync(tmpOutputDir, { recursive: true, force: true });
			} catch {
				/* ignore */
			}
	}
}

const TaskItem = Type.Object({
	agent: Type.String({ description: "Name of the agent to invoke" }),
	task: Type.String({ description: "Task to delegate to the agent" }),
	cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
	model: Type.Optional(Type.String({ description: "Override the agent's default model for this task" })),
});

const ChainItem = Type.Object({
	agent: Type.String({ description: "Name of the agent to invoke" }),
	task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
	cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
	model: Type.Optional(Type.String({ description: "Override the agent's default model for this step" })),
});

const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
	description: 'Which agent directories to use. Default: "both" (user + project-local).',
	default: "both",
});

const SubagentParams = Type.Object({
	agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (for single mode)" })),
	task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
	background: Type.Optional(
		Type.Boolean({
			description:
				"Launch the subagent run in the background for later retrieval via await_subagent. " +
				"Useful for longer autonomous test or research waves.",
			default: false,
		}),
	),
	model: Type.Optional(Type.String({
		description: "Override the agent's default model. Applies to single mode, or as a default for all tasks/chain steps unless they set their own `model`.",
	})),
	tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
	chain: Type.Optional(Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution" })),
	agentScope: Type.Optional(AgentScopeSchema),
	confirmProjectAgents: Type.Optional(
		Type.Boolean({ description: "Prompt before running project-local agents. Default: false.", default: false }),
	),
	cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
	isolated: Type.Optional(
		Type.Boolean({
			description:
				"Run the subagent in an isolated filesystem (git worktree). " +
				"Changes are captured as patches and merged back. " +
				"Only available when taskIsolation.mode is configured in settings.",
			default: false,
		}),
	),
});

export default function (pi: ExtensionAPI) {
	let backgroundJobs: SubagentBackgroundJobManager<SubagentDetails> | null = null;

	function getBackgroundJobs(): SubagentBackgroundJobManager<SubagentDetails> {
		if (!backgroundJobs) throw new Error("Subagent background job manager not initialized. Wait for session_start.");
		return backgroundJobs;
	}

	pi.on("session_start", async () => {
		backgroundJobs = new SubagentBackgroundJobManager<SubagentDetails>({
			onJobComplete: (job) => {
				if (job.awaited) return;
				const elapsed = ((Date.now() - job.startTime) / 1000).toFixed(1);
				const output = job.result
					? getPrimaryTextContent(job.result)
					: `Error: ${job.errorText ?? "unknown error"}`;
				const maxLen = 2000;
				const truncated = output.length > maxLen
					? `${output.slice(0, maxLen)}\n\n[... truncated, use await_subagent for full output]`
					: output;
				pi.sendMessage(
					{
						customType: "subagent_job_result",
						content: [
							`**Background subagent ${job.status}: ${job.id}** (${job.label}, ${elapsed}s)`,
							"",
							truncated,
						].join("\n"),
						display: true,
					},
					{ deliverAs: "followUp" },
				);
			},
		});
	});

	pi.on("session_before_switch", async () => {
		if (!backgroundJobs) return;
		for (const job of backgroundJobs.getRunningJobs()) {
			backgroundJobs.cancel(job.id);
		}
	});

	pi.on("session_shutdown", async () => {
		if (backgroundJobs) {
			backgroundJobs.shutdown();
			backgroundJobs = null;
		}
		await stopLiveSubagents();
	});

	// /subagent command - list available agents
	pi.registerCommand("subagent", {
		description: "List available subagents",
		handler: async (_args, ctx) => {
			const discovery = discoverAgents(ctx.cwd, "both");
			if (discovery.agents.length === 0) {
				ctx.ui.notify("No agents found. Add .md files to ~/.sf/agent/agents/ or .sf/agents/", "warning");
				return;
			}
			const lines = discovery.agents.map(
				(a) => `  ${a.name} [${a.source}]${a.model ? ` (${a.model})` : ""}: ${a.description}`,
			);
			ctx.ui.notify(`Available agents (${discovery.agents.length}):\n${lines.join("\n")}`, "info");
		},
	});

	pi.registerTool({
		name: "subagent",
		label: "Subagent",
		description: [
			"Delegate tasks to specialized subagents with isolated context windows.",
			"Each subagent is a separate pi process with its own tools, model, and system prompt.",
			"Modes: single ({ agent, task }), parallel ({ tasks: [{agent, task},...] }), chain ({ chain: [{agent, task},...] } with {previous} placeholder).",
			"Agents are defined as .md files in ~/.sf/agent/agents/ (user) or .sf/agents/ (project).",
			"Use the /subagent command to list available agents and their descriptions.",
			"Use chain mode to pipeline: scout finds context, planner designs, worker implements.",
		].join(" "),
		promptGuidelines: [
			"Use subagent to delegate self-contained tasks that benefit from an isolated context window.",
			"Use scout agent first when you need codebase context before implementing.",
			"Use chain mode for scout→planner→worker or worker→reviewer→worker pipelines.",
			"Use parallel mode when tasks are independent and don't need each other's output.",
			"Always check available agents with /subagent before choosing one.",
		],
		parameters: SubagentParams,

		async execute(_toolCallId, params, signal, onUpdate, ctx) {
			const agentScope: AgentScope = params.agentScope ?? "both";
			const discovery = discoverAgents(ctx.cwd, agentScope);
			const agents = discovery.agents;
			const confirmProjectAgents = params.confirmProjectAgents ?? false;
			const cmuxClient = CmuxClient.fromPreferences(loadEffectiveSFPreferences()?.preferences);
			const cmuxSplitsEnabled = cmuxClient.getConfig().splits;

			// Resolve isolation mode
			const isolationMode = readIsolationMode();
			const useIsolation = Boolean(params.isolated) && isolationMode !== "none";

			const hasChain = (params.chain?.length ?? 0) > 0;
			const hasTasks = (params.tasks?.length ?? 0) > 0;
			const hasSingle = Boolean(params.agent && params.task);
			const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);

			const makeDetails =
				(mode: "single" | "parallel" | "chain") =>
				(results: SingleResult[]): SubagentDetails => ({
					mode,
					agentScope,
					projectAgentsDir: discovery.projectAgentsDir,
					results,
				});

			if (modeCount !== 1) {
				const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
				return {
					content: [
						{
							type: "text",
							text: `Invalid parameters. Provide exactly one mode.\nAvailable agents: ${available}`,
						},
					],
					details: makeDetails("single")([]),
				};
			}

			if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && ctx.hasUI) {
				const requestedAgentNames = new Set<string>();
				if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
				if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
				if (params.agent) requestedAgentNames.add(params.agent);

				const projectAgentsRequested = Array.from(requestedAgentNames)
					.map((name) => agents.find((a) => a.name === name))
					.filter((a): a is AgentConfig => a?.source === "project");

				if (projectAgentsRequested.length > 0) {
					const names = projectAgentsRequested.map((a) => a.name).join(", ");
					const dir = discovery.projectAgentsDir ?? "(unknown)";
					const ok = await ctx.ui.confirm(
						"Run project-local agents?",
						`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
					);
					if (!ok)
						return {
							content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
							details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
						};
				}
			}
			if (params.background) {
				const manager = getBackgroundJobs();
				const jobId = manager.register(summarizeBackgroundInvocation(params), (backgroundSignal) =>
					executeSubagentInvocation({
						defaultCwd: ctx.cwd,
						agents,
						agentScope,
						projectAgentsDir: discovery.projectAgentsDir,
						params: { ...params, confirmProjectAgents: false, background: false },
						signal: backgroundSignal,
						onUpdate: undefined,
						cmuxClient,
						cmuxSplitsEnabled,
						useIsolation,
					}),
				);
				return {
					content: [{
						type: "text",
						text:
							`Background subagent job started: **${jobId}**\n` +
							`Invocation: \`${summarizeBackgroundInvocation(params)}\`\n\n` +
							"Use `await_subagent` to retrieve the result or `cancel_subagent` to stop it.",
					}],
					details: undefined,
				};
			}

			return executeSubagentInvocation({
				defaultCwd: ctx.cwd,
				agents,
				agentScope,
				projectAgentsDir: discovery.projectAgentsDir,
				params,
				signal,
				onUpdate,
				cmuxClient,
				cmuxSplitsEnabled,
				useIsolation,
			});
		},

		renderCall(args, theme) {
			const scope: AgentScope = args.agentScope ?? "both";
			if (args.chain && args.chain.length > 0) {
				let text =
					theme.fg("toolTitle", theme.bold("subagent ")) +
					theme.fg("accent", `chain (${args.chain.length} steps)`) +
					theme.fg("muted", ` [${scope}]`);
				for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
					const step = args.chain[i];
					// Clean up {previous} placeholder for display
					const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
					const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
					text +=
						"\n  " +
						theme.fg("muted", `${i + 1}.`) +
						" " +
						theme.fg("accent", step.agent) +
						theme.fg("dim", ` ${preview}`);
				}
				if (args.chain.length > 3) text += `\n  ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`;
				return new Text(text, 0, 0);
			}
			if (args.tasks && args.tasks.length > 0) {
				let text =
					theme.fg("toolTitle", theme.bold("subagent ")) +
					theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
					theme.fg("muted", ` [${scope}]`);
				for (const t of args.tasks.slice(0, 3)) {
					const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
					text += `\n  ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
				}
				if (args.tasks.length > 3) text += `\n  ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
				return new Text(text, 0, 0);
			}
			const agentName = args.agent || "...";
			const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "...";
			let text =
				theme.fg("toolTitle", theme.bold("subagent ")) +
				theme.fg("accent", agentName) +
				theme.fg("muted", ` [${scope}]`);
			text += `\n  ${theme.fg("dim", preview)}`;
			return new Text(text, 0, 0);
		},

		renderResult(result, { expanded }, theme) {
			const details = result.details as SubagentDetails | undefined;
			if (!details || details.results.length === 0) {
				const text = result.content[0];
				return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
			}

			const mdTheme = getMarkdownTheme();

			const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
				const toShow = limit ? items.slice(-limit) : items;
				const skipped = limit && items.length > limit ? items.length - limit : 0;
				let text = "";
				if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
				for (const item of toShow) {
					if (item.type === "text") {
						const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
						text += `${theme.fg("toolOutput", preview)}\n`;
					} else {
						text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
					}
				}
				return text.trimEnd();
			};

			if (details.mode === "single" && details.results.length === 1) {
				const r = details.results[0];
				const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
				const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
				const displayItems = getDisplayItems(r.messages);
				const finalOutput = getFinalOutput(r.messages);

				if (expanded) {
					const container = new Container();
					let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
					if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
					container.addChild(new Text(header, 0, 0));
					if (isError && r.errorMessage)
						container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
					container.addChild(new Spacer(1));
					container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
					container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
					container.addChild(new Spacer(1));
					container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
					if (displayItems.length === 0 && !finalOutput) {
						container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
					} else {
						for (const item of displayItems) {
							if (item.type === "toolCall")
								container.addChild(
									new Text(
										theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
										0,
										0,
									),
								);
						}
						if (finalOutput) {
							container.addChild(new Spacer(1));
							container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
						}
					}
					const usageStr = formatUsageStats(r.usage, r.model);
					if (usageStr) {
						container.addChild(new Spacer(1));
						container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
					}
					return container;
				}

				let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
				if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
				if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
				else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
				else {
					text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
					if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
				}
				const usageStr = formatUsageStats(r.usage, r.model);
				if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
				return new Text(text, 0, 0);
			}

			const aggregateUsage = (results: SingleResult[]) => {
				const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
				for (const r of results) {
					total.input += r.usage.input;
					total.output += r.usage.output;
					total.cacheRead += r.usage.cacheRead;
					total.cacheWrite += r.usage.cacheWrite;
					total.cost += r.usage.cost;
					total.turns += r.usage.turns;
				}
				return total;
			};

			if (details.mode === "chain") {
				const successCount = details.results.filter((r) => r.exitCode === 0).length;
				const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");

				if (expanded) {
					const container = new Container();
					container.addChild(
						new Text(
							icon +
								" " +
								theme.fg("toolTitle", theme.bold("chain ")) +
								theme.fg("accent", `${successCount}/${details.results.length} steps`),
							0,
							0,
						),
					);

					for (const r of details.results) {
						const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
						const displayItems = getDisplayItems(r.messages);
						const finalOutput = getFinalOutput(r.messages);

						container.addChild(new Spacer(1));
						container.addChild(
							new Text(
								`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
								0,
								0,
							),
						);
						container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));

						// Show tool calls
						for (const item of displayItems) {
							if (item.type === "toolCall") {
								container.addChild(
									new Text(
										theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
										0,
										0,
									),
								);
							}
						}

						// Show final output as markdown
						if (finalOutput) {
							container.addChild(new Spacer(1));
							container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
						}

						const stepUsage = formatUsageStats(r.usage, r.model);
						if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
					}

					const usageStr = formatUsageStats(aggregateUsage(details.results));
					if (usageStr) {
						container.addChild(new Spacer(1));
						container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
					}
					return container;
				}

				// Collapsed view
				let text =
					icon +
					" " +
					theme.fg("toolTitle", theme.bold("chain ")) +
					theme.fg("accent", `${successCount}/${details.results.length} steps`);
				for (const r of details.results) {
					const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
					const displayItems = getDisplayItems(r.messages);
					text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
					if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
					else text += `\n${renderDisplayItems(displayItems, 5)}`;
				}
				const usageStr = formatUsageStats(aggregateUsage(details.results));
				if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
				text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
				return new Text(text, 0, 0);
			}

			if (details.mode === "parallel") {
				const running = details.results.filter((r) => r.exitCode === -1).length;
				const successCount = details.results.filter((r) => r.exitCode === 0).length;
				const failCount = details.results.filter((r) => r.exitCode > 0).length;
				const isRunning = running > 0;
				const icon = isRunning
					? theme.fg("warning", "⏳")
					: failCount > 0
						? theme.fg("warning", "◐")
						: theme.fg("success", "✓");
				const status = isRunning
					? `${successCount + failCount}/${details.results.length} done, ${running} running`
					: `${successCount}/${details.results.length} tasks`;

				if (expanded && !isRunning) {
					const container = new Container();
					container.addChild(
						new Text(
							`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
							0,
							0,
						),
					);

					for (const r of details.results) {
						const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
						const displayItems = getDisplayItems(r.messages);
						const finalOutput = getFinalOutput(r.messages);

						container.addChild(new Spacer(1));
						container.addChild(
							new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
						);
						container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));

						// Show tool calls
						for (const item of displayItems) {
							if (item.type === "toolCall") {
								container.addChild(
									new Text(
										theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
										0,
										0,
									),
								);
							}
						}

						// Show final output as markdown
						if (finalOutput) {
							container.addChild(new Spacer(1));
							container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
						}

						const taskUsage = formatUsageStats(r.usage, r.model);
						if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
					}

					const usageStr = formatUsageStats(aggregateUsage(details.results));
					if (usageStr) {
						container.addChild(new Spacer(1));
						container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
					}
					return container;
				}

				// Collapsed view (or still running)
				let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
				for (const r of details.results) {
					const rIcon =
						r.exitCode === -1
							? theme.fg("warning", "⏳")
							: r.exitCode === 0
								? theme.fg("success", "✓")
								: theme.fg("error", "✗");
					const displayItems = getDisplayItems(r.messages);
					text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
					if (displayItems.length === 0)
						text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
					else text += `\n${renderDisplayItems(displayItems, 5)}`;
				}
				if (!isRunning) {
					const usageStr = formatUsageStats(aggregateUsage(details.results));
					if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
				}
				if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
				return new Text(text, 0, 0);
			}

			const text = result.content[0];
			return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
		},
	});

	pi.registerTool({
		name: "await_subagent",
		label: "Await Subagent",
		description: "Wait for a background subagent job to complete and return its full result.",
		parameters: Type.Object({
			job_id: Type.String({ description: "Background subagent job ID (for example sub_a1b2c3d4)" }),
			timeout: Type.Optional(Type.Number({
				description:
					"Maximum seconds to wait before returning control. Defaults to 120. " +
					"If the timeout is reached the subagent keeps running in the background.",
			})),
		}),
		async execute(_toolCallId, params) {
			const manager = getBackgroundJobs();
			const job = manager.getJob(params.job_id);
			if (!job) {
				return { content: [{ type: "text", text: `Background subagent job not found: ${params.job_id}` }], details: undefined };
			}

			manager.suppressFollowUp(job.id);
			if (job.status !== "running") {
				if (job.result) return job.result;
				return {
					content: [{ type: "text", text: `Background subagent ${job.status}: ${job.errorText ?? "unknown error"}` }],
					details: undefined,
					isError: job.status !== "completed",
				};
			}

			const timeoutSeconds = params.timeout ?? 120;
			const timeoutMs = timeoutSeconds * 1000;
			const TIMEOUT_SENTINEL = Symbol("timeout");
			const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) => {
				const timer = setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs);
				if (typeof timer === "object" && "unref" in timer) timer.unref();
			});
			const raceResult = await Promise.race([job.promise.then(() => "completed" as const), timeoutPromise]);
			if (raceResult === TIMEOUT_SENTINEL) {
				const elapsed = ((Date.now() - job.startTime) / 1000).toFixed(1);
				return {
					content: [{
						type: "text",
						text:
							`Background subagent still running: **${job.id}** (${job.label}, ${elapsed}s)\n\n` +
							`Timed out after ${timeoutSeconds}s. Call \`await_subagent\` again later or \`cancel_subagent\` to stop it.`,
					}],
					details: undefined,
				};
			}

			const finished = manager.getJob(job.id);
			if (finished?.result) return finished.result;
			return {
				content: [{ type: "text", text: `Background subagent ${finished?.status ?? "failed"}: ${finished?.errorText ?? "unknown error"}` }],
				details: undefined,
				isError: true,
			};
		},
	});

	pi.registerTool({
		name: "cancel_subagent",
		label: "Cancel Subagent",
		description: "Cancel a running background subagent job by ID.",
		parameters: Type.Object({
			job_id: Type.String({ description: "Background subagent job ID (for example sub_a1b2c3d4)" }),
		}),
		async execute(_toolCallId, params) {
			const manager = getBackgroundJobs();
			const result = manager.cancel(params.job_id);
			const messages: Record<string, string> = {
				cancelled: `Background subagent ${params.job_id} has been cancelled.`,
				not_found: `Background subagent ${params.job_id} not found.`,
				already_completed: `Background subagent ${params.job_id} has already completed (or failed/cancelled).`,
			};
			return { content: [{ type: "text", text: messages[result] ?? `Unknown result: ${result}` }], details: undefined };
		},
	});
}
