var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
    if (typeof path === "string" && /^\.\.?\//.test(path)) {
        return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
            return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
        });
    }
    return path;
};
import { createBashToolDefinition, } from '@mariozechner/pi-coding-agent';
import { Type } from '@sinclair/typebox';
import { createRequire } from 'node:module';
import { HubClient } from "./client.js";
import { processActions } from "./handlers.js";
import { getToolRenderers } from "./renderers.js";
import { setupCoderFooter } from "./footer.js";
import { setupTitlebar } from "./titlebar.js";
import { registerAgentCommands } from "./commands.js";
import { AgentManagerOverlay } from "./overlay.js";
import { ChainEditorOverlay } from "./chain-preview.js";
import { HubOverlay } from "./hub-overlay.js";
import { OutputViewerOverlay } from "./output-viewer.js";
import { setNativeRemoteExtensionContext } from "./native-remote-ui-context.js";
import { handleRemoteUiRequest } from "./remote-ui-handler.js";
import { buildInboundRpcPromptText, getInboundRpcDeliverAs } from "./inbound-rpc.js";
import { applyCoderAuthHeaders, getCoderAuthCurlArgs } from "./auth.js";
import { formatToolDisplay } from "./agentuity-cli.js";
import { adaptInitMessageForLocalTui } from "./local-init-filter.js";
import { selectSubAgentToolNames } from "./subagent-tool-selection.js";
import { setupRemoteMode, } from "./remote-session.js";
// ESM doesn't have require() — create one for synchronous child_process access
const _require = createRequire(import.meta.url);
const HUB_URL_ENV = 'AGENTUITY_CODER_HUB_URL';
const AGENT_ENV = 'AGENTUITY_CODER_AGENT';
const REMOTE_SESSION_ENV = 'AGENTUITY_CODER_REMOTE_SESSION';
const NATIVE_REMOTE_ENV = 'AGENTUITY_CODER_NATIVE_REMOTE';
// Populated by `agentuity coder start` for hub bootstrap and runtime auth.
const API_KEY_ENV = 'AGENTUITY_CODER_API_KEY';
const RECONNECT_WAIT_TIMEOUT_MS = 120_000;
// Recent agent results for full-screen viewer (Ctrl+Shift+V / Alt+Shift+V)
const recentResults = [];
const MAX_STORED_RESULTS = 20;
function startStreamingResult(agentName, description, prompt) {
    const result = {
        agentName,
        text: '',
        thinking: '',
        timestamp: Date.now(),
        isStreaming: true,
        description,
        prompt,
    };
    recentResults.unshift(result);
    if (recentResults.length > MAX_STORED_RESULTS)
        recentResults.pop();
    return result;
}
// ══════════════════════════════════════════════
// Sub-Agent Output Limits (prevents context bloat in parent)
// Inspired by pi-subagents (200KB/5K lines) and oh-my-pi (500KB/5K lines)
// ══════════════════════════════════════════════
const MAX_OUTPUT_BYTES = 200_000;
const MAX_OUTPUT_LINES = 5_000;
// All Pi events we subscribe to
const PROXY_EVENTS = [
    'session_shutdown',
    'session_before_switch',
    'session_before_fork',
    'session_before_compact',
    'session_compact',
    'before_agent_start',
    'agent_start',
    'agent_end',
    'turn_start',
    'turn_end',
    'tool_call',
    'tool_result',
    'tool_execution_start',
    'tool_execution_update',
    'tool_execution_end',
    'message_start',
    'message_update',
    'message_end',
    'input',
    'model_select',
    'context',
];
const DEBUG = !!process.env['AGENTUITY_DEBUG'];
function log(msg) {
    if (DEBUG)
        console.error(`[agentuity-coder] ${msg}`);
}
/** Build headers object with API key if available. Merges with any existing headers. */
function authHeaders(extra) {
    const apiKey = process.env[API_KEY_ENV];
    const orgId = process.env.AGENTUITY_ORGID;
    const headers = { ...extra };
    return applyCoderAuthHeaders(headers, apiKey, orgId);
}
// ══════════════════════════════════════════════
// Synchronous Bootstrap — fetch InitMessage from Hub REST endpoint
// This runs BEFORE tool registration so we know what tools/agents
// the server actually provides. No hardcoded schemas.
// ══════════════════════════════════════════════
function buildInitUrl(hubUrl, agentRole) {
    let httpUrl = hubUrl.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://');
    if (httpUrl.includes('/api/ws')) {
        httpUrl = httpUrl.replace('/api/ws', '/api/hub/init');
    }
    else if (/\/ws\b/.test(httpUrl)) {
        httpUrl = httpUrl.replace(/\/ws\b/, '/api/hub/init');
    }
    else {
        httpUrl = httpUrl.replace(/\/?$/, '/api/hub/init');
    }
    if (agentRole && agentRole !== 'lead') {
        httpUrl += `?agent=${encodeURIComponent(agentRole)}`;
    }
    return httpUrl;
}
function getHubHttpBaseUrl(hubUrl) {
    let httpUrl = hubUrl.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://');
    httpUrl = httpUrl.replace(/\/api\/ws\b.*$/, '');
    httpUrl = httpUrl.replace(/\/ws\b.*$/, '');
    return httpUrl.replace(/\/+$/, '');
}
/**
 * Synchronously fetch the InitMessage from Hub's REST endpoint.
 *
 * Uses `curl` via `execFileSync` because Pi's extension registration is synchronous —
 * we need tools/agents BEFORE the extension returns. Node's `fetch()` is async-only,
 * and `Bun.spawnSync` isn't available in Pi's Node.js runtime.
 *
 * Requires `curl` binary (available on macOS, Linux, Windows 10+).
 */
function fetchInitMessageSync(hubUrl, agentRole) {
    const httpUrl = buildInitUrl(hubUrl, agentRole);
    try {
        const { execFileSync } = _require('node:child_process');
        const apiKey = process.env[API_KEY_ENV];
        const orgId = process.env.AGENTUITY_ORGID;
        const curlArgs = ['-s', '--connect-timeout', '3', '--max-time', '5'];
        curlArgs.push(...getCoderAuthCurlArgs(apiKey, orgId));
        curlArgs.push(httpUrl);
        const result = execFileSync('curl', curlArgs, { encoding: 'utf-8' });
        const parsed = JSON.parse(result);
        if (parsed && parsed.type === 'init') {
            return parsed;
        }
        return null;
    }
    catch {
        return null;
    }
}
/**
 * Fetch session snapshot from Hub REST endpoint.
 * Extracts observer count and session label for the footer display.
 * Best-effort, non-blocking — failures are silently ignored.
 */
async function fetchSessionSnapshot(hubUrl, sessionId, observerState) {
    const baseUrl = getHubHttpBaseUrl(hubUrl);
    const httpUrl = sessionId
        ? `${baseUrl}/api/hub/session/${encodeURIComponent(sessionId)}`
        : `${baseUrl}/api/hub/sessions`;
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5_000);
    try {
        const response = await fetch(httpUrl, {
            signal: controller.signal,
            headers: authHeaders({ accept: 'application/json' }),
        });
        if (!response.ok)
            return;
        if (sessionId) {
            const snapshot = (await response.json());
            if (observerState) {
                if (snapshot.label)
                    observerState.label = snapshot.label;
                if (Array.isArray(snapshot.participants)) {
                    observerState.count = snapshot.participants.filter((p) => p.role === 'observer').length;
                }
            }
            return;
        }
        const data = (await response.json());
        const first = data.sessions?.websocket?.[0];
        if (first && observerState) {
            if (first.label)
                observerState.label = first.label;
            if (typeof first.observerCount === 'number')
                observerState.count = first.observerCount;
        }
    }
    catch {
        // Ignore — best effort
    }
    finally {
        clearTimeout(timeout);
    }
}
async function fetchInitMessage(hubUrl, agentRole) {
    const httpUrl = buildInitUrl(hubUrl, agentRole);
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5_000);
    try {
        const response = await fetch(httpUrl, {
            signal: controller.signal,
            headers: authHeaders({ accept: 'application/json' }),
        });
        if (!response.ok)
            return null;
        const parsed = (await response.json());
        if (parsed.type === 'init') {
            return parsed;
        }
        return null;
    }
    catch {
        return null;
    }
    finally {
        clearTimeout(timeout);
    }
}
export function agentuityCoderHub(pi) {
    const hubUrl = process.env[HUB_URL_ENV];
    if (!hubUrl)
        return;
    // ── Remote mode detection ──
    // If AGENTUITY_CODER_REMOTE_SESSION is set, the TUI connects as a controller
    // to an existing sandbox session. The full UI is set up (tools, commands, /hub)
    // but user input is relayed to the remote sandbox instead of the local Pi agent.
    const remoteSessionId = process.env[REMOTE_SESSION_ENV] || null;
    const isRemoteSession = Boolean(remoteSessionId);
    const isNativeRemote = !!process.env[NATIVE_REMOTE_ENV];
    if (remoteSessionId) {
        log(`Remote mode: will connect as controller to session ${remoteSessionId}`);
    }
    const isSubAgent = !!process.env[AGENT_ENV];
    const agentRole = process.env[AGENT_ENV] || 'lead';
    log(`Hub URL: ${hubUrl} (role: ${agentRole})`);
    // ══════════════════════════════════════════════
    // Fetch InitMessage from Hub REST endpoint (synchronous)
    // This is how we discover what tools/agents the server provides.
    // ══════════════════════════════════════════════
    const initialInitMsg = fetchInitMessageSync(hubUrl, agentRole);
    const initMsg = initialInitMsg
        ? adaptInitMessageForLocalTui(initialInitMsg, { isRemoteSession })
        : null;
    if (!initMsg) {
        log('Hub not reachable — no tools or agents registered');
        log('Make sure the Hub server is running');
        return;
    }
    const serverTools = initMsg.tools || [];
    const serverAgents = initMsg.agents || [];
    let hubConfig = initMsg.config;
    const openChainEditor = async (ctx, initialAgents = []) => {
        if (!ctx.hasUI)
            return;
        const result = await ctx.ui.custom((_tui, theme, _keybindings, done) => new ChainEditorOverlay(theme, serverAgents, done, initialAgents), {
            overlay: true,
            overlayOptions: { width: '95%', maxHeight: '95%', anchor: 'center', margin: 1 },
        });
        if (!result || result.steps.length === 0)
            return;
        const instructions = result.steps
            .map((step, index) => `${index + 1}) @${step.agent}: ${step.task || '(no task provided)'}`)
            .join(', ');
        const message = result.mode === 'parallel'
            ? `@lead Execute these tasks in parallel: ${instructions}`
            : `@lead Execute this plan in order: ${instructions}`;
        pi.sendUserMessage(message, { deliverAs: 'followUp' });
    };
    const openAgentManager = async (ctx) => {
        if (!ctx.hasUI)
            return;
        const result = await ctx.ui.custom((_tui, theme, _keybindings, done) => new AgentManagerOverlay(theme, serverAgents, done), {
            overlay: true,
            overlayOptions: { width: '95%', maxHeight: '95%', anchor: 'center', margin: 1 },
        });
        // TODO: chain action from Agent Manager overlay (multi-select + Ctrl+R) not yet implemented
        if (result?.action === 'chain' && Array.isArray(result.agents)) {
            await openChainEditor(ctx, result.agents);
            return;
        }
        if (result?.action === 'run' && result.agent) {
            const task = await ctx.ui.input(`Task for ${result.agent}`, 'What should this agent do?');
            const trimmed = task?.trim();
            if (trimmed) {
                pi.sendUserMessage(`@${result.agent} ${trimmed}`, { deliverAs: 'followUp' });
            }
        }
    };
    const openHubOverlay = async (ctx, activeSessionId, detailSessionId) => {
        if (!ctx.hasUI)
            return;
        if (hubOverlayOpen)
            return;
        hubOverlayOpen = true;
        try {
            await ctx.ui.custom((tui, theme, _keybindings, done) => new HubOverlay(tui, theme, {
                baseUrl: getHubHttpBaseUrl(hubUrl),
                currentSessionId: activeSessionId ?? undefined,
                initialSessionId: detailSessionId ?? undefined,
                startInDetail: !!detailSessionId,
                done,
            }), {
                overlay: true,
                overlayOptions: { width: '95%', maxHeight: '95%', anchor: 'center', margin: 1 },
            });
        }
        finally {
            hubOverlayOpen = false;
        }
    };
    const buildActionContext = (ctx) => ({
        ui: ctx.hasUI ? ctx.ui : undefined,
        sendUserMessage: (message, options) => {
            pi.sendUserMessage(message, { deliverAs: options?.deliverAs ?? 'followUp' });
        },
    });
    log(`Hub connected. Tools: ${serverTools.length}, Agents: ${serverAgents.length}`);
    // Titlebar: branding + spinner (registers its own event handlers)
    setupTitlebar(pi);
    // Override Pi's built-in bash call-row rendering so local transcript rows
    // can brand Agentuity CLI invocations without changing bash execution/result behavior.
    const hasBashTool = serverTools.some((tool) => tool.name === 'bash');
    const localBashRenderers = hasBashTool ? getToolRenderers('bash') : undefined;
    if (hasBashTool && localBashRenderers?.renderCall) {
        const bashToolDefinition = createBashToolDefinition(process.cwd());
        pi.registerTool({
            ...bashToolDefinition,
            renderCall: localBashRenderers.renderCall,
        });
    }
    // ══════════════════════════════════════════════
    // WebSocket client for runtime communication (tool execution + events)
    // ══════════════════════════════════════════════
    const client = new HubClient();
    client.apiKey = process.env[API_KEY_ENV] || null;
    let cachedInitMessage = initMsg;
    let currentSessionId = initMsg.sessionId ?? null;
    let systemPromptApplied = false;
    let connectPromise = null;
    // In native remote mode, remote-tui.ts owns the Hub connection — show as connected
    let hubUiStatus = process.env[NATIVE_REMOTE_ENV] ? 'connected' : 'offline';
    let footerCtx = null;
    let hubOverlayOpen = false;
    // Observer awareness state — tracks who's watching this session.
    // Updated via broadcast events from the Hub (session_join, session_leave).
    const observerState = { count: 0, label: '' };
    const observerParticipantIds = new Set();
    function getHubUiStatus() {
        return hubUiStatus;
    }
    function getObserverState() {
        return observerState;
    }
    function mapConnectionStateToUiStatus(state) {
        if (state === 'connected')
            return 'connected';
        if (state === 'reconnecting')
            return 'reconnecting';
        return 'offline';
    }
    function updateHubUiStatus(state) {
        hubUiStatus = mapConnectionStateToUiStatus(state);
        if (footerCtx?.hasUI) {
            footerCtx.ui.setStatus('hub_connection', hubUiStatus);
        }
    }
    function applyInitMessage(nextInit) {
        const effectiveInit = adaptInitMessageForLocalTui(nextInit, { isRemoteSession });
        cachedInitMessage = effectiveInit;
        if (effectiveInit.sessionId)
            currentSessionId = effectiveInit.sessionId;
        if (effectiveInit.config)
            hubConfig = effectiveInit.config;
    }
    client.onInitMessage = (nextInit) => {
        applyInitMessage(nextInit);
    };
    client.onConnectionStateChange = (state) => {
        updateHubUiStatus(state);
        log(`Hub connection state: ${state}`);
    };
    client.onBeforeReconnect = async () => {
        const refreshedInit = await fetchInitMessage(hubUrl, agentRole);
        if (refreshedInit) {
            applyInitMessage(refreshedInit);
            log('Refreshed Hub init payload before reconnect');
        }
    };
    function handleInboundRpcCommand(message) {
        const command = message.command;
        if (!command) {
            log('Ignoring inbound rpc_command without command payload');
            return;
        }
        const commandType = typeof command.type === 'string' ? command.type : '';
        if (commandType === 'abort') {
            if (!footerCtx) {
                log('Ignoring inbound abort before session context is ready');
                return;
            }
            footerCtx.abort();
            log('Handled inbound rpc abort');
            return;
        }
        if (commandType !== 'prompt' && commandType !== 'follow_up' && commandType !== 'steer') {
            log(`Ignoring unsupported inbound rpc command: ${commandType || 'unknown'}`);
            return;
        }
        const promptText = buildInboundRpcPromptText(command);
        if (!promptText) {
            log(`Ignoring empty inbound rpc ${commandType}`);
            return;
        }
        if (Array.isArray(command.attachments) && command.attachments.length > 0) {
            log(`Inbound rpc ${commandType} included attachments; native TUI forwarding text only`);
        }
        const deliverAs = getInboundRpcDeliverAs(commandType, footerCtx?.isIdle() ?? true);
        try {
            if (deliverAs) {
                pi.sendUserMessage(promptText, { deliverAs });
            }
            else {
                pi.sendUserMessage(promptText);
            }
            log(`Handled inbound rpc ${commandType}`);
            return;
        }
        catch (err) {
            if (!deliverAs && commandType === 'prompt') {
                try {
                    pi.sendUserMessage(promptText, { deliverAs: 'followUp' });
                    log('Handled inbound rpc prompt as follow-up after busy-state fallback');
                    return;
                }
                catch (fallbackErr) {
                    log(`Inbound rpc prompt fallback failed: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}`);
                }
            }
            log(`Inbound rpc ${commandType} failed: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    // Handle unsolicited server messages (broadcast, presence)
    // Updates observer state for footer display
    client.onServerMessage = (message) => {
        const msgType = message.type;
        if (msgType === 'rpc_command') {
            if (!remoteSessionId && !isSubAgent) {
                handleInboundRpcCommand(message);
            }
            return;
        }
        if (msgType === 'rpc_command_error') {
            log(`Hub reported rpc_command_error: ${typeof message.error === 'string' ? message.error : 'unknown error'}`);
            return;
        }
        if (msgType === 'broadcast') {
            const event = message.event;
            if (event === 'session_join') {
                const participant = message.data?.participant;
                if (participant?.role === 'observer' && typeof participant.id === 'string') {
                    observerParticipantIds.add(participant.id);
                    observerState.count = observerParticipantIds.size;
                    log(`Observer joined: ${observerState.count} observers`);
                }
            }
            else if (event === 'session_label_updated') {
                const label = message.data?.label;
                if (label) {
                    observerState.label = label;
                    log(`Session label updated: ${label}`);
                }
            }
            else if (event === 'session_leave') {
                const participant = message.data?.participant;
                if (participant?.role === 'observer' && typeof participant.id === 'string') {
                    observerParticipantIds.delete(participant.id);
                    observerState.count = observerParticipantIds.size;
                    log(`Observer left: ${observerState.count} observers`);
                }
            }
        }
        else if (msgType === 'presence') {
            // Full presence update — may include participant list
            const participants = message.participants;
            if (participants) {
                observerParticipantIds.clear();
                for (const participant of participants) {
                    if (participant.role === 'observer' && typeof participant.id === 'string') {
                        observerParticipantIds.add(participant.id);
                    }
                }
                observerState.count = observerParticipantIds.size;
                log(`Presence update: ${observerState.count} observers`);
            }
        }
    };
    // Lazy WebSocket connect — returns cached InitMessage
    // In native remote mode, remote-tui.ts owns the controller WebSocket via RemoteSession.
    // Skip the extension's own HubClient connection to avoid duplicate controllers.
    function ensureConnected() {
        if (isNativeRemote) {
            log('Native remote mode — skipping HubClient WebSocket (remote-tui owns controller)');
            return Promise.resolve(cachedInitMessage);
        }
        if (client.connected && cachedInitMessage)
            return Promise.resolve(cachedInitMessage);
        if (client.connectionState === 'reconnecting' || client.connectionState === 'disconnected') {
            return client
                .waitUntilConnected(RECONNECT_WAIT_TIMEOUT_MS)
                .then(() => cachedInitMessage)
                .catch(() => null);
        }
        if (connectPromise)
            return connectPromise;
        connectPromise = (async () => {
            log('Connecting WebSocket to Hub...');
            try {
                // In remote mode, connect as a controller to the existing session
                const connectOpts = remoteSessionId
                    ? { sessionId: remoteSessionId, role: 'controller' }
                    : { origin: 'tui' };
                const wsInitMsg = await client.connect(hubUrl, connectOpts);
                log('WebSocket connected');
                applyInitMessage(wsInitMsg);
                connectPromise = null; // Clear so future disconnects can reconnect
                return wsInitMsg;
            }
            catch (err) {
                log(`WebSocket failed: ${err instanceof Error ? err.message : String(err)}`);
                connectPromise = null;
                return null;
            }
        })();
        return connectPromise;
    }
    // ══════════════════════════════════════════════
    // Register Hub tools from server's InitMessage
    // Tools come from the server — NOT hardcoded in the extension.
    // ══════════════════════════════════════════════
    for (const toolDef of serverTools) {
        log(`Registering tool: ${toolDef.name}`);
        const renderers = getToolRenderers(toolDef.name);
        pi.registerTool({
            name: toolDef.name,
            label: toolDef.label || toolDef.name,
            description: toolDef.description,
            // Server sends JSON Schema; TypeBox schemas are JSON Schema at runtime
            parameters: toolDef.parameters,
            ...(toolDef.promptSnippet ? { promptSnippet: toolDef.promptSnippet } : {}),
            ...(toolDef.promptGuidelines
                ? {
                    promptGuidelines: Array.isArray(toolDef.promptGuidelines)
                        ? toolDef.promptGuidelines
                        : [toolDef.promptGuidelines],
                }
                : {}),
            async execute(toolCallId, params, _signal, _onUpdate, ctx) {
                // Ensure WebSocket is connected before executing
                await ensureConnected();
                if (!client.connected) {
                    return {
                        content: [{ type: 'text', text: 'Error: Hub WebSocket not connected' }],
                        details: undefined,
                    };
                }
                const id = client.nextId();
                let response;
                try {
                    response = await client.send({
                        id,
                        type: 'tool',
                        name: toolDef.name,
                        toolCallId,
                        params: (params ?? {}),
                    });
                }
                catch {
                    return {
                        content: [{ type: 'text', text: 'Error: Hub connection lost' }],
                        details: undefined,
                    };
                }
                // Process ALL Hub actions (NOTIFY, STATUS, RETURN, etc.)
                const result = await processActions(response.actions, buildActionContext(ctx));
                // If there's a return value from processActions, use it
                if (result.returnValue !== undefined) {
                    const text = typeof result.returnValue === 'string'
                        ? result.returnValue
                        : JSON.stringify(result.returnValue, null, 2);
                    return {
                        content: [{ type: 'text', text }],
                        details: undefined,
                    };
                }
                // Fallback — check for RETURN action directly (backward compat)
                const returnAction = response.actions.find((a) => a.action === 'RETURN');
                if (returnAction && 'result' in returnAction) {
                    const text = typeof returnAction.result === 'string'
                        ? returnAction.result
                        : JSON.stringify(returnAction.result, null, 2);
                    return {
                        content: [{ type: 'text', text }],
                        details: undefined,
                    };
                }
                return {
                    content: [{ type: 'text', text: 'Done' }],
                    details: undefined,
                };
            },
            // TUI renderers — optional, only for known Hub tools.
            // Cast needed: SimpleText satisfies Component, but TS can't verify cross-package structural match.
            ...(renderers?.renderCall && {
                renderCall: renderers.renderCall,
            }),
            ...(renderers?.renderResult && {
                renderResult: renderers.renderResult,
            }),
        });
    }
    // ══════════════════════════════════════════════
    // Register task tools (LEAD only) from server's agent list
    // Agent names and configs come from the Hub, not hardcoded.
    // ══════════════════════════════════════════════
    if (!isSubAgent && serverAgents.length > 0) {
        pi.registerShortcut('ctrl+shift+a', {
            description: 'Open Agent Manager',
            handler: async (ctx) => {
                await openAgentManager(ctx);
            },
        });
        const openOutputViewer = async (ctx) => {
            if (!ctx.hasUI || recentResults.length === 0)
                return;
            await ctx.ui.custom((tui, theme, _keybindings, done) => new OutputViewerOverlay(tui, theme, recentResults, done), {
                overlay: true,
                overlayOptions: { width: '95%', maxHeight: '95%', anchor: 'center', margin: 1 },
            });
        };
        pi.registerShortcut('ctrl+shift+v', {
            description: 'View full agent output',
            handler: openOutputViewer,
        });
        // Tmux/terminal environments often cannot emit Ctrl+Shift+V consistently.
        pi.registerShortcut('alt+shift+v', {
            description: 'View full agent output',
            handler: openOutputViewer,
        });
        pi.registerShortcut('ctrl+shift+c', {
            description: 'Open Chain Editor',
            handler: async (ctx) => {
                await openChainEditor(ctx);
            },
        });
        pi.registerShortcut('ctrl+h', {
            description: 'Open Hub overlay',
            handler: async (ctx) => {
                if (!ctx.hasUI)
                    return;
                await openHubOverlay(ctx, currentSessionId);
            },
        });
        const agentRegistry = new Map(serverAgents.map((a) => [a.name, a]));
        const agentNames = serverAgents.map((a) => a.name);
        log(`Registering task tools. Agents: ${agentNames.join(', ')}`);
        const taskRenderers = getToolRenderers('task');
        pi.registerTool({
            name: 'task',
            label: 'Delegate Task to Agent',
            description: `Delegate a task to a specialized agent on your team. ` +
                `Available agents: ${agentNames.join(', ')}. ` +
                `Each agent runs independently with its own context window.`,
            promptSnippet: 'Use task({ description, prompt, subagent_type }) to delegate one focused sub-task to a specialist agent.',
            parameters: Type.Object({
                description: Type.String({ description: 'Short 3-5 word task description' }),
                prompt: Type.String({ description: 'Detailed task instructions for the agent' }),
                subagent_type: Type.String({
                    description: `Agent: ${agentNames.join(', ')}`,
                }),
            }),
            async execute(toolCallId, params, signal, _onUpdate, ctx) {
                const { description, prompt, subagent_type } = params;
                if (signal?.aborted) {
                    return {
                        content: [{ type: 'text', text: 'Cancelled' }],
                        details: undefined,
                    };
                }
                const agent = agentRegistry.get(subagent_type);
                if (!agent) {
                    return {
                        content: [
                            {
                                type: 'text',
                                text: `Unknown agent: ${subagent_type}. Available: ${agentNames.join(', ')}`,
                            },
                        ],
                        details: undefined,
                    };
                }
                log(`Task: ${description} → ${subagent_type}`);
                const startTime = Date.now();
                const formatElapsed = () => {
                    const s = Math.floor((Date.now() - startTime) / 1000);
                    if (s < 60)
                        return `${s}s`;
                    return `${Math.floor(s / 60)}m ${s % 60}s`;
                };
                let elapsedTimer = null;
                // ── Single-agent status via working message ──
                let lastWidgetTool;
                let lastWidgetToolArgs;
                function updateWidget(status, tool, toolArgs) {
                    if (!ctx.hasUI)
                        return;
                    let msg = '';
                    if (status === 'running') {
                        msg = '\u25CF ' + subagent_type; // ● name
                        if (tool) {
                            const toolInfo = toolArgs ? `${tool} ${toolArgs}` : tool;
                            msg += '  ' + toolInfo.slice(0, 40);
                        }
                        msg += '  ' + formatElapsed();
                    }
                    else if (status === 'completed') {
                        msg = '\u2713 ' + subagent_type + '  ' + formatElapsed(); // ✓ name Xs
                    }
                    else if (status === 'failed') {
                        msg = '\u2717 ' + subagent_type + '  failed'; // ✗ name failed
                    }
                    ctx.ui.setWorkingMessage(msg);
                }
                if (ctx.hasUI) {
                    ctx.ui.setStatus('active_agent', subagent_type);
                    updateWidget('running');
                    elapsedTimer = setInterval(() => {
                        updateWidget('running', lastWidgetTool, lastWidgetToolArgs);
                    }, 1000);
                }
                // Create live streaming result before starting sub-agent
                const liveResult = startStreamingResult(subagent_type, description, prompt);
                sendEventNoWait('task_start', {
                    taskId: toolCallId,
                    agent: subagent_type,
                    prompt,
                    description,
                });
                try {
                    const result = await runSubAgent(agent, prompt, client, ctx.hasUI
                        ? (progress) => {
                            // Update TUI working message with live tool activity
                            try {
                                if (progress.status === 'thinking_delta' && progress.delta) {
                                    liveResult.thinking += progress.delta;
                                    updateWidget('running', 'thinking...');
                                }
                                else if (progress.status === 'text_delta' && progress.delta) {
                                    liveResult.text += progress.delta;
                                    updateWidget('running', 'writing...');
                                }
                                else if (progress.status === 'tool_start' && progress.currentTool) {
                                    lastWidgetTool = progress.currentTool;
                                    lastWidgetToolArgs = progress.currentToolArgs;
                                    updateWidget('running', progress.currentTool, progress.currentToolArgs);
                                }
                            }
                            catch {
                                // Best-effort live widget updates.
                            }
                            // Forward progress to Hub (fire-and-forget, queued while disconnected)
                            sendEventNoWait('agent_progress', {
                                agentName: progress.agentName,
                                status: progress.status,
                                taskId: toolCallId,
                                delta: progress.delta,
                                currentTool: progress.currentTool,
                                currentToolArgs: progress.currentToolArgs,
                                elapsed: progress.elapsed,
                            });
                        }
                        : undefined, signal);
                    // Flash completed state briefly before clearing
                    updateWidget('completed');
                    // Finalize the live result instead of creating a new one
                    liveResult.isStreaming = false;
                    liveResult.text = result.output || liveResult.text || '(no output)';
                    await sendEvent('task_complete', {
                        taskId: toolCallId,
                        agent: subagent_type,
                        duration: result.duration,
                        result: result.output.slice(0, 10000),
                    }, ctx);
                    let output = result.output;
                    let tokenInfoStr;
                    if (result.tokens && (result.tokens.input > 0 || result.tokens.output > 0)) {
                        tokenInfoStr = `${subagent_type}: ${result.duration}ms | ${result.tokens.input} in ${result.tokens.output} out | $${result.tokens.cost.toFixed(4)}`;
                        output += `\n\n---\n_${subagent_type}: ${result.duration}ms | ${result.tokens.input} in ${result.tokens.output} out tokens | $${result.tokens.cost.toFixed(4)}_`;
                    }
                    if (tokenInfoStr)
                        liveResult.tokenInfo = tokenInfoStr;
                    return {
                        content: [{ type: 'text', text: output }],
                        details: undefined,
                    };
                }
                catch (err) {
                    const errorMsg = err instanceof Error ? err.message : String(err);
                    liveResult.isStreaming = false;
                    liveResult.text = liveResult.text || `Agent ${subagent_type} failed: ${errorMsg}`;
                    await sendEvent('task_error', {
                        taskId: toolCallId,
                        agent: subagent_type,
                        error: errorMsg,
                    }, ctx);
                    updateWidget('failed');
                    return {
                        content: [
                            { type: 'text', text: `Agent ${subagent_type} failed: ${errorMsg}` },
                        ],
                        details: undefined,
                    };
                }
                finally {
                    if (elapsedTimer)
                        clearInterval(elapsedTimer);
                    if (ctx.hasUI) {
                        ctx.ui.setStatus('active_agent', undefined);
                        ctx.ui.setWorkingMessage(); // Restore Pi's default working message
                    }
                }
            },
            ...(taskRenderers?.renderCall && {
                renderCall: taskRenderers.renderCall,
            }),
            ...(taskRenderers?.renderResult && {
                renderResult: taskRenderers.renderResult,
            }),
        });
        const parallelRenderers = getToolRenderers('parallel_tasks');
        pi.registerTool({
            name: 'parallel_tasks',
            label: 'Delegate Parallel Tasks',
            description: `Run multiple agent tasks concurrently (max 4). ` +
                `Available agents: ${agentNames.join(', ')}.`,
            promptSnippet: 'Use parallel_tasks({ tasks: [...] }) when multiple independent specialist tasks can run at the same time.',
            parameters: Type.Object({
                tasks: Type.Array(Type.Object({
                    description: Type.String({ description: 'Short task description' }),
                    prompt: Type.String({ description: 'Detailed instructions' }),
                    subagent_type: Type.String({ description: 'Agent to delegate to' }),
                }), { maxItems: 4 }),
            }),
            async execute(toolCallId, params, signal, _onUpdate, ctx) {
                const { tasks } = params;
                if (signal?.aborted) {
                    return {
                        content: [{ type: 'text', text: 'Cancelled' }],
                        details: undefined,
                    };
                }
                if (tasks.length > 4) {
                    return {
                        content: [{ type: 'text', text: 'Maximum 4 concurrent tasks allowed.' }],
                        details: undefined,
                    };
                }
                log(`Parallel tasks: ${tasks.map((t) => `${t.subagent_type}:${t.description}`).join(', ')}`);
                let elapsedTimer = null;
                const agentStatuses = tasks.map((t) => ({
                    name: t.subagent_type,
                    status: 'pending',
                }));
                function updateWidget() {
                    if (!ctx.hasUI)
                        return;
                    const parts = agentStatuses
                        .filter((s) => s.status !== 'pending')
                        .map((s) => {
                        const elapsed = s.startTime
                            ? Math.floor((Date.now() - s.startTime) / 1000)
                            : 0;
                        const timeStr = elapsed < 60
                            ? `${elapsed}s`
                            : `${Math.floor(elapsed / 60)}m${elapsed % 60}s`;
                        if (s.status === 'running') {
                            let info = `\u25CF ${s.name}`;
                            if (s.currentTool)
                                info += ` ${s.currentTool.slice(0, 15)}`;
                            return info + ` ${timeStr}`;
                        }
                        if (s.status === 'completed') {
                            return `\u2713 ${s.name} ${timeStr}`;
                        }
                        if (s.status === 'failed') {
                            return `\u2717 ${s.name}`;
                        }
                        return `\u25CB ${s.name}`;
                    });
                    ctx.ui.setWorkingMessage(parts.join('  '));
                }
                if (ctx.hasUI) {
                    ctx.ui.setStatus('active_agent', 'agents');
                    updateWidget();
                    elapsedTimer = setInterval(() => {
                        updateWidget(); // Refresh elapsed times in widget
                    }, 1000);
                }
                // Create live streaming results for each parallel task
                const liveResults = tasks.map((task) => startStreamingResult(task.subagent_type, task.description, task.prompt));
                const promises = tasks.map(async (task, index) => {
                    const taskId = `${toolCallId}-${index}-${task.subagent_type}`;
                    const agent = agentRegistry.get(task.subagent_type);
                    if (!agent) {
                        agentStatuses[index].status = 'failed';
                        liveResults[index].isStreaming = false;
                        liveResults[index].text = `Unknown agent: ${task.subagent_type}`;
                        await sendEvent('task_error', {
                            taskId,
                            agent: task.subagent_type,
                            error: `Unknown agent: ${task.subagent_type}`,
                        }, ctx);
                        updateWidget();
                        return {
                            agent: task.subagent_type,
                            error: `Unknown agent: ${task.subagent_type}`,
                        };
                    }
                    agentStatuses[index].status = 'running';
                    agentStatuses[index].startTime = Date.now();
                    sendEventNoWait('task_start', {
                        taskId,
                        agent: task.subagent_type,
                        prompt: task.prompt,
                        description: task.description,
                    });
                    updateWidget();
                    try {
                        const result = await runSubAgent(agent, task.prompt, client, ctx.hasUI
                            ? (progress) => {
                                // Handle streaming deltas
                                if (progress.status === 'thinking_delta' && progress.delta) {
                                    liveResults[index].thinking += progress.delta;
                                }
                                else if (progress.status === 'text_delta' && progress.delta) {
                                    liveResults[index].text += progress.delta;
                                }
                                // Update per-agent widget with tool activity
                                agentStatuses[index].currentTool = progress.currentTool;
                                agentStatuses[index].currentToolArgs = progress.currentToolArgs;
                                updateWidget();
                                // Forward progress to Hub (fire-and-forget, queued while disconnected)
                                sendEventNoWait('agent_progress', {
                                    agentName: progress.agentName,
                                    status: progress.status,
                                    taskId,
                                    delta: progress.delta,
                                    currentTool: progress.currentTool,
                                    currentToolArgs: progress.currentToolArgs,
                                    elapsed: progress.elapsed,
                                });
                            }
                            : undefined, signal);
                        agentStatuses[index].status = 'completed';
                        agentStatuses[index].duration = result.duration;
                        agentStatuses[index].currentTool = undefined;
                        agentStatuses[index].currentToolArgs = undefined;
                        // Finalize the live result
                        liveResults[index].isStreaming = false;
                        liveResults[index].text =
                            result.output || liveResults[index].text || '(no output)';
                        await sendEvent('task_complete', {
                            taskId,
                            agent: task.subagent_type,
                            duration: result.duration,
                            result: result.output.slice(0, 10000),
                        }, ctx);
                        updateWidget();
                        return {
                            agent: task.subagent_type,
                            output: result.output,
                            duration: result.duration,
                            tokens: result.tokens,
                        };
                    }
                    catch (err) {
                        const errorMsg = err instanceof Error ? err.message : String(err);
                        agentStatuses[index].status = 'failed';
                        agentStatuses[index].currentTool = undefined;
                        agentStatuses[index].currentToolArgs = undefined;
                        liveResults[index].isStreaming = false;
                        liveResults[index].text = liveResults[index].text || `Failed: ${errorMsg}`;
                        await sendEvent('task_error', {
                            taskId,
                            agent: task.subagent_type,
                            error: errorMsg,
                        }, ctx);
                        updateWidget();
                        return { agent: task.subagent_type, error: errorMsg };
                    }
                });
                try {
                    const results = await Promise.all(promises);
                    // Finalize live results with token info
                    results.forEach((r, idx) => {
                        if ('output' in r && r.output && !('error' in r && r.error)) {
                            if ('tokens' in r && r.tokens && (r.tokens.input > 0 || r.tokens.output > 0)) {
                                liveResults[idx].tokenInfo =
                                    `${r.agent}: ${'duration' in r ? r.duration : 0}ms | ${r.tokens.input} in ${r.tokens.output} out | $${r.tokens.cost.toFixed(4)}`;
                            }
                        }
                    });
                    const output = results
                        .map((r) => {
                        if ('error' in r && r.error)
                            return `### ${r.agent} (FAILED)\n${r.error}`;
                        let text = `### ${r.agent} (${'duration' in r ? r.duration : 0}ms)\n${'output' in r ? r.output : ''}`;
                        if ('tokens' in r && r.tokens && (r.tokens.input > 0 || r.tokens.output > 0)) {
                            text += `\n\n---\n_${r.agent}: ${'duration' in r ? r.duration : 0}ms | ${r.tokens.input} in ${r.tokens.output} out tokens | $${r.tokens.cost.toFixed(4)}_`;
                        }
                        return text;
                    })
                        .join('\n\n---\n\n');
                    return {
                        content: [{ type: 'text', text: output }],
                        details: undefined,
                    };
                }
                finally {
                    if (elapsedTimer)
                        clearInterval(elapsedTimer);
                    if (ctx.hasUI) {
                        ctx.ui.setStatus('active_agent', undefined);
                        ctx.ui.setWorkingMessage(); // Restore Pi's default working message
                    }
                }
            },
            ...(parallelRenderers?.renderCall && {
                renderCall: parallelRenderers.renderCall,
            }),
            ...(parallelRenderers?.renderResult && {
                renderResult: parallelRenderers.renderResult,
            }),
        });
    }
    log('Tool registration complete');
    // ══════════════════════════════════════════════
    // Register slash commands for agent routing (LEAD only)
    // When user types /memory, /scout, etc., the message is routed
    // to that specific agent via a routing prefix.
    // ══════════════════════════════════════════════
    if (!isSubAgent && serverAgents.length > 0) {
        registerAgentCommands(pi, serverAgents, getHubUiStatus, openAgentManager, openChainEditor);
    }
    // ══════════════════════════════════════════════
    // /hub command — Hub session overview (LEAD only)
    // ══════════════════════════════════════════════
    if (!isSubAgent) {
        pi.registerCommand('hub', {
            description: 'Open Coder overlay (sessions, detail, feed)',
            handler: async (_args, ctx) => {
                if (!ctx.hasUI)
                    return;
                await openHubOverlay(ctx, currentSessionId);
            },
        });
        pi.registerCommand('todos', {
            description: 'Open session todo board for current Hub session',
            handler: async (args, ctx) => {
                if (!ctx.hasUI)
                    return;
                const targetSessionId = args.trim().length > 0 ? args.trim() : currentSessionId;
                if (!targetSessionId) {
                    ctx.ui.notify('No active Hub session id available yet.', 'warning');
                    return;
                }
                await openHubOverlay(ctx, currentSessionId, targetSessionId);
            },
        });
        pi.registerCommand('rename-session', {
            description: 'Rename the current Hub session (max 30 chars)',
            handler: async (args, ctx) => {
                const label = args.trim().slice(0, 30);
                if (!label) {
                    if (ctx.hasUI)
                        ctx.ui.notify('Usage: /rename-session <label>', 'warning');
                    return;
                }
                if (!client.connected) {
                    if (ctx.hasUI)
                        ctx.ui.notify('Not connected to Hub', 'warning');
                    return;
                }
                try {
                    client.send({ type: 'rename_session', label });
                    observerState.label = label;
                    if (ctx.hasUI)
                        ctx.ui.notify(`Session renamed to "${label}"`, 'info');
                    log(`Session renamed to "${label}"`);
                }
                catch (err) {
                    if (ctx.hasUI)
                        ctx.ui.notify(`Failed to rename: ${err?.message || err}`, 'error');
                }
            },
        });
        pi.registerCommand('sync-hub-skills', {
            description: 'Sync skills from Coder to local .agents/skills/ directory',
            handler: async (_args, ctx) => {
                const baseUrl = getHubHttpBaseUrl(hubUrl);
                const url = `${baseUrl}/api/hub/skills`;
                try {
                    const resp = await fetch(url, { headers: authHeaders() });
                    if (!resp.ok) {
                        const msg = `Hub skills fetch failed: ${resp.status} ${resp.statusText}`;
                        if (ctx.hasUI)
                            ctx.ui.notify(msg, 'error');
                        return;
                    }
                    const data = (await resp.json());
                    if (!data.ok || !data.skills?.length) {
                        if (ctx.hasUI)
                            ctx.ui.notify('No skills available from Hub.', 'info');
                        return;
                    }
                    const { mkdirSync, writeFileSync } = _require('node:fs');
                    const { dirname, resolve, relative } = _require('node:path');
                    const cwd = process.cwd();
                    const synced = [];
                    for (const skill of data.skills) {
                        if (skill.path.includes('\0')) {
                            log(`Skipping skill with null byte in path: ${skill.path}`);
                            continue;
                        }
                        const fullPath = resolve(cwd, skill.path);
                        const rel = relative(cwd, fullPath);
                        if (rel.startsWith('..') || resolve(cwd, rel) !== fullPath) {
                            log(`Skipping skill with path traversal: ${skill.path}`);
                            continue;
                        }
                        mkdirSync(dirname(fullPath), { recursive: true });
                        writeFileSync(fullPath, skill.content, 'utf-8');
                        synced.push(skill.path);
                    }
                    const msg = `Synced ${synced.length} skill files:\n${synced.map((p) => `  ${p}`).join('\n')}`;
                    if (ctx.hasUI)
                        ctx.ui.notify(msg, 'info');
                    log(msg);
                }
                catch (err) {
                    const msg = `Failed to sync skills: ${err?.message || err}`;
                    if (ctx.hasUI)
                        ctx.ui.notify(msg, 'error');
                }
            },
        });
    }
    // ══════════════════════════════════════════════
    // Event Handlers
    // ══════════════════════════════════════════════
    function serializeEvent(event) {
        const data = {};
        if (event && typeof event === 'object') {
            for (const [key, value] of Object.entries(event)) {
                if (typeof value !== 'function' && key !== 'signal') {
                    try {
                        JSON.stringify(value);
                        data[key] = value;
                    }
                    catch {
                        /* skip */
                    }
                }
            }
        }
        return data;
    }
    async function sendEvent(eventName, data, ctx) {
        const id = client.nextId();
        try {
            const response = await client.send({
                id,
                type: 'event',
                event: eventName,
                data: { ...data, agentRole },
            });
            const result = await processActions(response.actions, buildActionContext(ctx));
            if (result.block)
                return result.block;
            if (result.returnValue !== undefined)
                return result.returnValue;
        }
        catch {
            /* ignore */
        }
        return undefined;
    }
    function sendEventNoWait(eventName, data) {
        client.sendNoWait({
            id: client.nextId(),
            type: 'event',
            event: eventName,
            data: { ...data, agentRole },
        });
    }
    const onEvent = pi.on.bind(pi);
    // session_start: establish WebSocket connection to Hub + set up footer
    onEvent('session_start', async (event, ctx) => {
        footerCtx = ctx;
        if (isNativeRemote && remoteSessionId) {
            setNativeRemoteExtensionContext(ctx);
        }
        await ensureConnected();
        if (ctx.hasUI) {
            ctx.ui.setStatus('hub_connection', getHubUiStatus());
        }
        // Set up Coder footer (powerline: model or active agent > branch > status + observer count)
        setupCoderFooter(ctx, getHubUiStatus, getObserverState);
        // Fire-and-forget: fetch session snapshot for label + initial observer count.
        // Uses the Hub REST endpoint — non-blocking, best-effort.
        if (!isSubAgent) {
            fetchSessionSnapshot(hubUrl, currentSessionId, observerState).catch(() => { });
        }
        return sendEvent('session_start', serializeEvent(event), ctx);
    });
    // before_agent_start: inject system prompt from Hub
    onEvent('before_agent_start', async (event, ctx) => {
        const eventData = event;
        let systemPrompt = eventData.systemPrompt || '';
        const id = client.nextId();
        try {
            const response = await client.send({
                id,
                type: 'event',
                event: 'before_agent_start',
                data: { ...serializeEvent(event), agentRole },
            });
            const result = await processActions(response.actions, buildActionContext(ctx));
            if (result.block)
                return result.block;
            if (result.systemPrompt) {
                const mode = result.systemPromptMode || 'suffix';
                if (mode === 'prefix') {
                    systemPrompt = result.systemPrompt + '\n\n' + systemPrompt;
                }
                else if (mode === 'suffix') {
                    systemPrompt = systemPrompt + '\n\n' + result.systemPrompt;
                }
                else {
                    systemPrompt = result.systemPrompt;
                }
            }
        }
        catch {
            /* ignore */
        }
        // Apply config prefix/suffix — LEAD ONLY
        if (!isSubAgent) {
            if (hubConfig?.systemPromptPrefix && !systemPromptApplied) {
                systemPrompt = hubConfig.systemPromptPrefix + '\n\n' + systemPrompt;
                systemPromptApplied = true;
            }
            if (hubConfig?.systemPromptSuffix) {
                systemPrompt = systemPrompt + '\n\n' + hubConfig.systemPromptSuffix;
            }
        }
        return { systemPrompt };
    });
    // Proxy all other events
    for (const eventName of PROXY_EVENTS) {
        if (eventName === 'before_agent_start')
            continue;
        onEvent(eventName, async (event, ctx) => {
            return sendEvent(eventName, serializeEvent(event), ctx);
        });
    }
    // ── Remote mode: input interception + remote event rendering ──
    // Two sub-modes:
    //   1. Native remote (AGENTUITY_CODER_NATIVE_REMOTE=1): remote-tui.ts drives rendering
    //      via Agent.emit(). Extension only provides Hub UI (footer, /hub, commands).
    //      No pi.sendMessage() rendering, no setupRemoteMode() event handlers.
    //   2. Legacy remote: Extension handles all rendering via pi.sendMessage({ customType }).
    if (remoteSessionId) {
        let remoteSession = null;
        // Register custom message renderers (used only in legacy mode, harmless in native)
        try {
            pi.registerMessageRenderer('remote_message', () => undefined);
            pi.registerMessageRenderer('remote_history', () => undefined);
        }
        catch {
            /* not available in this Pi version */
        }
        if (!isNativeRemote) {
            // Legacy remote: intercept input and render events via pi.sendMessage()
            pi.on('input', async (event, ctx) => {
                const inputEvent = event;
                const userMessage = inputEvent.text || inputEvent.message;
                if (!userMessage)
                    return;
                if (!remoteSession?.isConnected) {
                    if (ctx.hasUI)
                        ctx.ui.notify('Not connected to remote session yet');
                    return { action: 'handled' };
                }
                pi.sendMessage({
                    customType: 'remote_message',
                    content: `**You:** ${userMessage}`,
                    display: true,
                });
                remoteSession.prompt(userMessage, inputEvent.images);
                log(`Sent prompt to remote: ${userMessage.slice(0, 100)}`);
                if (ctx.hasUI) {
                    ctx.ui.setWorkingMessage('Sending to remote agent…');
                }
                return { action: 'handled' };
            });
            // Connect the remote session with legacy event rendering
            (async () => {
                try {
                    remoteSession = await setupRemoteMode(pi, hubUrl, remoteSessionId);
                    log(`Remote session connected: ${remoteSessionId}`);
                    if (footerCtx) {
                        remoteSession._setExtensionCtx?.(footerCtx);
                    }
                    pi.sendMessage({
                        customType: 'remote_message',
                        content: `Connected to remote session **${remoteSessionId}**`,
                        display: true,
                    });
                    remoteSession.setUiHandler(async (request) => {
                        if (!footerCtx)
                            return null;
                        return await handleRemoteUiRequest(footerCtx, request);
                    });
                }
                catch (err) {
                    const msg = err instanceof Error ? err.message : String(err);
                    log(`Remote connection failed: ${msg}`);
                    pi.sendMessage({
                        customType: 'remote_message',
                        content: `Failed to connect to remote session: ${msg}`,
                        display: true,
                    });
                }
            })();
        }
        // In native remote mode: no input interception, no event rendering.
        // remote-tui.ts handles Agent.emit() for native rendering and
        // monkey-patches Agent.prompt/steer/abort for input relay.
    }
    // Clean up on shutdown
    pi.on('session_shutdown', async (_event, _ctx) => {
        log('Shutting down — closing Hub connection');
        if (isNativeRemote && remoteSessionId) {
            setNativeRemoteExtensionContext(null);
        }
        try {
            client.close();
        }
        catch {
            /* pending promises rejected on close — safe to ignore */
        }
    });
}
function truncateOutput(text) {
    let result = text;
    const lines = result.split('\n');
    if (lines.length > MAX_OUTPUT_LINES) {
        result =
            lines.slice(0, MAX_OUTPUT_LINES).join('\n') +
                `\n\n[Output truncated — ${lines.length - MAX_OUTPUT_LINES} lines omitted]`;
    }
    if (result.length > MAX_OUTPUT_BYTES) {
        result =
            result.slice(0, MAX_OUTPUT_BYTES) +
                `\n\n[Output truncated — exceeded ${MAX_OUTPUT_BYTES} bytes]`;
    }
    return result;
}
/** Cache resolved Pi SDK modules to avoid repeated dynamic import resolution */
let _piSdkCache = null;
/**
 * Load Pi SDK packages at runtime.
 * The extension runs inside Pi's process, but @mariozechner/pi-ai isn't in
 * our node_modules — resolve it from Pi's install directory via process.argv[1].
 */
async function loadPiSdk() {
    if (_piSdkCache)
        return _piSdkCache;
    // Try direct import first (works if packages are in module resolution path)
    try {
        const piSdk = await import('@mariozechner/pi-coding-agent');
        const piAi = await import('@mariozechner/pi-ai');
        _piSdkCache = { piSdk, piAi };
        return _piSdkCache;
    }
    catch {
        /* fall through to argv[1] resolution */
    }
    // Resolve from Pi CLI binary (process.argv[1] → pi-coding-agent package root)
    const { realpathSync } = _require('node:fs');
    const { pathToFileURL } = _require('node:url');
    const { dirname, join } = _require('node:path');
    const piRealPath = realpathSync(process.argv[1] || '');
    const piPkgDir = dirname(dirname(piRealPath));
    const piSdkEntry = pathToFileURL(join(piPkgDir, 'dist', 'index.js')).href;
    const piAiEntry = pathToFileURL(join(piPkgDir, 'node_modules', '@mariozechner', 'pi-ai', 'dist', 'index.js')).href;
    const piSdk = await import(__rewriteRelativeImportExtension(piSdkEntry));
    const piAi = await import(__rewriteRelativeImportExtension(piAiEntry));
    _piSdkCache = { piSdk, piAi };
    return _piSdkCache;
}
/**
 * Create a Pi-compatible tool that proxies execution to the Hub via WebSocket.
 * Used to give sub-agents access to Hub tools (memory, context7, etc.).
 */
function createHubToolProxy(toolDef, hubClient) {
    return {
        name: toolDef.name,
        label: toolDef.label || toolDef.name,
        description: toolDef.description,
        parameters: toolDef.parameters,
        async execute(toolCallId, params) {
            if (!hubClient.connected) {
                return {
                    content: [
                        { type: 'text', text: `Hub not connected — cannot execute ${toolDef.name}` },
                    ],
                    details: undefined,
                };
            }
            const id = hubClient.nextId();
            try {
                const response = await hubClient.send({
                    id,
                    type: 'tool',
                    name: toolDef.name,
                    toolCallId,
                    params: (params ?? {}),
                });
                // Extract RETURN action result
                const returnAction = response.actions.find((a) => a.action === 'RETURN');
                if (returnAction && 'result' in returnAction) {
                    const text = typeof returnAction.result === 'string'
                        ? returnAction.result
                        : JSON.stringify(returnAction.result, null, 2);
                    return { content: [{ type: 'text', text }], details: undefined };
                }
                return { content: [{ type: 'text', text: 'Done' }], details: undefined };
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                return {
                    content: [{ type: 'text', text: `Hub tool error: ${msg}` }],
                    details: undefined,
                };
            }
        },
    };
}
/**
 * Run a sub-agent in-process using Pi's createAgentSession().
 * Sub-agents are created with noExtensions=true so they can't recursively
 * spawn further sub-agents (no task tool registered).
 * Sub-agents DO get Hub tools (memory, context7, etc.) via extensionFactories.
 * Only returns the final assistant text, not intermediate events.
 */
async function runSubAgent(agentConfig, task, hubClient, onProgress, signal) {
    const startTime = Date.now();
    const { piSdk, piAi } = await loadPiSdk();
    // Runtime-resolved dynamic imports — exact types unavailable statically
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager } = piSdk;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const { getModel } = piAi;
    // Model — use agent's configured model (sub-agents typically use haiku for speed)
    const modelId = agentConfig.model || 'claude-haiku-4-5';
    const [provider, id] = modelId.includes('/')
        ? modelId.split('/', 2)
        : ['anthropic', modelId];
    const subModel = getModel(provider, id);
    if (!subModel) {
        throw new Error(`Model "${modelId}" not available. ` +
            `Check that the ${provider} API key is configured ` +
            `(e.g. ${provider.toUpperCase().replace(/[^A-Z]/g, '_')}_API_KEY).`);
    }
    // Hub tools for this sub-agent (shared WebSocket connection)
    // Sub-agents get Hub tools (memory, context7, etc.) via extensionFactories
    // so they work in both driver and TUI mode.
    const hubTools = agentConfig.hubTools ?? [];
    const cwd = process.cwd();
    const agentDir = getAgentDir();
    // Resource loader — no extensions (prevents recursive task tool registration),
    // no skills, agent's system prompt injected directly.
    // Hub tools are injected via extensionFactories so sub-agents can use
    // memory_recall, context7_search, etc.
    const subLoader = new DefaultResourceLoader({
        cwd,
        agentDir,
        noExtensions: true,
        extensionFactories: hubTools.length > 0
            ? [
                (pi) => {
                    for (const toolDef of hubTools) {
                        // Proxy object has the correct shape; cast needed because return type is Record<string, unknown>
                        pi.registerTool(createHubToolProxy(toolDef, hubClient));
                    }
                },
            ]
            : [],
        systemPromptOverride: () => agentConfig.systemPrompt,
    });
    await subLoader.reload();
    // Pi v0.68.x uses a name allowlist for both built-in and extension/custom tools.
    const builtInToolNames = selectSubAgentToolNames(agentConfig);
    const hubToolNames = hubTools
        .map((tool) => (typeof tool.name === 'string' ? tool.name.trim() : ''))
        .filter((name) => name.length > 0);
    const tools = Array.from(new Set([...builtInToolNames, ...hubToolNames]));
    const { session } = await createAgentSession({
        // subModel is already untyped (from dynamic import) — createAgentSession is also dynamically imported
        model: subModel,
        thinkingLevel: (agentConfig.thinkingLevel || 'xhigh'),
        tools,
        resourceLoader: subLoader,
        // Pi now tracks cwd per session, so bind in-memory sub-agents to the actual repo cwd.
        sessionManager: SessionManager.inMemory(cwd),
    });
    await session.bindExtensions({});
    // Subscribe to sub-agent events for live progress tracking
    if (onProgress) {
        try {
            session.subscribe?.((event) => {
                try {
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    const evt = event;
                    const elapsed = Date.now() - startTime;
                    // Handle streaming message updates (thinking + text tokens)
                    if (evt.type === 'message_update' && evt.assistantMessageEvent) {
                        const ame = evt.assistantMessageEvent;
                        if (ame.type === 'thinking_delta' && ame.delta) {
                            onProgress({
                                agentName: agentConfig.name,
                                status: 'thinking_delta',
                                delta: ame.delta,
                                elapsed,
                            });
                        }
                        else if (ame.type === 'text_delta' && ame.delta) {
                            onProgress({
                                agentName: agentConfig.name,
                                status: 'text_delta',
                                delta: ame.delta,
                                elapsed,
                            });
                        }
                        return;
                    }
                    if (evt.type === 'tool_execution_start') {
                        const toolName = evt.toolName || evt.name || evt.tool || 'unknown';
                        const display = formatToolDisplay(toolName, typeof evt.args === 'string' || (evt.args && typeof evt.args === 'object')
                            ? evt.args
                            : undefined);
                        onProgress({
                            agentName: agentConfig.name,
                            status: 'tool_start',
                            toolCallId: typeof evt.toolCallId === 'string' ? evt.toolCallId : undefined,
                            currentTool: display.toolName,
                            currentToolArgs: display.toolArgs,
                            elapsed,
                        });
                    }
                    else if (evt.type === 'tool_execution_end') {
                        onProgress({
                            agentName: agentConfig.name,
                            status: 'tool_end',
                            toolCallId: typeof evt.toolCallId === 'string' ? evt.toolCallId : undefined,
                            elapsed,
                        });
                    }
                }
                catch {
                    /* ignore — progress tracking is best-effort */
                }
            });
        }
        catch {
            /* ignore — subscribe may not be available */
        }
    }
    // Abort signal support — cancel sub-agent when user presses Esc
    if (signal) {
        if (signal.aborted) {
            throw new Error('Aborted');
        }
        const onAbort = () => {
            log(`Sub-agent ${agentConfig.name} aborted by signal`);
            try {
                session.abort?.();
            }
            catch {
                /* ignore */
            }
        };
        signal.addEventListener('abort', onAbort, { once: true });
    }
    log(`Sub-agent started: ${agentConfig.name} (model: ${modelId})`);
    try {
        await session.prompt(task);
        // Only return the final assistant text — NOT intermediate JSONL events
        const output = session.getLastAssistantText?.() || '(no output)';
        const duration = Date.now() - startTime;
        log(`Sub-agent ${agentConfig.name} completed in ${duration}ms`);
        // Best-effort token extraction from sub-agent session messages
        const subTokens = { input: 0, output: 0, cost: 0 };
        try {
            const branch = session.sessionManager?.getBranch?.() || [];
            for (const entry of branch) {
                if (entry.type === 'message') {
                    const msg = entry.message;
                    if (msg.role === 'assistant' && msg.usage) {
                        subTokens.input += msg.usage.input;
                        subTokens.output += msg.usage.output;
                        subTokens.cost += msg.usage.cost.total;
                    }
                }
            }
        }
        catch {
            /* ignore — token extraction is best-effort */
        }
        return { output: truncateOutput(output.trim()), duration, tokens: subTokens };
    }
    catch (err) {
        try {
            session.abort?.();
        }
        catch {
            /* ignore */
        }
        throw err;
    }
}
export default agentuityCoderHub;
//# sourceMappingURL=index.js.map