import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/**
 * RunCode ink-based terminal UI.
 * Real-time streaming, thinking animation, tool progress, slash commands.
 */
import chalk from 'chalk';
import { useState, useEffect, useCallback, useRef } from 'react';
import { render, Static, Box, Text, useApp, useInput, useStdout } from 'ink';
import Spinner from 'ink-spinner';
import TextInput from 'ink-text-input';
import VimInput from './vim-input.js';
import { renderMarkdown, renderMarkdownStreaming } from './markdown.js';
import { resolveModel, PICKER_CATEGORIES, PICKER_MODELS_FLAT, } from './model-picker.js';
import { estimateCost } from '../pricing.js';
import { formatTokens, shortModelName } from '../stats/format.js';
import { mouse, forceDisableMouseTracking } from './mouse.js';
// ─── Full-width input box ──────────────────────────────────────────────────
const DISABLE_AUTO_WRAP = '\x1b[?7l';
const ENABLE_AUTO_WRAP = '\x1b[?7h';
function disableTerminalAutoWrap() {
    if (!process.stdout.isTTY)
        return undefined;
    let restored = false;
    const restore = () => {
        if (restored || !process.stdout.writable)
            return;
        restored = true;
        process.stdout.write(ENABLE_AUTO_WRAP);
    };
    process.stdout.write(DISABLE_AUTO_WRAP);
    process.once('exit', restore);
    return () => {
        process.off('exit', restore);
        restore();
    };
}
// Subscribe to terminal resize so React re-renders with fresh dimensions.
// Without this, useStdout() returns a stable ref and children that read
// stdout.columns on each render still need React to re-execute them — which
// only happens if some state changes. stdout.on('resize') → setState does that.
function useTerminalSize() {
    const { stdout } = useStdout();
    const [size, setSize] = useState(() => ({
        cols: stdout?.columns ?? 80,
        rows: stdout?.rows ?? 24,
    }));
    useEffect(() => {
        if (!stdout)
            return;
        const onResize = () => setSize({
            cols: stdout.columns ?? 80,
            rows: stdout.rows ?? 24,
        });
        stdout.on('resize', onResize);
        return () => { stdout.off('resize', onResize); };
    }, [stdout]);
    return size;
}
function InputBox({ input, setInput, onSubmit, model, balance, chain, walletTail, sessionCost, queued, queuedCount, focused, busy, awaitingApproval, awaitingAnswer, contextPct, vimMode, onVimModeChange }) {
    const { cols } = useTerminalSize();
    // Avoid drawing right up to the terminal edge. Several terminals auto-wrap
    // a full-width border glyph onto the next row, which leaves "ghost" top
    // borders behind on re-render after errors / status changes.
    const boxWidth = Math.max(20, cols - 2);
    // Awaiting-input states beat "Working..." — the agent isn't busy, it's
    // blocked on the user. Saying "Working..." here while a permission dialog
    // sits in the scrollback above is exactly how users miss it (verified
    // 2026-05-04 from a real screenshot — "Working..." spinner kept turning
    // while the agent waited on a Bash approval).
    const placeholder = awaitingApproval
        ? '⚠  Approval needed — press [y]/[a]/[n] in the prompt above'
        : awaitingAnswer
            ? '⚠  Question above — type your answer'
            : busy
                ? (queued
                    ? `⏎ ${queuedCount ?? 1} queued: ${queued.slice(0, 40)}`
                    : 'Working...')
                : 'Type a message...';
    // Color the input-box border to match the urgency. Awaiting-user states
    // get a bright yellow border so the focal point physically moves down to
    // the input field, even peripheral vision picks it up.
    const borderColor = awaitingApproval || awaitingAnswer ? 'yellow' : undefined;
    const showSpinner = busy && !input && !awaitingApproval && !awaitingAnswer;
    const leadingGlyph = (awaitingApproval || awaitingAnswer)
        ? _jsx(Text, { color: "yellow", bold: true, children: "\u26A0 " })
        : (showSpinner ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null);
    return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: borderColor, borderDimColor: !borderColor, paddingX: 1, width: boxWidth, children: [leadingGlyph, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange })) : (_jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false })) })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), "  \u00B7  ", (() => {
                            // Color the balance by funding state. Real session 2026-05-04
                            // had a user staring at "$0.08 USDC" in dim text wondering
                            // whether it meant "out of money" or "wrong chain". Make
                            // low/critical balances unmistakable. Thresholds match the
                            // ~$0.10 / ~$0.50 ranges where a typical Opus turn ($0.08–
                            // $0.15) tips over: <$0.50 = red bold + low hint;
                            // <$1.00 = yellow; otherwise plain dim.
                            const m = balance.match(/\$([\d.]+)/);
                            const num = m ? parseFloat(m[1]) : null;
                            if (num !== null && num < 0.50) {
                                return _jsxs(_Fragment, { children: [_jsx(Text, { color: "red", bold: true, children: balance }), _jsx(Text, { color: "red", children: " \u26A0 low \u2014 fund wallet or /model free" })] });
                            }
                            if (num !== null && num < 1.00) {
                                return _jsx(Text, { color: "yellow", children: balance });
                            }
                            return balance;
                        })(), chain ? _jsxs(Text, { children: ["  \u00B7  ", _jsx(Text, { color: "magenta", children: chain }), walletTail ? _jsxs(Text, { dimColor: true, children: [":", walletTail] }) : ''] }) : '', sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: ["  -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (() => {
                            // Visual context bar: ▓▓▓▓▓▓░░░░ 75%
                            const filled = Math.round(contextPct / 10);
                            const empty = 10 - filled;
                            const barColor = contextPct > 85 ? 'red' : contextPct > 70 ? 'yellow' : 'green';
                            return (_jsxs(Text, { children: ['  ', _jsx(Text, { color: barColor, children: '▓'.repeat(filled) }), _jsx(Text, { dimColor: true, children: '░'.repeat(empty) }), _jsxs(Text, { color: barColor, children: [' ', contextPct, "%"] })] }));
                        })() : null, (queuedCount ?? 0) > 0 ? _jsxs(Text, { color: "cyan", children: ["  \u00B7  ", queuedCount, " queued"] }) : null, '  ·  esc'] }) })] }));
}
function formatAgentErrorForDisplay(error) {
    const lines = error.split('\n').map((line) => line.trim()).filter(Boolean);
    const tipIndex = lines.findIndex((line) => /^tip:/i.test(line));
    const mainLines = tipIndex >= 0 ? lines.slice(0, tipIndex) : lines;
    const tipLines = tipIndex >= 0 ? lines.slice(tipIndex) : [];
    let main = mainLines.join(' ').replace(/\s+/g, ' ').trim();
    let tip = tipLines.join(' ').replace(/\s+/g, ' ').trim();
    const labelMatch = /^\[([^\]]+)\]\s*/.exec(main);
    const label = labelMatch?.[1];
    if (labelMatch)
        main = main.slice(labelMatch[0].length).trim();
    if (tip)
        tip = tip.replace(/^tip:\s*/i, '');
    const out = ['**Request failed**'];
    if (label)
        out.push(`- Type: ${label}`);
    if (main)
        out.push(`- Message: ${main}`);
    if (tip)
        out.push(`- Tip: ${tip}`);
    return out.join('\n');
}
function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
    const { exit } = useApp();
    // Track terminal rows so we can cap the dynamic-region height. Ink wipes the
    // terminal scrollback (via ansiEscapes.clearTerminal → \x1b[3J) whenever the
    // dynamic output exceeds rows, so any tall live region (streaming text,
    // model picker) must be windowed to preserve "scroll to the start" history.
    const { rows: termRows } = useTerminalSize();
    const [input, setInput] = useState('');
    const [streamText, setStreamText] = useState('');
    const [thinking, setThinking] = useState(false);
    const [waiting, setWaiting] = useState(false);
    const [tools, setTools] = useState(new Map());
    // Completed tool results committed to Static (permanent scrollback — no re-render artifacts)
    const [completedTools, setCompletedTools] = useState([]);
    // Last completed tool — shown in dynamic area so it can be expanded/collapsed with Tab
    const [expandableTool, setExpandableTool] = useState(null);
    // Full responses committed to Static immediately — goes into terminal scrollback
    const [committedResponses, setCommittedResponses] = useState([]);
    // Short preview of latest response shown in dynamic area (last ~5 lines, cleared on next turn)
    const [responsePreview, setResponsePreview] = useState('');
    const [currentModel, setCurrentModel] = useState(initialModel || PICKER_MODELS_FLAT[0].id);
    const [ready, setReady] = useState(!startWithPicker);
    const [mode, setMode] = useState(startWithPicker ? 'model-picker' : 'input');
    const [pickerIdx, setPickerIdx] = useState(0);
    const [statusMsg, setStatusMsg] = useState('');
    const [statusTone, setStatusTone] = useState('success');
    const [turnTokens, setTurnTokens] = useState({ input: 0, output: 0, calls: 0 });
    const [contextPct, setContextPct] = useState(0);
    const [totalCost, setTotalCost] = useState(0);
    const [showHelp, setShowHelp] = useState(false);
    const [showWallet, setShowWallet] = useState(false);
    const [vimEnabled, setVimEnabled] = useState(false);
    const [currentVimMode, setCurrentVimMode] = useState('insert');
    const [balance, setBalance] = useState(walletBalance);
    // Parse the fetched balance to a number so we can compute live balance = fetchedBalance - sessionCost.
    // costAtLastFetch tracks totalCost when balance was last fetched, to avoid double-subtracting.
    const parseBalanceNum = (s) => {
        const m = s.match(/\$([\d.]+)/);
        return m ? parseFloat(m[1]) : null;
    };
    const [baseBalanceNum, setBaseBalanceNum] = useState(() => parseBalanceNum(walletBalance));
    const [costAtLastFetch, setCostAtLastFetch] = useState(0);
    const costAtLastFetchRef = useRef(0);
    const baseBalanceNumRef = useRef(parseBalanceNum(walletBalance));
    const [thinkingText, setThinkingText] = useState('');
    const [lastPrompt, setLastPrompt] = useState('');
    const [inputHistory, setInputHistory] = useState([]);
    const [historyIdx, setHistoryIdx] = useState(-1);
    const [permissionRequest, setPermissionRequest] = useState(null);
    const [askUserRequest, setAskUserRequest] = useState(null);
    const [askUserInput, setAskUserInput] = useState('');
    // Ring the terminal bell exactly once when a permission/askUser dialog
    // first appears. Helpful when the user has Franklin in a background
    // tab and the agent stops to ask for approval — verified 2026-05-04
    // from a real screenshot where the user missed the dialog because the
    // input box still read "Working...". Opt-out via FRANKLIN_NO_BELL=1.
    const bellPlayedRef = useRef(false);
    useEffect(() => {
        const dialogActive = !!permissionRequest || !!askUserRequest;
        if (dialogActive && !bellPlayedRef.current) {
            bellPlayedRef.current = true;
            if (process.env.FRANKLIN_NO_BELL !== '1') {
                try {
                    process.stderr.write('\x07');
                }
                catch { /* swallow — never break the UI on a TTY without bell */ }
            }
        }
        else if (!dialogActive) {
            bellPlayedRef.current = false;
        }
    }, [permissionRequest, askUserRequest]);
    // Messages queued while agent is busy — auto-submitted FIFO when turns complete.
    const [queuedInputs, setQueuedInputs] = useState([]);
    const turnDoneCallbackRef = useRef(null);
    // ── Render throttling: batch rapid text_delta/thinking_delta into 50ms frames ──
    // Without this, each delta (20-100/sec) triggers a full React re-render.
    // With this, we accumulate in refs and flush at ~20fps — smooth and efficient.
    const pendingTextRef = useRef('');
    const pendingThinkingRef = useRef('');
    const flushTimerRef = useRef(null);
    // Per-turn reasoning meter: first thinking delta starts the clock, first text
    // delta (or turn_done) stops it. Persists on the committed response as
    // "✻ Thought for 3.2s · ~420 tokens" so users see the cost of reasoning even
    // after the live thinking spinner collapses.
    const thinkStartRef = useRef(null);
    const thinkCharsRef = useRef(0);
    const thinkMsRef = useRef(null);
    const flushPendingText = useCallback(() => {
        flushTimerRef.current = null;
        const text = pendingTextRef.current;
        const thinking = pendingThinkingRef.current;
        if (text) {
            pendingTextRef.current = '';
            setWaiting(false);
            setThinking(false);
            // Text started: freeze the reasoning meter
            if (thinkStartRef.current !== null && thinkMsRef.current === null) {
                thinkMsRef.current = Date.now() - thinkStartRef.current;
            }
            setStreamText(prev => prev + text);
        }
        if (thinking) {
            pendingThinkingRef.current = '';
            setWaiting(false);
            setThinking(true);
            if (thinkStartRef.current === null)
                thinkStartRef.current = Date.now();
            thinkCharsRef.current += thinking.length;
            setThinkingText(prev => {
                const updated = prev + thinking;
                return updated.length > 500 ? updated.slice(-500) : updated;
            });
        }
    }, []);
    const scheduleFlush = useCallback(() => {
        if (!flushTimerRef.current) {
            flushTimerRef.current = setTimeout(flushPendingText, 50);
        }
    }, [flushPendingText]);
    // Refs to read current state values inside memoized event handlers (avoids stale closures)
    const streamTextRef = useRef('');
    const turnTokensRef = useRef({ input: 0, output: 0, calls: 0 });
    const totalCostRef = useRef(0);
    const turnCostRef = useRef(0); // per-turn cost (reset each turn)
    const turnModelRef = useRef(undefined);
    const turnTierRef = useRef(undefined);
    const turnSavingsRef = useRef(undefined);
    const turnCtxPctRef = useRef(undefined);
    const queuedInputsRef = useRef([]);
    // Keep refs in sync so memoized event handlers can read current values
    streamTextRef.current = streamText;
    turnTokensRef.current = turnTokens;
    totalCostRef.current = totalCost;
    queuedInputsRef.current = queuedInputs;
    costAtLastFetchRef.current = costAtLastFetch;
    baseBalanceNumRef.current = baseBalanceNum;
    // Compute live balance = fetchedBalance - spend_since_last_fetch
    const liveBalance = baseBalanceNum !== null
        ? `$${Math.max(0, baseBalanceNum - (totalCost - costAtLastFetch)).toFixed(2)} USDC`
        : balance;
    const showStatus = useCallback((text, tone = 'success', durationMs = 3000) => {
        setStatusTone(tone);
        setStatusMsg(text);
        if (durationMs > 0) {
            setTimeout(() => setStatusMsg(''), durationMs);
        }
    }, []);
    const commitResponse = useCallback((text, tokens = turnTokensRef.current, cost = turnCostRef.current) => {
        if (!text.trim())
            return;
        // Snapshot the thinking meter for this turn (reset happens in turn_done,
        // which covers the empty-response-but-thinking case too)
        const thinkMs = thinkMsRef.current ?? (thinkStartRef.current !== null
            ? Date.now() - thinkStartRef.current
            : undefined);
        const thinkChars = thinkCharsRef.current || undefined;
        setCommittedResponses((rs) => {
            const next = [...rs, {
                    key: String(Date.now() + Math.random()),
                    text,
                    tokens,
                    cost,
                    model: turnModelRef.current,
                    tier: turnTierRef.current,
                    savings: turnSavingsRef.current,
                    ctxPct: turnCtxPctRef.current,
                    thinkMs,
                    thinkChars,
                }];
            // Cap at 300 items — older items are already in terminal scrollback
            return next.length > 300 ? next.slice(-300) : next;
        });
        setResponsePreview('');
    }, []);
    // Permission dialog key handler — captures y/n/a when dialog is visible.
    // ink 6.x: useInput handlers all fire regardless of TextInput focus prop,
    // so we handle here AND block TextInput onChange (see focused prop below).
    useInput((ch, _key) => {
        if (!permissionRequest)
            return;
        // Clear any character that leaked into the text input
        setInput('');
        const c = ch.toLowerCase();
        if (c === 'y') {
            const r = permissionRequest.resolve;
            setPermissionRequest(null);
            r('yes');
        }
        else if (c === 'n') {
            const r = permissionRequest.resolve;
            setPermissionRequest(null);
            r('no');
        }
        else if (c === 'a') {
            const r = permissionRequest.resolve;
            setPermissionRequest(null);
            r('always');
        }
    }, { isActive: !!permissionRequest });
    // Key handler for picker + esc + abort
    const isPickerOrEsc = mode === 'model-picker' || (mode === 'input' && ready && !input) || !ready;
    useInput((ch, key) => {
        // Escape during generation → abort current turn (skip if permission dialog open)
        if (key.escape && !ready && !permissionRequest) {
            onAbort();
            showStatus('Aborted', 'warning', 3000);
            setReady(true);
            setWaiting(false);
            setThinking(false);
            return;
        }
        // Esc to quit (only when input is empty and in input mode)
        // In Vim mode: Esc goes to normal mode (handled by VimInput), only quit on Esc in normal mode with empty input
        if (key.escape && mode === 'input' && ready && !input) {
            if (vimEnabled && currentVimMode === 'insert')
                return; // Let VimInput handle Esc → normal
            onExit();
            exit();
            return;
        }
        // Arrow key navigation for model picker
        if (mode !== 'model-picker')
            return;
        if (key.upArrow)
            setPickerIdx(i => Math.max(0, i - 1));
        else if (key.downArrow)
            setPickerIdx(i => Math.min(PICKER_MODELS_FLAT.length - 1, i + 1));
        else if (key.return) {
            const selected = PICKER_MODELS_FLAT[pickerIdx];
            setCurrentModel(selected.id);
            onModelChange(selected.id, 'user');
            showStatus(`Model → ${selected.label}`, 'success', 3000);
            // Clear any stale draft that was in the input when the picker opened —
            // previously a paste/typed value could leak back into the chat box after
            // the picker closed, which is both confusing and a privacy risk.
            setInput('');
            setHistoryIdx(-1);
            setMode('input');
            setReady(true);
        }
        else if (key.escape) {
            setInput('');
            setHistoryIdx(-1);
            setMode('input');
            setReady(true);
        }
    }, { isActive: isPickerOrEsc });
    // Tab key: toggle expand/collapse on the last completed tool
    useInput((_ch, key) => {
        if (key.tab && expandableTool) {
            setExpandableTool(prev => prev ? { ...prev, expanded: !prev.expanded } : null);
        }
    }, { isActive: mode === 'input' && !permissionRequest && !askUserRequest });
    // Input history: Up/Down arrow when in ready input mode
    useInput((_ch, key) => {
        if (key.upArrow && inputHistory.length > 0) {
            const newIdx = historyIdx < 0 ? inputHistory.length - 1 : Math.max(0, historyIdx - 1);
            setHistoryIdx(newIdx);
            setInput(inputHistory[newIdx]);
        }
        else if (key.downArrow) {
            if (historyIdx >= 0 && historyIdx < inputHistory.length - 1) {
                const newIdx = historyIdx + 1;
                setHistoryIdx(newIdx);
                setInput(inputHistory[newIdx]);
            }
            else {
                setHistoryIdx(-1);
                setInput('');
            }
        }
    }, { isActive: ready && mode === 'input' });
    const handleSubmit = useCallback((value) => {
        const trimmed = value.trim();
        if (!trimmed)
            return;
        // Exit commands bypass the busy-queue gate — if the user wants out,
        // they want out immediately, not after whatever turn is in flight.
        // Covers bare 'exit' / 'quit' / 'q' and slash-prefixed /exit / /quit.
        const lower = trimmed.toLowerCase();
        const isExit = lower === 'exit' || lower === 'quit' || lower === 'q' ||
            lower === '/exit' || lower === '/quit';
        if (isExit) {
            onAbort();
            onExit();
            exit();
            return;
        }
        // If agent is busy, queue the message — it will be auto-submitted when the turn finishes
        if (!ready) {
            setQueuedInputs(prev => [...prev, trimmed]);
            setInput('');
            showStatus(`Queued message (${queuedInputsRef.current.length + 1} pending)`, 'warning', 1500);
            return;
        }
        // ── Slash commands ──
        if (trimmed.startsWith('/')) {
            setInput('');
            setShowHelp(false);
            setShowWallet(false);
            const parts = trimmed.split(/\s+/);
            const cmd = parts[0].toLowerCase();
            switch (cmd) {
                // /exit and /quit are handled earlier (before the busy-queue gate)
                // so they exit the session immediately even mid-turn.
                case '/model':
                case '/models':
                    if (parts[1]) {
                        const resolved = resolveModel(parts[1]);
                        setCurrentModel(resolved);
                        onModelChange(resolved, 'user');
                        showStatus(`Model → ${resolved}`, 'success', 3000);
                    }
                    else {
                        const idx = PICKER_MODELS_FLAT.findIndex(m => m.id === currentModel);
                        setPickerIdx(idx >= 0 ? idx : 0);
                        // Defensive: ensure no draft text survives into the picker —
                        // closing handlers clear input too, so both ends are covered.
                        setInput('');
                        setHistoryIdx(-1);
                        setMode('model-picker');
                    }
                    return;
                case '/wallet':
                case '/balance':
                    setShowWallet(true);
                    setShowHelp(false);
                    return;
                case '/cost':
                case '/usage':
                    showStatus(`Cost: $${totalCost.toFixed(4)} this session`, 'success', 4000);
                    return;
                case '/help':
                    setShowHelp(true);
                    setShowWallet(false);
                    return;
                case '/vim':
                    setVimEnabled(prev => !prev);
                    showStatus(vimEnabled ? 'Vim mode OFF' : 'Vim mode ON — Esc for normal, i for insert', 'success', 3000);
                    return;
                case '/clear':
                    setStreamText('');
                    setTools(new Map());
                    setTurnTokens({ input: 0, output: 0, calls: 0 });
                    turnCostRef.current = 0;
                    turnModelRef.current = undefined;
                    turnTierRef.current = undefined;
                    turnSavingsRef.current = undefined;
                    turnCtxPctRef.current = undefined;
                    setWaiting(true);
                    setReady(false);
                    // Pass through to agent loop to clear the actual conversation history
                    onSubmit('/clear');
                    return;
                case '/retry':
                    if (!lastPrompt) {
                        showStatus('No previous prompt to retry', 'warning', 3000);
                        return;
                    }
                    setStreamText('');
                    setThinking(false);
                    setThinkingText('');
                    setTools(new Map());
                    setReady(false);
                    setWaiting(true);
                    setTurnTokens({ input: 0, output: 0, calls: 0 });
                    turnCostRef.current = 0;
                    turnModelRef.current = undefined;
                    turnTierRef.current = undefined;
                    turnSavingsRef.current = undefined;
                    turnCtxPctRef.current = undefined;
                    onSubmit(lastPrompt);
                    return;
                default:
                    // All other slash commands pass through to the agent loop's command registry
                    setStreamText('');
                    setThinking(false);
                    setThinkingText('');
                    setTools(new Map());
                    setWaiting(true);
                    setReady(false);
                    onSubmit(trimmed);
                    return;
            }
        }
        // ── Normal prompt ──
        // Show user message in scrollback so the conversation is readable
        setCommittedResponses(rs => [...rs, {
                key: `user-${Date.now()}`,
                // Gold matches the top of the Franklin banner gradient (#FFD700).
                // Brand-consistent, readable on dark terminals, evokes $100-bill identity.
                text: chalk.hex('#FFD700').bold('❯ ') + chalk.hex('#FFD700').bold(trimmed),
                tokens: { input: 0, output: 0, calls: 0 },
                cost: 0,
            }]);
        setResponsePreview('');
        setLastPrompt(trimmed);
        setInputHistory(prev => [...prev.slice(-49), trimmed]); // Keep last 50
        setHistoryIdx(-1);
        setInput('');
        setStreamText('');
        setThinking(false);
        setThinkingText('');
        setTools(new Map());
        // Flush expandable tool to Static before clearing
        setExpandableTool(prev => {
            if (prev)
                setCompletedTools(prev2 => [...prev2, { ...prev, expanded: false }]);
            return null;
        });
        setCompletedTools([]);
        setReady(false);
        setWaiting(true);
        setStatusMsg('');
        setShowHelp(false);
        setShowWallet(false);
        setTurnTokens({ input: 0, output: 0, calls: 0 });
        turnCostRef.current = 0;
        turnModelRef.current = undefined;
        turnTierRef.current = undefined;
        turnSavingsRef.current = undefined;
        turnCtxPctRef.current = undefined;
        onSubmit(trimmed);
    }, [ready, currentModel, totalCost, onSubmit, onModelChange, onAbort, onExit, exit, lastPrompt, inputHistory, showStatus]);
    // Mouse support — OFF by default because Node stdin is shared: mouse escape
    // sequences leak into Ink's input handler as typed text. Opt in with
    // FRANKLIN_MOUSE=1 if you want click-to-expand-tool + drag-to-copy.
    useEffect(() => {
        // Always disable any leftover mouse tracking from a previous session
        forceDisableMouseTracking();
        if (process.env.FRANKLIN_MOUSE !== '1')
            return;
        const cleanup = mouse.enable();
        const handleClick = (event) => {
            // Ignore clicks in the input area (bottom 4 rows of the terminal)
            const termRows = process.stdout.rows ?? 24;
            if (event.row >= termRows - 4)
                return;
            // Click: toggle expandable tool
            setExpandableTool(prev => prev ? { ...prev, expanded: !prev.expanded } : null);
        };
        const handleCopied = (info) => {
            // Show status when text is copied via drag-select
            showStatus(`Copied ${info.length} chars to clipboard`, 'success', 2000);
        };
        mouse.on('click', handleClick);
        mouse.on('copied', handleCopied);
        return () => {
            mouse.removeListener('click', handleClick);
            mouse.removeListener('copied', handleCopied);
            cleanup();
        };
    }, []);
    // Expose event handler, balance updater, and permission bridge
    useEffect(() => {
        globalThis.__franklin_ui = {
            updateModel: (model) => { setCurrentModel(model); },
            updateBalance: (bal) => {
                setBalance(bal);
                const num = parseBalanceNum(bal);
                if (num !== null) {
                    setBaseBalanceNum(num);
                    // Reset cost baseline — the fetched balance already reflects costs up to this point
                    setCostAtLastFetch(totalCostRef.current);
                }
            },
            onTurnDone: (cb) => { turnDoneCallbackRef.current = cb; },
            requestPermission: (toolName, description) => {
                return new Promise((resolve) => {
                    // Ring the terminal bell — causes tab to show notification badge in iTerm2/Terminal.app
                    process.stderr.write('\x07');
                    setPermissionRequest({ toolName, description, resolve });
                });
            },
            requestAskUser: (question, options) => {
                return new Promise((resolve) => {
                    process.stderr.write('\x07');
                    setAskUserInput('');
                    setAskUserRequest({ question, options, resolve });
                });
            },
            handleEvent: (event) => {
                switch (event.kind) {
                    case 'text_delta':
                        // Throttled: accumulate in ref, flush every 50ms (~20fps)
                        pendingTextRef.current += event.text;
                        scheduleFlush();
                        break;
                    case 'thinking_delta':
                        // Throttled: accumulate in ref, flush every 50ms
                        pendingThinkingRef.current += event.text;
                        scheduleFlush();
                        break;
                    case 'capability_start':
                        setWaiting(false);
                        setTools(prev => {
                            const next = new Map(prev);
                            next.set(event.id, {
                                name: event.name, startTime: Date.now(),
                                done: false, error: false,
                                preview: event.preview || '',
                                liveOutput: '',
                                liveLines: [],
                                fullOutput: '',
                                expanded: false,
                                elapsed: 0,
                            });
                            return next;
                        });
                        break;
                    case 'capability_progress':
                        setTools(prev => {
                            const t = prev.get(event.id);
                            if (!t || t.done)
                                return prev;
                            const next = new Map(prev);
                            // Accumulate output lines for multi-line display (keep last 5)
                            const newLines = [...t.liveLines];
                            const incoming = event.text.split('\n').filter(Boolean);
                            newLines.push(...incoming);
                            while (newLines.length > 5)
                                newLines.shift();
                            next.set(event.id, { ...t, liveOutput: event.text, liveLines: newLines });
                            return next;
                        });
                        break;
                    case 'capability_done': {
                        setTools(prev => {
                            const next = new Map(prev);
                            const t = next.get(event.id);
                            if (t) {
                                // On success: show input preview (command/path). On error: show error output.
                                const resultPreview = event.result.isError
                                    ? event.result.output.replace(/\n/g, ' ').slice(0, 150)
                                    : (t.preview || event.result.output.replace(/\n/g, ' ').slice(0, 120));
                                const completed = {
                                    ...t,
                                    key: event.id,
                                    done: true,
                                    error: !!event.result.isError,
                                    preview: resultPreview,
                                    liveOutput: '',
                                    liveLines: [],
                                    fullOutput: event.result.output || '',
                                    diff: event.result.diff,
                                    expanded: false,
                                    elapsed: Date.now() - t.startTime,
                                };
                                // Move previous expandable tool to Static, set new one as expandable
                                setExpandableTool(prevExpTool => {
                                    if (prevExpTool) {
                                        setCompletedTools(prev2 => [...prev2, { ...prevExpTool, expanded: false }]);
                                    }
                                    return completed;
                                });
                                next.delete(event.id);
                            }
                            return next;
                        });
                        break;
                    }
                    case 'usage': {
                        // DO NOT setCurrentModel(event.model) here. currentModel
                        // represents the user's selection (e.g. 'blockrun/auto'),
                        // not what the router resolved for this specific turn. The
                        // per-turn resolved model is already captured in
                        // turnModelRef (rendered in the turn-summary line below
                        // each response) and in onModelChange('system') when the
                        // loop itself decides to swap (empty-response / 402 fallback).
                        // Overriding currentModel from every usage event made the
                        // status bar permanently show the last resolved model and
                        // create a false impression that auto mode was stuck.
                        setTurnTokens(prev => ({
                            input: prev.input + event.inputTokens,
                            output: prev.output + event.outputTokens,
                            calls: prev.calls + (event.calls ?? 1),
                        }));
                        const turnCallCost = estimateCost(event.model, event.inputTokens, event.outputTokens, event.calls ?? 1);
                        turnCostRef.current += turnCallCost;
                        setTotalCost(prev => prev + turnCallCost);
                        // Capture routing metadata for this turn
                        turnModelRef.current = event.model;
                        if (event.tier)
                            turnTierRef.current = event.tier;
                        if (event.savings !== undefined)
                            turnSavingsRef.current = event.savings;
                        if (event.contextPct !== undefined) {
                            setContextPct(event.contextPct);
                            turnCtxPctRef.current = event.contextPct;
                        }
                        break;
                    }
                    case 'turn_done': {
                        // Flush any pending throttled text immediately
                        if (flushTimerRef.current) {
                            clearTimeout(flushTimerRef.current);
                            flushTimerRef.current = null;
                        }
                        // Merge pending text into the ref so commitResponse sees the full text
                        if (pendingTextRef.current) {
                            streamTextRef.current += pendingTextRef.current;
                            pendingTextRef.current = '';
                        }
                        pendingThinkingRef.current = '';
                        // Freeze reasoning meter if turn ended while thinking (no text emitted)
                        if (thinkStartRef.current !== null && thinkMsRef.current === null) {
                            thinkMsRef.current = Date.now() - thinkStartRef.current;
                        }
                        // Flush expandable tool to Static before committing response
                        setExpandableTool(prev => {
                            if (prev)
                                setCompletedTools(prev2 => [...prev2, { ...prev, expanded: false }]);
                            return null;
                        });
                        const text = streamTextRef.current;
                        if (text.trim()) {
                            commitResponse(text, turnTokensRef.current, turnCostRef.current);
                            setStreamText('');
                        }
                        if (event.reason === 'error' && event.error) {
                            commitResponse(formatAgentErrorForDisplay(event.error), turnTokensRef.current, turnCostRef.current);
                            showStatus('Turn failed', 'error', 5000);
                        }
                        else if (event.reason === 'aborted') {
                            showStatus('Aborted', 'warning', 3000);
                        }
                        else if (event.reason === 'max_turns') {
                            showStatus('Stopped after reaching max turns', 'warning', 5000);
                        }
                        else {
                            setStatusMsg('');
                        }
                        setReady(true);
                        setWaiting(false);
                        setThinking(false);
                        setThinkingText('');
                        // Reset reasoning meter for the next turn
                        thinkStartRef.current = null;
                        thinkMsRef.current = null;
                        thinkCharsRef.current = 0;
                        // Trigger balance refresh after each completed turn
                        turnDoneCallbackRef.current?.();
                        // Ring the terminal bell so the user knows the AI finished
                        // (shows notification badge in iTerm2/Terminal.app when tabbed away)
                        process.stderr.write('\x07');
                        // Auto-submit any queued message while agent was busy
                        const queued = queuedInputsRef.current[0];
                        if (queued) {
                            setQueuedInputs((prev) => prev.slice(1));
                            // Small delay so React can flush the ready=true state first
                            setTimeout(() => {
                                const fn = globalThis.__franklin_submit;
                                if (typeof fn === 'function')
                                    fn(queued);
                            }, 50);
                        }
                        break;
                    }
                }
            },
        };
        globalThis.__franklin_submit = (msg) => {
            handleSubmit(msg);
        };
        return () => {
            delete globalThis.__franklin_ui;
            delete globalThis.__franklin_submit;
        };
    }, [handleSubmit, commitResponse, showStatus]);
    // ── Render ──
    // Note: the tree is ALWAYS the same shape across mode changes. Static
    // components (completedTools, committedResponses) stay mounted so Ink
    // doesn't discard already-committed scrollback when the model picker
    // opens/closes. The picker is rendered inline below scrollback, and the
    // InputBox is hidden while it's active.
    const inPicker = mode === 'model-picker';
    return (_jsxs(Box, { flexDirection: "column", children: [statusMsg && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: statusTone === 'error' ? 'red' : statusTone === 'warning' ? 'yellow' : 'green', children: statusMsg }) })), showHelp && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Commands" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/model" }), " [name]  Switch model (picker if no name)"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/wallet" }), "        Show wallet address & balance"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/cost" }), "          Session cost & savings"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/retry" }), "         Retry the last prompt"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/compact" }), "       Compress conversation history"] }), _jsx(Text, { dimColor: true, children: "  \u2500\u2500 Coding \u2500\u2500" }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/test" }), "          Run tests"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/fix" }), "           Fix last error"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/review" }), "        Code review"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/explain" }), " file  Explain code"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/search" }), " query  Search codebase"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/session-search" }), " q  Search past sessions"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/refactor" }), " desc Refactor code"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/scaffold" }), " desc Generate boilerplate"] }), _jsx(Text, { dimColor: true, children: "  \u2500\u2500 Git \u2500\u2500" }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/commit" }), "        Commit changes"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/push" }), "          Push to remote"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/pr" }), "            Create pull request"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/status" }), "        Git status"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/diff" }), "          Git diff"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/log" }), "           Git log"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/branch" }), " [name] Branches"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/stash" }), "         Stash changes"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/undo" }), "          Undo last commit"] }), _jsx(Text, { dimColor: true, children: "  \u2500\u2500 Analysis \u2500\u2500" }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/security" }), "      Security audit"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/lint" }), "          Quality check"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/optimize" }), "      Performance check"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/todo" }), "          Find TODOs"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/deps" }), "          Dependencies"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/clean" }), "         Dead code removal"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/context" }), "       Session info (model, tokens, mode)"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/plan" }), "          Enter plan mode (read-only tools)"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/execute" }), "       Exit plan mode (enable all tools)"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/sessions" }), "      List saved sessions"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/resume" }), " id     Resume a saved session"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/clear" }), "         Clear conversation history"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/doctor" }), "        Diagnose setup issues"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/vim" }), "           Toggle Vim input mode"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/help" }), "          This help"] }), _jsxs(Text, { children: ["  ", _jsx(Text, { color: "cyan", children: "/exit" }), "          Quit"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "  Shortcuts: sonnet, opus, gpt, gemini, deepseek, flash, free, r1, o4, nano, mini, haiku" })] })), showWallet && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Wallet" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["  Chain:   ", _jsx(Text, { color: "magenta", children: chain })] }), _jsxs(Text, { children: ["  Address: ", _jsx(Text, { color: "cyan", children: walletAddress })] }), _jsxs(Text, { children: ["  Balance: ", _jsx(Text, { color: "green", children: balance })] })] })), _jsx(Static, { items: completedTools, children: (tool) => {
                    const elapsedFmt = tool.elapsed >= 1000
                        ? `${(tool.elapsed / 1000).toFixed(1)}s`
                        : `${tool.elapsed}ms`;
                    return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { children: [tool.error
                                        ? _jsx(Text, { color: "red", children: "\u2717" })
                                        : _jsx(Text, { color: "green", children: "\u2713" }), ' ', _jsx(Text, { bold: true, children: tool.name }), tool.preview ? _jsxs(Text, { dimColor: true, children: ["(", tool.preview.slice(0, 80), ")"] }) : null, _jsxs(Text, { dimColor: true, children: [" ", elapsedFmt] })] }), tool.diff && !tool.error && tool.diff.oldLines.length <= 8 && tool.diff.newLines.length <= 8 && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [tool.diff.oldLines.map((line, i) => (_jsxs(Text, { color: "red", wrap: "truncate-end", children: ['⎿  ', "- ", line.slice(0, 120)] }, `old-${i}`))), tool.diff.newLines.map((line, i) => (_jsxs(Text, { color: "green", wrap: "truncate-end", children: ['⎿  ', "+ ", line.slice(0, 120)] }, `new-${i}`)))] })), tool.diff && !tool.error && (tool.diff.oldLines.length > 8 || tool.diff.newLines.length > 8) && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ['⎿  ', tool.diff.oldLines.length, " lines \u2192 ", tool.diff.newLines.length, " lines"] }) })), tool.error && tool.fullOutput && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: tool.fullOutput.split('\n').filter(Boolean).slice(0, 3).map((line, i) => (_jsxs(Text, { color: "red", wrap: "truncate-end", children: ['⎿  ', line.slice(0, 120)] }, i))) }))] }, tool.key));
                } }), _jsx(Static, { items: committedResponses, children: (r) => {
                    const isUserMsg = r.key.startsWith('user-');
                    return (_jsxs(Box, { flexDirection: "column", children: [!isUserMsg && (r.tokens.input > 0 || r.tokens.output > 0) && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(60) }) })), isUserMsg && (_jsx(Box, { marginTop: 1 })), !isUserMsg && r.thinkMs !== undefined && r.thinkMs >= 500 && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: "magenta", dimColor: true, children: ["\u273B Thought for ", (r.thinkMs / 1000).toFixed(1), "s", r.thinkChars && r.thinkChars > 20
                                            ? ` · ~${Math.round(r.thinkChars / 4)} tokens`
                                            : ''] }) })), _jsx(Box, { paddingLeft: isUserMsg ? 0 : 2, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(r.text) }) }), (r.tokens.input > 0 || r.tokens.output > 0) && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [r.tier
                                            ? _jsxs(Text, { color: "cyan", children: ["[", r.tier, "] "] })
                                            : (r.model ? _jsx(Text, { dimColor: true, children: "[direct] " }) : null), r.model ? shortModelName(r.model) : '', r.model ? '  ·  ' : '', r.tokens.calls > 0 && r.tokens.input === 0
                                            ? `${r.tokens.calls} calls`
                                            : `${formatTokens(r.tokens.input)} in / ${formatTokens(r.tokens.output)} out`, r.cost > 0 ? `  ·  $${r.cost.toFixed(4)}` : '', r.savings !== undefined && r.savings > 0 ? _jsxs(Text, { color: "green", children: ["  saved ", Math.round(r.savings * 100), "%"] }) : '', r.ctxPct !== undefined && r.ctxPct >= 5
                                            ? _jsxs(Text, { color: r.ctxPct >= 80 ? 'red' : r.ctxPct >= 50 ? 'yellow' : undefined, dimColor: r.ctxPct < 50, children: ["  \u00B7  ctx ", r.ctxPct, "%"] })
                                            : ''] }) }))] }, r.key));
                } }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "red", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0  ACTION REQUIRED  \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "yellow", children: "\u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: ["\u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes  " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always  " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsx(Text, { color: "magenta", bold: true, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u26A0  ANSWER REQUIRED  \u26A0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }), _jsx(Text, { color: "cyan", children: "\u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: ["\u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: ["\u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
                                    const answer = val.trim() || '(no response)';
                                    const r = askUserRequest.resolve;
                                    setAskUserRequest(null);
                                    setAskUserInput('');
                                    r(answer);
                                }, focus: true })] })] })), expandableTool && !permissionRequest && !askUserRequest && (() => {
                const tool = expandableTool;
                const elapsedFmt = tool.elapsed >= 1000
                    ? `${(tool.elapsed / 1000).toFixed(1)}s`
                    : `${tool.elapsed}ms`;
                const hasExpandableContent = !!(tool.diff || (tool.fullOutput && tool.fullOutput.split('\n').length > 1));
                return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { children: [tool.error ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "green", children: "\u2713" }), ' ', _jsx(Text, { bold: true, children: tool.name }), tool.preview ? _jsxs(Text, { dimColor: true, children: ["(", tool.preview.slice(0, 80), ")"] }) : null, _jsxs(Text, { dimColor: true, children: [" ", elapsedFmt] }), hasExpandableContent && (_jsxs(Text, { dimColor: true, children: [" ", tool.expanded ? '(tab to collapse)' : '(tab to expand)'] }))] }), !tool.expanded && tool.diff && !tool.error && tool.diff.oldLines.length <= 8 && tool.diff.newLines.length <= 8 && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [tool.diff.oldLines.map((line, i) => (_jsxs(Text, { color: "red", wrap: "truncate-end", children: ['⎿  ', "- ", line.slice(0, 120)] }, `old-${i}`))), tool.diff.newLines.map((line, i) => (_jsxs(Text, { color: "green", wrap: "truncate-end", children: ['⎿  ', "+ ", line.slice(0, 120)] }, `new-${i}`)))] })), tool.expanded && tool.fullOutput && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [tool.fullOutput.split('\n').slice(0, 30).map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿  ', line.slice(0, 120)] }, i))), tool.fullOutput.split('\n').length > 30 && (_jsxs(Text, { dimColor: true, children: ['⎿  ', "... ", tool.fullOutput.split('\n').length - 30, " more lines"] }))] })), tool.error && !tool.expanded && tool.fullOutput && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: tool.fullOutput.split('\n').filter(Boolean).slice(0, 3).map((line, i) => (_jsxs(Text, { color: "red", wrap: "truncate-end", children: ['⎿  ', line.slice(0, 120)] }, i))) }))] }));
            })(), !permissionRequest && !askUserRequest && Array.from(tools.entries()).map(([id, tool]) => {
                const elapsed = Math.round((Date.now() - tool.startTime) / 1000);
                const elapsedStr = elapsed > 0 ? ` ${elapsed}s` : '';
                return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), ' ', _jsx(Text, { bold: true, color: "cyan", children: tool.name }), tool.preview ? _jsxs(Text, { dimColor: true, children: ["(", tool.preview.slice(0, 70), ")"] }) : null, _jsx(Text, { dimColor: true, children: elapsedStr })] }), tool.liveLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: tool.liveLines.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿  ', line.slice(0, 120)] }, i))) }))] }, id));
            }), thinking && !permissionRequest && !askUserRequest && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "magenta", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsx(Text, { bold: true, children: "thinking" }), completedTools.length > 0 ? _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 step ", completedTools.length + 1] }) : null] }), process.env.FRANKLIN_SHOW_THINKING === '1' && thinkingText && (() => {
                        const lines = thinkingText.split('\n').filter(Boolean).slice(-3);
                        return (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: lines.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿  ', line.slice(0, 120)] }, i))) }));
                    })()] })), waiting && !thinking && tools.size === 0 && !permissionRequest && !askUserRequest && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsxs(Text, { dimColor: true, children: [shortModelName(currentModel), completedTools.length > 0 ? ` · step ${completedTools.length + 1}` : ''] })] }) })), streamText && !permissionRequest && !askUserRequest && (() => {
                const maxLines = Math.max(8, termRows - 12);
                const lines = streamText.split('\n');
                const truncated = lines.length > maxLines;
                const visible = truncated ? lines.slice(-maxLines).join('\n') : streamText;
                const { rendered, partial } = renderMarkdownStreaming(visible);
                return (_jsxs(Box, { flexDirection: "column", marginTop: 0, marginBottom: 0, marginLeft: 2, children: [truncated && (_jsxs(Text, { dimColor: true, children: ["\u2191 ", lines.length - maxLines, " earlier line", lines.length - maxLines === 1 ? '' : 's', " \u2014 full response will appear in scrollback when this turn finishes"] })), _jsxs(Text, { wrap: "wrap", children: [rendered, rendered && partial ? '\n' : '', partial] })] }));
            })(), responsePreview && !streamText && !permissionRequest && !askUserRequest && (_jsx(Box, { flexDirection: "column", marginBottom: 0, marginLeft: 2, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(responsePreview) }) })), inPicker && (() => {
                const totalModels = PICKER_MODELS_FLAT.length;
                const maxModels = Math.max(6, termRows - 12);
                let start = Math.max(0, pickerIdx - Math.floor(maxModels / 2));
                let end = Math.min(totalModels, start + maxModels);
                // Expand window backward if we hit the bottom of the list, so we
                // always fill `maxModels` rows when the list is long enough.
                if (end - start < maxModels)
                    start = Math.max(0, end - maxModels);
                const hiddenAbove = start;
                const hiddenBelow = totalModels - end;
                // Pre-compute each category's base offset into the flat model list so
                // we can map (cat, localIdx) → globalIdx in one pass without re-walking.
                let cursor = 0;
                const catBases = PICKER_CATEGORIES.map((cat) => {
                    const base = cursor;
                    cursor += cat.models.length;
                    return base;
                });
                return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "Select a model " }), _jsx(Text, { dimColor: true, children: "(\u2191\u2193 navigate, Enter select, Esc cancel)" })] }), hiddenAbove > 0 && (_jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\u2191 ", hiddenAbove, " more above"] }) })), PICKER_CATEGORIES.map((cat, catIdx) => {
                            const base = catBases[catIdx];
                            const visible = cat.models
                                .map((m, localIdx) => ({ m, globalIdx: base + localIdx }))
                                .filter(({ globalIdx }) => globalIdx >= start && globalIdx < end);
                            if (visible.length === 0)
                                return null;
                            return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2500\u2500 ", cat.category, " \u2500\u2500"] }) }), visible.map(({ m, globalIdx }) => {
                                        const isSelected = globalIdx === pickerIdx;
                                        const isCurrent = m.id === currentModel;
                                        const isHighlight = m.highlight === true;
                                        return (_jsxs(Box, { marginLeft: 2, children: [_jsxs(Text, { inverse: isSelected, color: isSelected ? 'cyan' : isHighlight ? 'yellow' : undefined, bold: isSelected || isHighlight, children: [' ', m.label.padEnd(26), ' '] }), _jsxs(Text, { dimColor: true, children: [" ", m.shortcut.padEnd(14)] }), _jsx(Text, { color: m.price === 'FREE' ? 'green' : isHighlight ? 'yellow' : undefined, dimColor: !isHighlight && m.price !== 'FREE', children: m.price }), isCurrent && _jsx(Text, { color: "green", children: " \u2190" })] }, m.id));
                                    })] }, cat.category));
                        }), hiddenBelow > 0 && (_jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\u2193 ", hiddenBelow, " more below"] }) })), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(Text, { dimColor: true, children: "Your conversation stays above \u2014 picking a model keeps all history intact." }) })] }));
            })(), !inPicker && !permissionRequest && !askUserRequest && (_jsx(InputBox, { input: input, setInput: setInput, onSubmit: handleSubmit, model: currentModel, balance: liveBalance, chain: chain, walletTail: walletAddress && walletAddress.length >= 4 && !walletAddress.startsWith('not set') ? walletAddress.slice(-4) : undefined, sessionCost: totalCost, queued: queuedInputs[0] || undefined, queuedCount: queuedInputs.length, focused: !permissionRequest && !askUserRequest, busy: !askUserRequest && (waiting || thinking || tools.size > 0), awaitingApproval: !!permissionRequest, awaitingAnswer: !!askUserRequest, contextPct: contextPct, vimMode: vimEnabled, onVimModeChange: setCurrentVimMode }))] }));
}
export function launchInkUI(opts) {
    let resolveInput = null;
    let pendingInput = null; // Queue for inputs that arrive before waitForInput
    let exiting = false;
    let abortCallback = null;
    const restoreTerminalAutoWrap = disableTerminalAutoWrap();
    const instance = render(_jsx(RunCodeApp, { initialModel: opts.model, workDir: opts.workDir, walletAddress: opts.walletAddress || 'not set — run: franklin setup', walletBalance: opts.walletBalance || 'unknown', chain: opts.chain || 'base', startWithPicker: opts.showPicker, onSubmit: (value) => {
            if (resolveInput) {
                resolveInput(value);
                resolveInput = null;
            }
            else {
                // Agent loop hasn't called waitForInput yet — queue the input
                pendingInput = value;
            }
        }, onModelChange: (model, reason) => { opts.onModelChange?.(model, reason); }, onAbort: () => { abortCallback?.(); }, onExit: () => {
            exiting = true;
            if (resolveInput) {
                resolveInput(null);
                resolveInput = null;
            }
        } }));
    return {
        handleEvent: (event) => {
            const ui = globalThis.__franklin_ui;
            ui?.handleEvent(event);
        },
        updateModel: (model) => {
            const ui = globalThis.__franklin_ui;
            ui?.updateModel(model);
        },
        updateBalance: (bal) => {
            const ui = globalThis.__franklin_ui;
            ui?.updateBalance(bal);
        },
        onTurnDone: (cb) => {
            const ui = globalThis.__franklin_ui;
            ui?.onTurnDone(cb);
        },
        waitForInput: () => {
            if (exiting)
                return Promise.resolve(null);
            // If user already submitted while we were processing, return immediately
            if (pendingInput !== null) {
                const input = pendingInput;
                pendingInput = null;
                return Promise.resolve(input);
            }
            return new Promise((resolve) => { resolveInput = resolve; });
        },
        onAbort: (cb) => { abortCallback = cb; },
        cleanup: () => {
            mouse.disable();
            instance.unmount();
            restoreTerminalAutoWrap?.();
        },
        requestPermission: (toolName, description) => {
            const ui = globalThis.__franklin_ui;
            return ui?.requestPermission(toolName, description) ?? Promise.resolve('no');
        },
        requestAskUser: (question, options) => {
            const ui = globalThis.__franklin_ui;
            return ui?.requestAskUser(question, options) ?? Promise.resolve('(no response)');
        },
    };
}
