import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
 * ============================================================
 *  src/ui/App.tsx —— TUI 顶层组件
 * ------------------------------------------------------------
 *  组合所有 UI 子组件，把 useChat 暴露的状态映射到各组件 props。
 *  不直接处理键盘输入，那是 InputBox 的事。
 * ============================================================
 */
import React, { useDeferredValue } from 'react';
import { Box, Text } from 'ink';
import { getWorkingDir } from '../bootstrap/workingDir.js';
import { listArchives, restoreArchive } from '../context/archive.js';
import { saveContext } from '../context/persistContext.js';
import { sessionFileFor } from '../context/sessionPath.js';
import { listSessions } from '../context/sessionRegistry.js';
import { AskQuestion } from './AskQuestion.js';
import { InputBox } from './InputBox.js';
import { LiveArea } from './LiveArea.js';
import { MessageList } from './MessageList.js';
import { StatusLine } from './StatusLine.js';
import { useChat } from './hooks/useChat.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
/**
 * ANSI escape: 清屏 + 光标归位。
 *
 * 注意：项目里仅这两处保留 process.stdout.write 旁路 Ink：
 *   1) onCompactDone（压缩完）
 *   2) /new 清空历史后
 * CLEAR_SCREEN 是**一次性**写入 + 整屏清空的语义，不存在"反复写新内容到 live 区"
 * 的问题；ESC[2J 清屏后 ink 下次 onRender 会重发完整 lastOutput，lineCount 残留
 * 也不会留下脏行（写在空白处）。其它"用户看到的非消息内容"必须走 chat.ephemeralLine
 * 走 Ink 渲染管线，否则会破坏 log-update 计数。
 */
const CLEAR_SCREEN = '\x1b[2J\x1b[H';
export function App({ provider, initialHistory }) {
    // v2 multi-session：持久化路径完全由 useChat 内部管控（按 activeSessionId 选文件）。
    // App 不再传 onPersist —— 之前 App 端调 saveContext(messages) 会污染 default session。
    const chat = useChat({
        provider,
        initialHistory,
        onCompactDone: () => process.stdout.write(CLEAR_SCREEN),
    });
    // 终端尺寸感知：rows → MessageList 决定窄终端是否折叠 reasoning + LiveArea 收敛预览高；
    //                cols → LiveArea 逐行截断防换行
    const { rows: terminalRows, cols: terminalCols } = useTerminalSize();
    /**
     * 用 useDeferredValue 把 history 推后到低优先级渲染队列。
     *
     * 流式期间 transient state 高频 setState 是高优先级，
     * 而 MessageList 重算 turn meta + Static reconcile 比较重，可以"等空闲再做"。
     * React 在繁忙时会让 deferredHistory 保持旧值跳过 MessageList rerender，
     * 直到 streamingText 稳定下来再 commit 新 history。
     *
     * 注意：assistant_message 落定时 history 增长 + streamingText 清空是同一次
     * runQuery yield —— useDeferredValue 不会无限延迟，最终一定会 catch up，
     * Static commit 新 message 进 scrollback 不会丢。
     */
    const deferredHistory = useDeferredValue(chat.history);
    // /new：内存 reset + 磁盘 clearContext 全部由 chat.clearHistory() 内部完成，await 之后再清屏。
    // ★ deps 用精细字段 chat.clearHistory，不是整个 chat。
    //   理由：useChat 返回的 chat 对象每次 useChat 重渲都是新对象字面量引用。
    //   如果 deps=[chat]，流式期 reasoningPreview/streamingText 每 100ms flush 触发
    //   useChat rerender → chat 新引用 → handleClear 新引用 → InputBox 收到新 onClear
    //   prop → 强制重渲 → 整个 live 区被 Ink 重写一次（看起来就是 spinner 每帧打新行）。
    const handleClear = React.useCallback(async () => {
        await chat.clearHistory();
        process.stdout.write(CLEAR_SCREEN);
    }, [chat.clearHistory]);
    // history ref：在 handleSubmit 闭包里读最新 chat.history，避免每次流式 flush
    // 重建 callback；同时避免把 chat.history 放进 deps 导致 InputBox 频繁 rerender。
    const historyRef = React.useRef(chat.history);
    React.useEffect(() => {
        historyRef.current = chat.history;
    }, [chat.history]);
    /**
     * v2 slash 命令拦截层。
     *
     * InputBox 已经拦截了 /exit /quit /new /compact 不走 onSubmit；
     * 其他 / 开头命令落到这里。命中命令走 chat.ephemeralLine() 把提示作为
     * _ephemeral 的 system 消息推进 history → MessageList <Static> 把它 commit
     * 进 scrollback；不调 chat.submit 避免被 LLM 当成用户输入。
     *
     * 旧方案是 process.stdout.write 旁路 Ink，但那样会破坏 ink log-update 的
     * previousLineCount 计数，下一帧 eraseLines 用过期 N 去清会留下脏行。
     *
     * 支持：
     *   /session <name>   — 切到指定 session（同 cwd）
     *   /sessions         — 列当前 cwd 下所有 session
     *   /list-archive     — 列归档
     *   /restore <id>     — 恢复归档为当前 session 历史（覆盖+重读）
     *   /show <id-suffix> — 打印 tool_call_id 末尾匹配的完整 tool result
     */
    const handleSubmit = React.useCallback(async (text) => {
        // /session <name>
        if (text.startsWith('/session ')) {
            const id = text.slice('/session '.length).trim();
            if (id.length === 0) {
                chat.ephemeralLine('[/session] 用法：/session <name>');
                return;
            }
            await chat.switchSession(id);
            chat.ephemeralLine(`[/session] 已切换到 session "${id}"`);
            return;
        }
        // /sessions
        if (text.trim() === '/sessions') {
            const ids = await listSessions(getWorkingDir());
            const lines = ids
                .map((id) => (id === chat.activeSessionId ? `* ${id}` : `  ${id}`))
                .join('\n');
            chat.ephemeralLine(`[/sessions] 当前 cwd 下的 session（* = 激活）：\n${lines}`);
            return;
        }
        // /list-archive
        if (text.trim() === '/list-archive') {
            const entries = await listArchives();
            if (entries.length === 0) {
                chat.ephemeralLine('[/list-archive] 当前 cwd 下无归档。');
                return;
            }
            const lines = entries
                .slice(0, 30)
                .map((e) => `  ${e.id}  ·  ${e.msgCount} msgs  ·  ${e.firstUserPreview || '(empty)'}`)
                .join('\n');
            const tail = entries.length > 30 ? `\n  ...（共 ${entries.length} 条，仅显示前 30）` : '';
            chat.ephemeralLine(`[/list-archive] 归档列表（最新优先）：\n${lines}${tail}`);
            return;
        }
        // /restore <id>
        if (text.startsWith('/restore ')) {
            const id = text.slice('/restore '.length).trim();
            if (id.length === 0) {
                chat.ephemeralLine('[/restore] 用法：/restore <archive-id>');
                return;
            }
            const messages = await restoreArchive(id);
            if (!messages) {
                chat.ephemeralLine(`[/restore] 未找到 archive id "${id}"`);
                return;
            }
            // 覆盖当前 active session 文件；强制 reload 让 history 替换
            const targetFile = sessionFileFor(getWorkingDir(), chat.activeSessionId);
            await saveContext(messages, targetFile);
            await chat.switchSession(chat.activeSessionId, { force: true });
            chat.ephemeralLine(`[/restore] 已恢复 archive ${id} 到 session "${chat.activeSessionId}"（${messages.length} 条消息）`);
            return;
        }
        // /show <id-suffix>
        if (text.startsWith('/show ')) {
            const suffix = text.slice('/show '.length).trim();
            if (suffix.length === 0) {
                chat.ephemeralLine('[/show] 用法：/show <tool_call_id 末尾 8 字符>');
                return;
            }
            const hit = historyRef.current.find((m) => m.role === 'tool' && m.tool_call_id.endsWith(suffix));
            if (!hit || hit.role !== 'tool') {
                chat.ephemeralLine(`[/show] 未找到 tool_call_id 末尾匹配 "${suffix}" 的 tool result`);
                return;
            }
            chat.ephemeralLine(`[tool result · #${hit.tool_call_id.slice(-8)} · 完整 ${hit.content.split('\n').length} 行]\n${hit.content}`);
            return;
        }
        // /plan <目标> —— 便捷别名，等价 /workflow plan（跑 plan 工作流：
        // 调研 → AskUserQuestion 选择 → 产出 ./PLAN.md）。改写后透传给 pluginRunner。
        // 目标里的引号剥掉，避免破坏 workflow 参数 tokenizer（"..." 整体当一个位置参数）。
        if (text.startsWith('/plan ') || text.trim() === '/plan') {
            const goal = text.slice('/plan'.length).trim().replace(/"/g, '');
            if (goal.length === 0) {
                chat.ephemeralLine('[/plan] 用法：/plan <目标>（= /workflow plan：调研→提问选择→产出 ./PLAN.md）');
                return;
            }
            void chat.submit(`/workflow plan "${goal}"`);
            return;
        }
        // 非命令 → 走正常 LLM 流程
        void chat.submit(text);
    }, 
    // ★ 精细 deps：用 chat 的具体方法/字段，**不是**整个 chat 对象。
    //   useChat 每次重渲都返回新的 chat 对象字面量；用 [chat] 会让 handleSubmit
    //   随每次流式 flush（100ms）重建一次 → InputBox 收到新 onSubmit → 强制重渲
    //   → Ink 重写 live 区 → spinner 看起来每帧打新行。
    [
        chat.ephemeralLine,
        chat.switchSession,
        chat.activeSessionId,
        chat.submit,
    ]);
    // banner 改作为 Static items[0] 走 MessageList。原因：Ink 5 的 Static 约定
    // 它必须是兄弟节点中第一个，否则 above-sibling 会跟 Static commit 一起被重打
    // 到 stdout（实测看到 banner 反复打印 N 次）。
    const bannerText = `minimal-agent  · ${provider.name}/${provider.model} · /new 清空 · /compact 压缩 · /exit 退出 · Ctrl+C 中断`;
    return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MessageList, { history: deferredHistory, compactGeneration: chat.compactGeneration, bannerText: bannerText, terminalRows: terminalRows }), _jsxs(Box, { flexDirection: "column", width: Math.max(1, terminalCols - 1), children: [_jsx(LiveArea, { progressStage: chat.progressStage, reasoningPreview: chat.reasoningPreview, streamingText: chat.streamingText, runningTools: chat.runningTools, pluginProgress: chat.pluginProgress, terminalCols: terminalCols, terminalRows: terminalRows }), chat.error && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "red", children: ["\u26A0 ", chat.error] }) })), chat.interrupted && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A1 \u64CD\u4F5C\u88AB\u7528\u6237\u4E2D\u65AD\uFF0C\u7B49\u5F85\u65B0\u7684\u4EFB\u52A1\u8F93\u5165..." }) })), chat.pendingAsk ? (_jsx(AskQuestion, { questions: chat.pendingAsk, onAnswer: chat.answerAsk, onCancel: chat.abort })) : (
                    /* handleSubmit 包装做 slash 命令拦截，非命令透传 chat.submit */
                    _jsx(InputBox, { onSubmit: handleSubmit, disabled: chat.isLoading, onAbort: chat.abort, onClear: handleClear, onCompact: chat.compactNow })), _jsx(StatusLine, { provider: provider, history: chat.history, pluginProgress: chat.pluginProgress })] })] }));
}
