/**
 * Subagent Tool for Draht — adapted from the pi subagent example
 *
 * Spawns isolated draht processes for delegated tasks.
 * Agents are defined in .draht/agents/*.md (project) or ~/.draht/agent/agents/*.md (global).
 *
 * Modes:
 *   single   — { agent, task }
 *   parallel — { tasks: [{ agent, task }] }  (max 8, concurrency 4)
 *   chain    — { chain: [{ agent, task }] }  (sequential, {previous} placeholder)
 */
import { spawn } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { StringEnum } from "@draht/ai";
import { Text } from "@draht/tui";
import { Type } from "@sinclair/typebox";
import { getAgentDir, getPackageDir, isBunBinary } from "../../config.js";
import { parseFrontmatter } from "../../utils/frontmatter.js";
const MAX_PARALLEL = 8;
const MAX_CONCURRENCY = 4;
// Build the command to spawn a subagent process.
// Compiled Bun binary: process.execPath IS the CLI binary, args go directly.
// Source/dev mode: process.execPath is the runtime (bun/node), process.argv[1] is the CLI script.
const DRAHT_BIN = process.execPath;
const DRAHT_ARGS_PREFIX = isBunBinary ? [] : [process.argv[1]];
function loadAgentsFromDir(dir, source) {
    if (!fs.existsSync(dir))
        return [];
    let entries;
    try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
    }
    catch {
        return [];
    }
    const agents = [];
    for (const entry of entries) {
        if (!entry.isFile() || !entry.name.endsWith(".md"))
            continue;
        try {
            const content = fs.readFileSync(path.join(dir, entry.name), "utf-8");
            const { frontmatter, body } = parseFrontmatter(content);
            if (!frontmatter.name || !frontmatter.description)
                continue;
            const tools = frontmatter.tools
                ?.split(",")
                .map((t) => t.trim())
                .filter(Boolean);
            agents.push({
                name: frontmatter.name,
                description: frontmatter.description,
                tools: tools?.length ? tools : undefined,
                model: frontmatter.model,
                systemPrompt: body,
                source,
            });
        }
        catch { }
    }
    return agents;
}
function findProjectAgentsDir(cwd) {
    let dir = cwd;
    while (true) {
        const candidate = path.join(dir, ".draht", "agents");
        try {
            if (fs.statSync(candidate).isDirectory())
                return candidate;
        }
        catch { }
        const parent = path.dirname(dir);
        if (parent === dir)
            return null;
        dir = parent;
    }
}
function discoverAgents(cwd, scope) {
    // Shipped agents (bundled with the package) — lowest priority
    const shippedDir = path.join(getPackageDir(), "agents");
    const shippedAgents = loadAgentsFromDir(shippedDir, "user");
    const userDir = path.join(getAgentDir(), "agents");
    const projectDir = findProjectAgentsDir(cwd);
    const userAgents = scope !== "project" ? loadAgentsFromDir(userDir, "user") : [];
    const projectAgents = scope !== "user" && projectDir ? loadAgentsFromDir(projectDir, "project") : [];
    // Priority: shipped < user < project
    const map = new Map();
    for (const a of shippedAgents)
        map.set(a.name, a);
    for (const a of userAgents)
        map.set(a.name, a);
    for (const a of projectAgents)
        map.set(a.name, a);
    return Array.from(map.values());
}
function writeTemp(name, content) {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), "draht-subagent-"));
    const file = path.join(dir, `${name.replace(/[^\w.-]/g, "_")}.md`);
    fs.writeFileSync(file, content, { encoding: "utf-8", mode: 0o600 });
    return { file, dir };
}
function cleanTemp(file, dir) {
    try {
        fs.unlinkSync(file);
    }
    catch { }
    try {
        fs.rmdirSync(dir);
    }
    catch { }
}
function getFinalText(messages) {
    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 "";
}
async function runAgent(cwd, agent, task, signal, step, onProgress) {
    const args = ["--mode", "json", "-p", "--no-session"];
    if (agent.model)
        args.push("--model", agent.model);
    if (agent.tools?.length)
        args.push("--tools", agent.tools.join(","));
    let tmpFile = null;
    let tmpDir = null;
    if (agent.systemPrompt.trim()) {
        const tmp = writeTemp(agent.name, agent.systemPrompt);
        tmpFile = tmp.file;
        tmpDir = tmp.dir;
        args.push("--append-system-prompt", tmpFile);
    }
    args.push(`Task: ${task}`);
    const messages = [];
    let stderr = "";
    try {
        const exitCode = await new Promise((resolve) => {
            const proc = spawn(DRAHT_BIN, [...DRAHT_ARGS_PREFIX, ...args], {
                cwd,
                shell: false,
                stdio: ["ignore", "pipe", "pipe"],
            });
            let buf = "";
            const processLine = (line) => {
                if (!line.trim())
                    return;
                try {
                    const event = JSON.parse(line);
                    if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
                        messages.push(event.message);
                    }
                    // Stream activity updates to the parent
                    if (onProgress) {
                        if (event.type === "tool_execution_start") {
                            const toolArgs = event.args ?? {};
                            const detail = toolArgs.command || toolArgs.path || toolArgs.file_path || toolArgs.pattern || "";
                            const short = typeof detail === "string" && detail.length > 60 ? `${detail.slice(0, 60)}...` : detail;
                            onProgress(short ? `${event.toolName} ${short}` : event.toolName);
                        }
                        else if (event.type === "text_delta") {
                            // Show first line of streaming text as activity
                            const text = event.text?.trim();
                            if (text) {
                                const firstLine = text.split("\n")[0];
                                const short = firstLine.length > 80 ? `${firstLine.slice(0, 80)}...` : firstLine;
                                onProgress(short);
                            }
                        }
                    }
                }
                catch { }
            };
            proc.stdout.on("data", (d) => {
                buf += d.toString();
                const lines = buf.split("\n");
                buf = lines.pop() || "";
                for (const l of lines)
                    processLine(l);
            });
            proc.stderr.on("data", (d) => {
                stderr += d.toString();
            });
            proc.on("close", (code) => {
                if (buf.trim())
                    processLine(buf);
                resolve(code ?? 0);
            });
            proc.on("error", () => resolve(1));
            if (signal) {
                const kill = () => {
                    proc.kill("SIGTERM");
                    setTimeout(() => {
                        if (!proc.killed)
                            proc.kill("SIGKILL");
                    }, 5000);
                };
                if (signal.aborted)
                    kill();
                else
                    signal.addEventListener("abort", kill, { once: true });
            }
        });
        return { agent: agent.name, task, exitCode, output: getFinalText(messages), stderr, step };
    }
    finally {
        if (tmpFile && tmpDir)
            cleanTemp(tmpFile, tmpDir);
    }
}
async function runParallel(items, concurrency, fn) {
    const results = new Array(items.length);
    let next = 0;
    await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, async () => {
        while (true) {
            const i = next++;
            if (i >= items.length)
                return;
            results[i] = await fn(items[i], i);
        }
    }));
    return results;
}
// ─── Extension ──────────────────────────────────────────────────────────────
const TaskItem = Type.Object({
    agent: Type.String({ description: "Agent name" }),
    task: Type.String({ description: "Task description" }),
});
const ChainItem = Type.Object({
    agent: Type.String({ description: "Agent name" }),
    task: Type.String({ description: "Task, optionally using {previous} placeholder" }),
});
const Params = Type.Object({
    agent: Type.Optional(Type.String()),
    task: Type.Optional(Type.String()),
    tasks: Type.Optional(Type.Array(TaskItem, { description: "Parallel tasks" })),
    chain: Type.Optional(Type.Array(ChainItem, { description: "Chained tasks" })),
    agentScope: Type.Optional(StringEnum(["user", "project", "both"], { default: "both" })),
});
// ─── Rendering helpers ──────────────────────────────────────────────────────
function truncateTask(task, maxLen = 120) {
    const oneLine = task.replace(/\n/g, " ").trim();
    return oneLine.length > maxLen ? `${oneLine.slice(0, maxLen)}...` : oneLine;
}
export default function (pi) {
    pi.registerTool({
        name: "subagent",
        label: "Subagent",
        description: "Delegate to specialized agents. single: {agent,task} | parallel: {tasks:[]} | chain: {chain:[]} with {previous} placeholder. agentScope: 'both' (default) uses project .draht/agents/ + global.",
        parameters: Params,
        renderCall(args, theme) {
            const lines = [];
            if (args.chain?.length) {
                const agents = args.chain.map((s) => s.agent).join(" -> ");
                lines.push(`${theme.fg("toolTitle", theme.bold(`subagent chain`))} ${theme.fg("accent", agents)}`);
                for (let i = 0; i < args.chain.length; i++) {
                    const step = args.chain[i];
                    const prefix = theme.fg("muted", `  ${i + 1}.`);
                    lines.push(`${prefix} ${theme.fg("accent", step.agent)} ${theme.fg("toolOutput", truncateTask(step.task))}`);
                }
            }
            else if (args.tasks?.length) {
                lines.push(theme.fg("toolTitle", theme.bold(`subagent parallel`)) +
                    theme.fg("muted", ` (${args.tasks.length} tasks)`));
                for (const t of args.tasks) {
                    lines.push(`  ${theme.fg("accent", t.agent)} ${theme.fg("toolOutput", truncateTask(t.task))}`);
                }
            }
            else if (args.agent) {
                lines.push(theme.fg("toolTitle", theme.bold("subagent")) +
                    " " +
                    theme.fg("accent", args.agent) +
                    (args.task ? ` ${theme.fg("toolOutput", truncateTask(args.task))}` : ""));
            }
            else {
                lines.push(theme.fg("toolTitle", theme.bold("subagent")));
            }
            return new Text(lines.join("\n"), 0, 0);
        },
        renderResult(result, options, theme) {
            const status = result.details?.status;
            const output = result.content
                ?.filter((c) => c.type === "text")
                .map((c) => ("text" in c ? c.text : ""))
                .join("\n") || "";
            const lines = [];
            if (status) {
                lines.push(theme.fg("muted", status));
            }
            if (output.trim()) {
                const trimmed = output.trim();
                if (options.expanded || options.isPartial) {
                    // When expanded or during execution, show full activity log
                    for (const line of trimmed.split("\n")) {
                        lines.push(theme.fg("toolOutput", line));
                    }
                }
                else {
                    // Collapsed final result — show preview
                    const allLines = trimmed.split("\n");
                    const previewLines = allLines.slice(0, 8);
                    lines.push(theme.fg("toolOutput", previewLines.join("\n")));
                    if (allLines.length > 8) {
                        lines.push(theme.fg("muted", `... (${allLines.length - 8} more lines)`));
                    }
                }
            }
            if (lines.length === 0)
                return new Text("", 0, 0);
            return new Text(lines.join("\n"), 0, 0);
        },
        async execute(_id, params, signal, onUpdate, ctx) {
            const scope = params.agentScope ?? "both";
            const agents = discoverAgents(ctx.cwd, scope);
            const available = agents.map((a) => a.name).join(", ") || "none";
            const find = (name) => agents.find((a) => a.name === name);
            const notFound = (name) => ({
                content: [{ type: "text", text: `Unknown agent "${name}". Available: ${available}` }],
                isError: true,
                details: {},
            });
            // Stream live activity from the subagent process
            const activityLines = [];
            const emitProgress = (agentName, activity) => {
                const status = activity ? `${agentName}: ${activity}` : `${agentName} working...`;
                onUpdate?.({
                    content: [{ type: "text", text: activityLines.join("\n") }],
                    details: { status },
                });
            };
            const makeProgressFn = (agentName) => (activity) => {
                // Keep a rolling log of recent activities
                activityLines.push(`${agentName}: ${activity}`);
                if (activityLines.length > 50)
                    activityLines.splice(0, activityLines.length - 50);
                emitProgress(agentName, activity);
            };
            // ── Chain mode ──
            if (params.chain?.length) {
                let previous = "";
                const results = [];
                for (let i = 0; i < params.chain.length; i++) {
                    const step = params.chain[i];
                    const agent = find(step.agent);
                    if (!agent)
                        return notFound(step.agent);
                    activityLines.length = 0;
                    emitProgress(step.agent, `step ${i + 1}/${params.chain.length}`);
                    const task = step.task.replace(/\{previous\}/g, previous);
                    const result = await runAgent(ctx.cwd, agent, task, signal, i + 1, makeProgressFn(step.agent));
                    results.push(result);
                    if (result.exitCode !== 0) {
                        return {
                            content: [
                                {
                                    type: "text",
                                    text: `Chain failed at step ${i + 1} (${step.agent}):\n${result.output || result.stderr}`,
                                },
                            ],
                            isError: true,
                            details: {},
                        };
                    }
                    previous = result.output;
                }
                return {
                    content: [{ type: "text", text: results[results.length - 1].output || "(no output)" }],
                    details: {},
                };
            }
            // ── Parallel mode ──
            if (params.tasks?.length) {
                if (params.tasks.length > MAX_PARALLEL) {
                    return {
                        content: [{ type: "text", text: `Too many tasks (max ${MAX_PARALLEL})` }],
                        isError: true,
                        details: {},
                    };
                }
                for (const t of params.tasks) {
                    if (!find(t.agent))
                        return notFound(t.agent);
                }
                const agentNames = params.tasks.map((t) => t.agent).join(", ");
                emitProgress(agentNames);
                const results = await runParallel(params.tasks, MAX_CONCURRENCY, async (t, _i) => {
                    return runAgent(ctx.cwd, find(t.agent), t.task, signal, undefined, makeProgressFn(t.agent));
                });
                const ok = results.filter((r) => r.exitCode === 0).length;
                const summary = results
                    .map((r) => `[${r.agent}] ${r.exitCode === 0 ? "ok" : "fail"} ${r.output.slice(0, 200)}`)
                    .join("\n\n");
                return {
                    content: [{ type: "text", text: `Parallel: ${ok}/${results.length} succeeded\n\n${summary}` }],
                    details: {},
                };
            }
            // ── Single mode ──
            if (params.agent && params.task) {
                const agent = find(params.agent);
                if (!agent)
                    return notFound(params.agent);
                emitProgress(params.agent);
                const result = await runAgent(ctx.cwd, agent, params.task, signal, undefined, makeProgressFn(params.agent));
                const isError = result.exitCode !== 0;
                return {
                    content: [{ type: "text", text: result.output || result.stderr || "(no output)" }],
                    ...(isError ? { isError: true } : {}),
                    details: {},
                };
            }
            return {
                content: [{ type: "text", text: `Provide exactly one mode. Available agents: ${available}` }],
                isError: true,
                details: {},
            };
        },
    });
    // ── Agent selection for user prompts ─────────────────────────────────────
    let selectedAgent;
    function updateAgentStatus(ctx) {
        ctx.ui.setStatus("agent", selectedAgent ? `agent: ${selectedAgent}` : undefined);
    }
    // /agent command — select an agent or clear selection
    pi.registerCommand("agent", {
        description: "Select an agent to handle your next prompts, or clear selection. Usage: /agent [name]",
        handler: async (args, ctx) => {
            const agents = discoverAgents(ctx.cwd, "both");
            if (args.trim()) {
                // Direct selection: /agent architect
                const name = args.trim();
                if (name === "none" || name === "off" || name === "clear") {
                    selectedAgent = undefined;
                    updateAgentStatus(ctx);
                    ctx.ui.notify("Agent cleared — prompts go to default model", "info");
                    return;
                }
                const agent = agents.find((a) => a.name === name);
                if (!agent) {
                    const available = agents.map((a) => a.name).join(", ") || "none";
                    ctx.ui.notify(`Unknown agent "${name}". Available: ${available}`, "warning");
                    return;
                }
                selectedAgent = name;
                updateAgentStatus(ctx);
                ctx.ui.notify(`Agent set to "${name}" — your prompts will be handled by this agent`, "info");
                return;
            }
            // Interactive selection
            if (!ctx.hasUI) {
                ctx.ui.notify("Usage: /agent <name> or /agent none", "warning");
                return;
            }
            const options = ["(none — default model)", ...agents.map((a) => `${a.name} — ${a.description}`)];
            const choice = await ctx.ui.select("Select agent for your prompts", options);
            if (choice === undefined)
                return; // cancelled
            if (choice === options[0]) {
                selectedAgent = undefined;
                updateAgentStatus(ctx);
                ctx.ui.notify("Agent cleared", "info");
            }
            else {
                const name = choice.split(" — ")[0];
                selectedAgent = name;
                updateAgentStatus(ctx);
                ctx.ui.notify(`Agent set to "${name}"`, "info");
            }
        },
        getArgumentCompletions: (partial) => {
            const agents = discoverAgents(process.cwd(), "both");
            const names = ["none", ...agents.map((a) => a.name)];
            return names.filter((n) => n.startsWith(partial)).map((n) => ({ value: n, label: n }));
        },
    });
    // Intercept user input when an agent is selected
    pi.on("input", (event) => {
        if (!selectedAgent)
            return { action: "continue" };
        // Don't intercept slash commands
        if (event.text.startsWith("/") || event.text.startsWith("!"))
            return { action: "continue" };
        const wrapped = `Use the subagent tool to delegate to the "${selectedAgent}" agent with this task:\n\n"${event.text}"\n\nSet agentScope to "both".`;
        return { action: "transform", text: wrapped, images: event.images };
    });
}
//# sourceMappingURL=subagent.js.map