/**
 * Remote TUI — Native Pi Coding Agent Renderer for Remote Sessions
 *
 * Creates a real AgentSessionRuntime + InteractiveMode backed by a remote sandbox
 * via Hub WebSocket, with the coder extension loaded for Hub UI (footer,
 * /hub overlay, commands, titlebar).
 *
 * Architecture:
 *   Remote TUI → Hub WebSocket (controller) → Sandbox (Pi RPC mode)
 *   - User input  → agent.prompt() (monkey-patched) → RPC `prompt` → Hub → sandbox
 *   - Sandbox Pi  → AgentEvent stream → Hub broadcast → AgentSession event handling → InteractiveMode renders natively
 *   - Hub UI      → coder extension (loaded via DefaultResourceLoader) provides footer, /hub, commands
 *
 * The local Agent never calls an LLM. Its prompt/steer/abort are monkey-patched
 * to send RPC commands. Its internal state is kept in sync with the remote agent
 * by mirroring state updates from each received event.
 *
 * The coder extension sees AGENTUITY_CODER_REMOTE_SESSION (Hub UI mode) +
 * AGENTUITY_CODER_NATIVE_REMOTE (skip legacy event rendering). It provides
 * Hub connection, footer, /hub overlay, commands, titlebar — but does NOT
 * intercept input or render events (this module handles both).
 *
 * IMPORTANT: Initialization order matters!
 *   1. Create RemoteSession (no connection yet)
 *   2. Create AgentSessionRuntime, patch Agent/Session methods
 *   3. Register ALL event handlers on RemoteSession
 *   4. THEN connect — so hydration + replay events are captured
 */
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, InteractiveMode, SessionManager, } from '@earendil-works/pi-coding-agent';
import { getNativeRemoteExtensionContext, setNativeRemoteExtensionContext, waitForNativeRemoteExtensionContext, } from "./native-remote-ui-context.js";
import { clearRemoteLifecycleWorkingMessage, getRemoteLifecycleActivityLabel, getRemoteLifecycleLabel, syncRemoteLifecycleWorkingMessage, } from "./remote-lifecycle.js";
import { RemoteSession } from "./remote-session.js";
import { agentuityCoderHub } from "./index.js";
import { handleRemoteUiRequest, REMOTE_FIRE_AND_FORGET_UI_METHODS } from "./remote-ui-handler.js";
import { dispatchRemoteSessionEvent, installRemoteRuntimeOperationGuards, REMOTE_RUNTIME_OPERATION_MESSAGE, } from "./remote-runtime.js";
const DEBUG = !!process.env['AGENTUITY_DEBUG'];
function log(msg) {
    if (DEBUG)
        console.error(`[remote-tui] ${msg}`);
}
/**
 * Run the native Pi TUI connected to a remote sandbox session.
 *
 * This is the entry point for `agentuity coder start --remote <sessionId>`.
 * Creates an AgentSessionRuntime with the coder extension loaded (Hub UI), then
 * monkey-patches the Agent for remote-backed execution.
 */
export async function runRemoteTui(options) {
    const { hubWsUrl, sessionId, apiKey, orgId } = options;
    log(`Starting remote TUI for session ${sessionId}`);
    log(`Hub URL: ${hubWsUrl}`);
    // Set env vars BEFORE loading the extension so it enters native remote mode
    process.env.AGENTUITY_CODER_HUB_URL = hubWsUrl;
    process.env.AGENTUITY_CODER_REMOTE_SESSION = sessionId;
    process.env.AGENTUITY_CODER_NATIVE_REMOTE = '1';
    setNativeRemoteExtensionContext(null);
    // ── 1. Create RemoteSession (NOT connected yet) ──
    // We register all handlers BEFORE connecting so that the hydration
    // message from the Hub (sent immediately after init) is captured.
    const remote = new RemoteSession(sessionId);
    // Resolve API key: explicit option → env var → null
    remote.apiKey = apiKey || process.env.AGENTUITY_CODER_API_KEY || null;
    remote.orgId = orgId || process.env.AGENTUITY_ORGID || null;
    let hydrationStreamingDetected = false;
    let sessionResumeSeen = false;
    // ── 2. Create AgentSessionRuntime with coder extension loaded ──
    // The extension provides Hub UI (footer, /hub overlay, commands, titlebar).
    // AGENTUITY_CODER_NATIVE_REMOTE=1 tells it to skip legacy event rendering.
    const cwd = process.cwd();
    const runtime = await createAgentSessionRuntime(async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
        const services = await createAgentSessionServices({
            cwd,
            agentDir,
            resourceLoaderOptions: {
                noExtensions: true, // Skip file-system extension discovery
                extensionFactories: [agentuityCoderHub], // Load coder extension directly
            },
        });
        return {
            ...(await createAgentSessionFromServices({
                services,
                sessionManager,
                sessionStartEvent,
                tools: [], // No local tools — sandbox has all the tools
            })),
            services,
            diagnostics: services.diagnostics,
        };
    }, {
        cwd,
        agentDir: getAgentDir(),
        sessionManager: SessionManager.inMemory(),
    });
    const session = runtime.session;
    const agent = session.agent;
    let runtimeDisposed = false;
    log('AgentSessionRuntime created');
    function notifyBlockedRuntimeOperation(operation) {
        const message = `${REMOTE_RUNTIME_OPERATION_MESSAGE} (${operation})`;
        const ctx = getNativeRemoteExtensionContext();
        if (ctx?.hasUI) {
            ctx.ui.notify(message, 'warning');
            ctx.ui.setStatus('remote_runtime', operation);
            return;
        }
        log(message);
    }
    installRemoteRuntimeOperationGuards(runtime, (operation) => notifyBlockedRuntimeOperation(operation));
    async function disposeRuntime() {
        if (runtimeDisposed)
            return;
        runtimeDisposed = true;
        await runtime.dispose();
    }
    // NOTE: Do NOT call session.bindExtensions() here.
    // InteractiveMode.initExtensions() calls it with the proper uiContext.
    // Calling it early fires session_start twice, duplicating extension init.
    let lifecycleState = remote.getLifecycleState();
    let lifecycleOwnsWorkingMessage = false;
    function emitSessionEvent(event) {
        dispatchRemoteSessionEvent(session, event);
    }
    function applyLifecycleUi(state) {
        const ctx = getNativeRemoteExtensionContext();
        if (!ctx?.hasUI)
            return;
        const shortSession = state.sessionId.slice(0, 16);
        ctx.ui.setStatus('remote_connection', `Remote: ${shortSession}${shortSession.length < state.sessionId.length ? '...' : ''} ${getRemoteLifecycleLabel(state)}`);
        const activity = getRemoteLifecycleActivityLabel(state);
        if (activity) {
            ctx.ui.setStatus('remote_activity', activity);
        }
        else {
            ctx.ui.setStatus('remote_activity', state.isStreaming ? 'agent working...' : 'idle');
        }
        lifecycleOwnsWorkingMessage = syncRemoteLifecycleWorkingMessage(state, ctx.ui, lifecycleOwnsWorkingMessage);
    }
    remote.onLifecycleChange((state) => {
        lifecycleState = state;
        applyLifecycleUi(state);
    });
    void waitForNativeRemoteExtensionContext(10_000).then((ctx) => {
        if (!ctx)
            return;
        applyLifecycleUi(lifecycleState);
    });
    // ── 3. Patch Agent to be remote-backed ──
    // Track the running prompt promise so InteractiveMode waits correctly
    let runningPromptResolve = null;
    let syntheticAgentStartEmitted = false;
    // Override Agent.prompt — send RPC prompt command, block until agent_end
    agent.prompt = async (input) => {
        const text = extractPromptText(input);
        log(`agent.prompt called, extracted text: ${text ? text.slice(0, 80) : '(empty)'}`);
        if (!text)
            return;
        // Set streaming state — InteractiveMode checks this
        agent._state.isStreaming = true;
        agent._state.streamMessage = null;
        agent._state.error = undefined;
        // Create runningPrompt so waitForIdle() works
        const runPromise = new Promise((resolve) => {
            runningPromptResolve = resolve;
        });
        agent.runningPrompt = runPromise;
        // Emit synthetic agent_start so InteractiveMode shows "working" immediately
        syntheticAgentStartEmitted = true;
        emitSessionEvent({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() });
        // Send RPC command to sandbox
        remote.prompt(text);
        log(`Sent prompt: ${text.slice(0, 100)}`);
        // Block until agent_end received from remote
        await runPromise;
    };
    // Override Agent.steer — send RPC steer command
    agent.steer = (m) => {
        const text = extractMessageText(m);
        if (text) {
            remote.steer(text);
            log(`Sent steer: ${text.slice(0, 100)}`);
        }
    };
    // Override Agent.abort — send RPC abort command
    agent.abort = () => {
        remote.abort();
        log('Sent abort');
        resolveRunningPrompt();
    };
    // Override Agent.waitForIdle
    agent.waitForIdle = () => {
        return agent.runningPrompt ?? Promise.resolve();
    };
    // ── 4. Patch AgentSession methods ──
    // session.prompt() does model/API key validation before calling agent.prompt().
    // In remote mode, skip validation — the sandbox has the model/key.
    // InteractiveMode calls session.prompt(text, { streamingBehavior: "steer" })
    // when user types during streaming, and session.prompt(text, { streamingBehavior: "followUp" })
    // for Alt+Enter follow-ups. Handle these by routing to steer/followUp commands.
    session.prompt = async (text, options) => {
        const behavior = options?.streamingBehavior;
        log(`session.prompt called (behavior=${behavior ?? 'normal'}): ${text.slice(0, 80)}`);
        // Extension commands (start with /) — let AgentSession handle them
        // so extension-registered slash commands still work in remote mode
        if (text.startsWith('/') && session._tryExecuteExtensionCommand) {
            try {
                const handled = await session._tryExecuteExtensionCommand(text);
                if (handled) {
                    log(`Extension command handled: ${text}`);
                    return;
                }
            }
            catch (err) {
                log(`Extension command error: ${err}`);
            }
        }
        if (behavior === 'steer') {
            remote.steer(text);
            log(`Sent steer: ${text.slice(0, 80)}`);
            return;
        }
        if (behavior === 'followUp') {
            remote.sendCommand({ type: 'follow_up', message: text });
            log(`Sent follow-up: ${text.slice(0, 80)}`);
            return;
        }
        // Normal prompt — send to sandbox
        await agent.prompt(text);
    };
    session.steer = async (text) => {
        remote.steer(text);
    };
    session.followUp = async (text) => {
        remote.sendCommand({ type: 'follow_up', message: text });
    };
    session.abort = async () => {
        remote.abort();
        resolveRunningPrompt();
    };
    // Disable auto-compaction (sandbox handles it)
    session.setAutoCompactionEnabled(false);
    session.setAutoRetryEnabled(false);
    // ── 5. Wire up remote events → Agent event pipeline ──
    // Only emit LIVE events (from broadcast) to Agent → InteractiveMode.
    // Replay events (from Durable Stream) are historical — skip them.
    // Hydration is handled separately via session_hydration → agent.replaceMessages().
    //
    // Events that arrive before InteractiveMode is initialized are buffered
    // and flushed after init (InteractiveMode registers listeners during init,
    // so session event dispatch before that fires into the void).
    let interactiveModeReady = false;
    let eventBuffer = [];
    let seenMessageStart = false;
    let seenAgentStart = false;
    // Dedup guard: some events may arrive twice via different broadcast paths
    // (rpc_event envelope + direct lifecycle broadcast). Track recent live events
    // by type+timestamp to skip duplicates.
    const recentEventKeys = new Set();
    const DEDUP_WINDOW_MS = 100;
    // InteractiveMode adds a new assistant component on every assistant message_start.
    // Track active/completed remote messages so normal terminal events and late duplicates
    // do not spawn extra components.
    let assistantStreamActive = false;
    const recentCompletedAssistantMessageKeys = [];
    const completedAssistantMessageKeySet = new Set();
    function rememberCompletedAssistantMessage(key) {
        if (completedAssistantMessageKeySet.has(key))
            return;
        recentCompletedAssistantMessageKeys.push(key);
        completedAssistantMessageKeySet.add(key);
        if (recentCompletedAssistantMessageKeys.length > 32) {
            const expired = recentCompletedAssistantMessageKeys.shift();
            if (expired)
                completedAssistantMessageKeySet.delete(expired);
        }
    }
    function emitRemoteUserPrompt(text, timestamp) {
        const userMessage = {
            role: 'user',
            content: [{ type: 'text', text }],
            timestamp,
        };
        const syntheticEvents = [
            { type: 'message_start', message: userMessage },
            { type: 'message_end', message: userMessage },
        ];
        if (!interactiveModeReady) {
            eventBuffer.push(...syntheticEvents);
            log('Buffered synthetic user_prompt events (InteractiveMode not ready)');
            return;
        }
        for (const event of syntheticEvents) {
            emitSessionEvent(event);
        }
    }
    remote.onEvent((rpcEvent) => {
        const source = rpcEvent._source ?? 'unknown';
        const isReplay = source === 'replay' ||
            rpcEvent.replay === true ||
            rpcEvent.isReplay === true;
        log(`Event received: ${rpcEvent.type} (source=${source})`);
        if (rpcEvent.type === 'session_resume') {
            sessionResumeSeen = true;
            log(`Session resume signaled (${typeof rpcEvent.streamId === 'string' ? rpcEvent.streamId : 'no stream id'})`);
            return;
        }
        if (rpcEvent.type === 'session_stream_ready') {
            log(`Durable stream ready (${typeof rpcEvent.streamId === 'string' ? rpcEvent.streamId : 'no stream id'})`);
            return;
        }
        if (rpcEvent.type === 'rpc_command_error') {
            const error = typeof rpcEvent.error === 'string'
                ? rpcEvent.error
                : 'Remote command failed';
            const ctx = getNativeRemoteExtensionContext();
            if (ctx?.hasUI) {
                ctx.ui.notify(error, 'warning');
                lifecycleOwnsWorkingMessage = clearRemoteLifecycleWorkingMessage(ctx.ui, lifecycleOwnsWorkingMessage);
            }
            agent._state.error = error;
            seenAgentStart = false;
            seenMessageStart = false;
            resolveRunningPrompt();
            assistantStreamActive = false;
            log(`Remote command error: ${error}`);
            return;
        }
        // session_hydration is handled separately below — skip it here
        if (rpcEvent.type === 'session_hydration')
            return;
        // Remote prompts from other controllers are broadcast as user_prompt.
        // Convert them to synthetic user message lifecycle events so InteractiveMode
        // renders them like locally-entered prompts. Replays are covered by hydration.
        if (rpcEvent.type === 'user_prompt') {
            if (isReplay) {
                log('Skipping replay user_prompt');
                return;
            }
            const promptText = typeof rpcEvent.content === 'string'
                ? rpcEvent.content
                : typeof rpcEvent.text === 'string'
                    ? rpcEvent.text
                    : '';
            if (!promptText.trim()) {
                log('Skipping empty user_prompt');
                return;
            }
            const promptTimestamp = typeof rpcEvent.timestamp === 'number'
                ? rpcEvent.timestamp
                : Date.now();
            log('Rendering live user_prompt as synthetic user message');
            emitRemoteUserPrompt(promptText, promptTimestamp);
            return;
        }
        // Skip user-role message events — the TUI already shows user messages
        // via InteractiveMode input or the synthetic user_prompt path above.
        // Pi emits message_start/end for both user and assistant messages; without
        // this guard the same prompt can appear twice.
        if (rpcEvent.type === 'message_start' || rpcEvent.type === 'message_end') {
            const msg = rpcEvent.message;
            if (msg?.role === 'user') {
                log(`Skipping ${rpcEvent.type} (role=user) — handled locally`);
                return;
            }
        }
        // Dedup: skip if we already processed the same event type + timestamp recently
        // Replays still check the cache, but they never populate it.
        const ts = rpcEvent.timestamp ?? 0;
        const dedupKey = `${rpcEvent.type}:${ts}`;
        if (recentEventKeys.has(dedupKey)) {
            log(`Dedup: skipping duplicate ${rpcEvent.type} (ts=${ts})`);
            return;
        }
        if (!isReplay && ts > 0) {
            recentEventKeys.add(dedupKey);
            setTimeout(() => recentEventKeys.delete(dedupKey), DEDUP_WINDOW_MS);
        }
        // Skip duplicate agent_start if we already emitted a synthetic one
        if (rpcEvent.type === 'agent_start' && syntheticAgentStartEmitted) {
            syntheticAgentStartEmitted = false;
            // Still update state from real event
            updateAgentState(agent, rpcEvent);
            return;
        }
        // Skip replay events — these are historical from Durable Stream
        if (isReplay) {
            log(`Skipping replay event: ${rpcEvent.type}`);
            return;
        }
        const hadSeenAgentStart = seenAgentStart;
        const hadSeenMessageStart = seenMessageStart;
        const assistantMessageKey = getRemoteAssistantMessageKey(rpcEvent);
        if (assistantMessageKey &&
            (rpcEvent.type === 'message_start' || rpcEvent.type === 'message_end') &&
            completedAssistantMessageKeySet.has(assistantMessageKey)) {
            log(`Dedup: skipping repeated assistant ${rpcEvent.type}`);
            return;
        }
        // State-based dedup for assistant message streaming.
        // Prevents duplicate AssistantMessageComponent from being added to the DOM.
        if (rpcEvent.type === 'message_start') {
            const msg = rpcEvent.message;
            if (msg?.role === 'assistant') {
                if (assistantStreamActive) {
                    log(`Dedup: skipping duplicate assistant message_start (stream already active)`);
                    return;
                }
                assistantStreamActive = true;
            }
        }
        if (rpcEvent.type === 'message_end') {
            const msg = rpcEvent.message;
            if (msg?.role === 'assistant') {
                assistantStreamActive = false;
                if (assistantMessageKey) {
                    rememberCompletedAssistantMessage(assistantMessageKey);
                }
            }
        }
        if (rpcEvent.type === 'agent_end') {
            assistantStreamActive = false;
        }
        // Track streaming lifecycle events so we can inject synthetics when
        // we attach mid-stream (controller connected after agent already started).
        if (rpcEvent.type === 'agent_start')
            seenAgentStart = true;
        if (rpcEvent.type === 'agent_end') {
            seenAgentStart = false;
            seenMessageStart = false;
        }
        if (rpcEvent.type === 'message_start')
            seenMessageStart = true;
        if (rpcEvent.type === 'message_end')
            seenMessageStart = false;
        // Update agent internal state (mirrors Agent._runLoop behavior)
        updateAgentState(agent, rpcEvent);
        // Buffer events until InteractiveMode is ready to receive them
        if (!interactiveModeReady) {
            eventBuffer.push(rpcEvent);
            log(`Buffered event: ${rpcEvent.type} (InteractiveMode not ready)`);
            return;
        }
        // Mid-stream attach guard: if we receive message_update or message_end
        // without having seen message_start, inject synthetic agent_start +
        // message_start so InteractiveMode sets up its streaming component.
        // Without this, the events are silently dropped because
        // InteractiveMode.streamingComponent is null.
        // This happens when the controller connects mid-stream or after the
        // agent finishes — the broadcast of agent_start/message_start occurred
        // before the controller WebSocket was registered.
        let injectedSyntheticMessageStart = false;
        if ((rpcEvent.type === 'message_update' || rpcEvent.type === 'message_end') &&
            !hadSeenMessageStart) {
            log(`Live ${rpcEvent.type} without prior message_start — injecting synthetics`);
            if (!hadSeenAgentStart) {
                emitSessionEvent({
                    type: 'agent_start',
                    agentName: 'lead',
                    timestamp: Date.now(),
                });
                seenAgentStart = true;
            }
            emitSessionEvent({
                type: 'message_start',
                message: {
                    role: 'assistant',
                    content: [],
                    api: 'anthropic-messages',
                    provider: 'anthropic',
                    model: 'remote',
                    usage: {
                        input: 0,
                        output: 0,
                        cacheRead: 0,
                        cacheWrite: 0,
                        totalTokens: 0,
                        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
                    },
                    timestamp: Date.now(),
                },
            });
            seenMessageStart = true;
            assistantStreamActive = true;
            injectedSyntheticMessageStart = true;
        }
        // Emit to subscribers — InteractiveMode.handleEvent processes this
        emitSessionEvent(rpcEvent);
        if (injectedSyntheticMessageStart && rpcEvent.type === 'message_end') {
            seenMessageStart = false;
            seenAgentStart = hadSeenAgentStart;
            assistantStreamActive = false;
        }
        // Resolve running prompt when agent finishes
        if (rpcEvent.type === 'agent_end') {
            resolveRunningPrompt();
        }
    });
    // ── 6. Wire up UI handlers for extension dialogs from sandbox ──
    remote.setUiHandler(async (request) => {
        const ctx = getNativeRemoteExtensionContext() ?? (await waitForNativeRemoteExtensionContext(10_000));
        if (!ctx) {
            log(`UI request: ${request.method} (${request.id}) timed out waiting for extension UI context`);
            return REMOTE_FIRE_AND_FORGET_UI_METHODS.has(request.method) ? undefined : null;
        }
        try {
            return await handleRemoteUiRequest(ctx, request);
        }
        catch (err) {
            log(`UI request handler error for ${request.method}: ${err instanceof Error ? err.message : String(err)}`);
            return REMOTE_FIRE_AND_FORGET_UI_METHODS.has(request.method) ? undefined : null;
        }
    });
    // ── 7. Handle hydration (initial state from Hub) ──
    // Hydration arrives as the message AFTER 'init' on the WebSocket.
    // remote.connect() resolves on 'init', so hydration arrives in the next
    // event loop tick. We use a promise to wait for it before creating
    // InteractiveMode (which calls renderInitialMessages from SessionManager).
    const sm = session.sessionManager;
    let resolveHydration;
    let hydrationComplete = false;
    const hydrationReady = new Promise((resolve) => {
        resolveHydration = () => {
            if (hydrationComplete)
                return;
            hydrationComplete = true;
            resolve();
        };
    });
    let hydrationCount = 0;
    remote.onEvent((event) => {
        if (event.type !== 'session_hydration')
            return;
        hydrationCount++;
        const entries = event.entries;
        // Extract task text from hydration (Hub includes session.sandbox?.task)
        const hydrationTask = event.task;
        // On reconnect (2nd+ hydration), clear SM to prevent duplicate accumulation.
        // agent.replaceMessages() already replaces state.messages, but SM only appends.
        if (hydrationCount > 1) {
            log(`Re-hydration #${hydrationCount} — clearing SessionManager to prevent duplicates`);
            try {
                if (typeof sm.clear === 'function') {
                    sm.clear();
                }
            }
            catch (err) {
                log(`SM clear error (non-fatal): ${err}`);
            }
        }
        if (!entries?.length) {
            log('Received session_hydration with no entries');
            // Even with no entries, inject task as user message if available
            if (hydrationTask) {
                const taskMsg = {
                    role: 'user',
                    content: [{ type: 'text', text: hydrationTask }],
                    timestamp: Date.now(),
                };
                agent.replaceMessages([taskMsg]);
                try {
                    sm.appendMessage(taskMsg);
                }
                catch (err) {
                    log(`SM append task error: ${err}`);
                }
                log('Injected task as user message (no entries)');
            }
            resolveHydration();
            return;
        }
        log(`Hydrating ${entries.length} entries (hydration #${hydrationCount})`);
        const agentMessages = [];
        // If we have a task and no user_prompt entry, inject the task as the first user message
        const hasUserEntry = entries.some((e) => e.type === 'user_prompt' || e.role === 'user');
        if (hydrationTask && !hasUserEntry) {
            const taskMsg = {
                role: 'user',
                content: [{ type: 'text', text: hydrationTask }],
                timestamp: Date.now(),
            };
            agentMessages.push(taskMsg);
            try {
                sm.appendMessage(taskMsg);
            }
            catch (err) {
                log(`SM append task error: ${err}`);
            }
            log('Injected task as user message');
        }
        for (const entry of entries) {
            const text = typeof entry.content === 'string'
                ? entry.content
                : Array.isArray(entry.content)
                    ? entry.content
                        .filter((c) => c.type === 'text' && typeof c.text === 'string')
                        .map((c) => c.text)
                        .join('\n')
                    : '';
            if (!text)
                continue;
            // Hub conversation entries use type: 'message' for assistant, 'thinking' for thinking,
            // 'task_result' for delegation results, 'turn' for turn markers, 'user_prompt' for user input.
            // Only 'user_prompt' entries are user messages; everything else is assistant-side.
            const isAssistant = entry.role === 'assistant' ||
                entry.type === 'message' ||
                entry.type === 'thinking' ||
                entry.type === 'task_result' ||
                entry.type === 'assistant';
            if (isAssistant) {
                const msg = {
                    role: 'assistant',
                    content: [{ type: 'text', text }],
                    api: 'anthropic-messages',
                    provider: 'anthropic',
                    model: 'remote',
                    usage: {
                        input: 0,
                        output: 0,
                        cacheRead: 0,
                        cacheWrite: 0,
                        totalTokens: 0,
                        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
                    },
                    stopReason: 'stop',
                    timestamp: entry.timestamp ?? Date.now(),
                };
                agentMessages.push(msg);
                try {
                    sm.appendMessage(msg);
                }
                catch (err) {
                    log(`SM append error: ${err}`);
                }
            }
            else {
                const msg = {
                    role: 'user',
                    content: [{ type: 'text', text }],
                    timestamp: entry.timestamp ?? Date.now(),
                };
                agentMessages.push(msg);
                try {
                    sm.appendMessage(msg);
                }
                catch (err) {
                    log(`SM append error: ${err}`);
                }
            }
        }
        if (agentMessages.length > 0) {
            agent.replaceMessages(agentMessages);
            log(`Hydrated ${agentMessages.length} agent messages (+ session manager)`);
        }
        else {
            log('Hydration: 0 messages after filtering (all entries had empty text?)');
        }
        // Restore streaming state from hydration — fixes first-connect miss
        const streamingState = event.streamingState;
        if (streamingState?.isStreaming) {
            agent._state.isStreaming = true;
            hydrationStreamingDetected = true;
            // Create runningPrompt so InteractiveMode knows we're busy
            if (!agent.runningPrompt) {
                const runPromise = new Promise((resolve) => {
                    runningPromptResolve = resolve;
                });
                agent.runningPrompt = runPromise;
            }
            log(`Hydration: session is streaming with ${streamingState.activeTasks?.length ?? 0} active tasks`);
        }
        resolveHydration();
    });
    // ── 8. NOW connect to Hub ──
    // All handlers are registered, so hydration + replay events will be captured.
    log('Connecting to Hub (handlers registered)...');
    await remote.connect(hubWsUrl);
    log('Connected to Hub as controller');
    // Wait for hydration message (arrives right after init), with a timeout
    // in case this is the first connection and there's nothing to hydrate.
    await Promise.race([
        hydrationReady,
        new Promise((resolve) => {
            const waitStartedAt = Date.now();
            const poll = () => {
                if (hydrationComplete) {
                    resolve();
                    return;
                }
                const timeoutMs = sessionResumeSeen ? 5000 : 2000;
                if (Date.now() - waitStartedAt >= timeoutMs) {
                    log('Hydration timeout — no session_hydration received');
                    resolve();
                    return;
                }
                setTimeout(poll, 50);
            };
            poll();
        }),
    ]);
    const smEntries = sm.getEntries?.() ?? [];
    log(`SessionManager has ${smEntries.length} entries after hydration`);
    log(`Post-hydration: SM has ${smEntries.length} entries, leafId=${sm.getLeafId?.() ?? 'N/A'}`);
    // ── 9. Start InteractiveMode — full native Pi TUI ──
    log('Creating InteractiveMode');
    const interactive = new InteractiveMode(runtime);
    log('InteractiveMode created, calling init...');
    await interactive.init();
    // Flush buffered events now that InteractiveMode is listening.
    // If the session was already streaming when we connected (mid-stream attach),
    // InteractiveMode needs agent_start + message_start to set up its streaming
    // components. Without these, message_update events are silently dropped
    // because InteractiveMode.streamingComponent is null.
    interactiveModeReady = true;
    if (hydrationStreamingDetected) {
        // Immediately emit agent_start + message_start so InteractiveMode shows
        // the streaming indicator right away, before any buffered events flush.
        // This prevents the blank screen gap between connect and first event.
        log('Hydration detected streaming — emitting immediate synthetics');
        emitSessionEvent({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() });
        emitSessionEvent({
            type: 'message_start',
            message: {
                role: 'assistant',
                content: [],
                api: 'anthropic-messages',
                provider: 'anthropic',
                model: 'remote',
                usage: {
                    input: 0,
                    output: 0,
                    cacheRead: 0,
                    cacheWrite: 0,
                    totalTokens: 0,
                    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
                },
                timestamp: Date.now(),
            },
        });
        seenAgentStart = true;
        seenMessageStart = true;
        assistantStreamActive = true;
        // Remove any agent_start/message_start from buffer since we already emitted them
        eventBuffer = eventBuffer.filter((e) => e.type !== 'agent_start' && e.type !== 'message_start');
    }
    if (eventBuffer.length > 0) {
        log(`Flushing ${eventBuffer.length} events: ${eventBuffer.map((e) => e.type).join(', ')}`);
        for (const buffered of eventBuffer) {
            emitSessionEvent(buffered);
            if (buffered.type === 'agent_end') {
                resolveRunningPrompt();
            }
        }
    }
    eventBuffer = [];
    log('InteractiveMode initialized, calling run...');
    // Handle clean shutdown
    const cleanup = () => {
        remote.close();
        setNativeRemoteExtensionContext(null);
        interactive.stop();
        void disposeRuntime().catch((err) => {
            log(`Runtime dispose error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
        });
    };
    process.on('SIGINT', cleanup);
    process.on('SIGTERM', cleanup);
    try {
        await interactive.run();
    }
    catch (err) {
        log(`InteractiveMode.run() threw: ${err instanceof Error ? err.stack : String(err)}`);
        throw err;
    }
    finally {
        remote.close();
        setNativeRemoteExtensionContext(null);
        await disposeRuntime().catch((err) => {
            log(`Runtime dispose error: ${err instanceof Error ? err.message : String(err)}`);
        });
        log('Remote TUI exited');
    }
    // ── Helper: resolve the running prompt promise ──
    function resolveRunningPrompt() {
        syntheticAgentStartEmitted = false;
        agent._state.isStreaming = false;
        agent._state.streamMessage = null;
        agent._state.pendingToolCalls = new Set();
        if (runningPromptResolve) {
            runningPromptResolve();
            runningPromptResolve = null;
            agent.runningPrompt = undefined;
        }
    }
}
// ══════════════════════════════════════════════
// Agent state synchronization
// Mirrors Agent._runLoop state updates (agent.js lines 317-352)
// ══════════════════════════════════════════════
function updateAgentState(agent, event) {
    const state = agent._state;
    switch (event.type) {
        case 'agent_start':
            state.isStreaming = true;
            break;
        case 'agent_end':
            state.isStreaming = false;
            state.streamMessage = null;
            state.pendingToolCalls = new Set();
            break;
        case 'message_start':
            state.streamMessage = event.message;
            state.isStreaming = true;
            break;
        case 'message_update':
            state.streamMessage = event.message;
            break;
        case 'message_end':
            state.streamMessage = null;
            // NOTE: Do NOT push to state.messages here.
            // AgentSession._handleAgentEvent already persists via sessionManager.appendMessage().
            // Pushing here causes state.messages to accumulate duplicates with SM,
            // leading to visual duplicates if rebuildChatFromMessages() is ever triggered.
            break;
        case 'tool_execution_start': {
            const s = new Set(state.pendingToolCalls);
            s.add(event.toolCallId);
            state.pendingToolCalls = s;
            break;
        }
        case 'tool_execution_end': {
            const s = new Set(state.pendingToolCalls);
            s.delete(event.toolCallId);
            state.pendingToolCalls = s;
            break;
        }
        case 'thinking_start':
            state.isStreaming = true;
            break;
        case 'thinking_end':
            break;
        case 'tool_call':
            state.isStreaming = true;
            break;
        case 'tool_result':
            break;
        case 'task_start':
            state.isStreaming = true;
            break;
        case 'task_complete':
        case 'task_error':
            break;
        case 'turn_end': {
            const msg = event.message;
            if (msg?.role === 'assistant' && msg?.errorMessage) {
                state.error = msg.errorMessage;
            }
            break;
        }
    }
}
// ══════════════════════════════════════════════
// Text extraction helpers
// ══════════════════════════════════════════════
function extractPromptText(input) {
    if (typeof input === 'string')
        return input;
    if (Array.isArray(input)) {
        return input.map(extractMessageText).filter(Boolean).join('\n');
    }
    return extractMessageText(input);
}
function extractMessageText(msg) {
    if (typeof msg === 'string')
        return msg;
    if (!msg || typeof msg !== 'object')
        return '';
    if (typeof msg.content === 'string')
        return msg.content;
    if (Array.isArray(msg.content)) {
        return msg.content
            .filter((c) => c?.type === 'text' && typeof c.text === 'string')
            .map((c) => c.text)
            .join('\n');
    }
    return '';
}
function getRemoteAssistantMessageKey(event) {
    if (event.type !== 'message_start' && event.type !== 'message_end')
        return undefined;
    const message = event.message;
    if (!message || typeof message !== 'object' || message.role !== 'assistant') {
        return undefined;
    }
    const messageId = typeof message.id === 'string' ? message.id : '';
    if (messageId)
        return `id:${messageId}`;
    const timestamp = typeof message.timestamp === 'number'
        ? String(message.timestamp)
        : typeof message.timestamp === 'string'
            ? message.timestamp
            : typeof event.timestamp === 'number'
                ? String(event.timestamp)
                : typeof event.timestamp === 'string'
                    ? event.timestamp
                    : '';
    if (timestamp)
        return `ts:${timestamp}`;
    const text = extractMessageText(message);
    if (!text)
        return undefined;
    const stopReason = typeof message.stopReason === 'string' ? message.stopReason : '';
    return `text:${stopReason}|${text}`;
}
//# sourceMappingURL=remote-tui.js.map