/**
 * Interactive mode for the coding agent.
 * Handles TUI rendering and user interaction, delegating business logic to AgentSession.
 */
import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
import { spawn, spawnSync } from "child_process";
import { APP_NAME, getAgentDir, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
import { parseSkillBlock } from "../../core/agent-session.js";
import { FooterDataProvider } from "../../core/footer-data-provider.js";
import { KeybindingsManager } from "../../core/keybindings.js";
import { createCompactionSummaryMessage } from "../../core/messages.js";
import { findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
import { DefaultPackageManager } from "../../core/package-manager.js";
import { SessionManager } from "../../core/session-manager.js";
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
import { copyToClipboard } from "../../utils/clipboard.js";
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
import { parseGitUrl } from "../../utils/git.js";
import { ensureTool } from "../../utils/tools-manager.js";
import { ArminComponent } from "./components/armin.js";
import { AssistantMessageComponent } from "./components/assistant-message.js";
import { BashExecutionComponent } from "./components/bash-execution.js";
import { BorderedLoader } from "./components/bordered-loader.js";
import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
import { CustomEditor } from "./components/custom-editor.js";
import { CustomMessageComponent } from "./components/custom-message.js";
import { DaxnutsComponent } from "./components/daxnuts.js";
import { DynamicBorder } from "./components/dynamic-border.js";
import { ExtensionEditorComponent } from "./components/extension-editor.js";
import { ExtensionInputComponent } from "./components/extension-input.js";
import { ExtensionSelectorComponent } from "./components/extension-selector.js";
import { FooterComponent } from "./components/footer.js";
import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
import { LoginDialogComponent } from "./components/login-dialog.js";
import { ModelSelectorComponent } from "./components/model-selector.js";
import { OAuthSelectorComponent } from "./components/oauth-selector.js";
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
import { SessionSelectorComponent } from "./components/session-selector.js";
import { SettingsSelectorComponent } from "./components/settings-selector.js";
import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
import { ToolExecutionComponent } from "./components/tool-execution.js";
import { TreeSelectorComponent } from "./components/tree-selector.js";
import { UserMessageComponent } from "./components/user-message.js";
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, Theme, theme, } from "./theme/theme.js";
function isExpandable(obj) {
    return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
}
export class InteractiveMode {
    options;
    session;
    ui;
    chatContainer;
    pendingMessagesContainer;
    statusContainer;
    defaultEditor;
    editor;
    autocompleteProvider;
    fdPath;
    editorContainer;
    footer;
    footerDataProvider;
    // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
    keybindings;
    version;
    isInitialized = false;
    onInputCallback;
    loadingAnimation = undefined;
    pendingWorkingMessage = undefined;
    defaultWorkingMessage = "Working...";
    defaultHiddenThinkingLabel = "Thinking...";
    hiddenThinkingLabel = this.defaultHiddenThinkingLabel;
    lastSigintTime = 0;
    lastEscapeTime = 0;
    changelogMarkdown = undefined;
    // Status line tracking (for mutating immediately-sequential status updates)
    lastStatusSpacer = undefined;
    lastStatusText = undefined;
    // Streaming message tracking
    streamingComponent = undefined;
    streamingMessage = undefined;
    // Tool execution tracking: toolCallId -> component
    pendingTools = new Map();
    // Tool output expansion state
    toolOutputExpanded = false;
    // Thinking block visibility state
    hideThinkingBlock = false;
    // Skill commands: command name -> skill file path
    skillCommands = new Map();
    // Agent subscription unsubscribe function
    unsubscribe;
    // Track if editor is in bash mode (text starts with !)
    isBashMode = false;
    // Track current bash execution component
    bashComponent = undefined;
    // Track pending bash components (shown in pending area, moved to chat on submit)
    pendingBashComponents = [];
    // Auto-compaction state
    autoCompactionLoader = undefined;
    autoCompactionEscapeHandler;
    // Auto-retry state
    retryLoader = undefined;
    retryEscapeHandler;
    // Messages queued while compaction is running
    compactionQueuedMessages = [];
    // Shutdown state
    shutdownRequested = false;
    // Extension UI state
    extensionSelector = undefined;
    extensionInput = undefined;
    extensionEditor = undefined;
    extensionTerminalInputUnsubscribers = new Set();
    // Extension widgets (components rendered above/below the editor)
    extensionWidgetsAbove = new Map();
    extensionWidgetsBelow = new Map();
    widgetContainerAbove;
    widgetContainerBelow;
    // Custom footer from extension (undefined = use built-in footer)
    customFooter = undefined;
    // Header container that holds the built-in or custom header
    headerContainer;
    // Built-in header (logo + keybinding hints + changelog)
    builtInHeader = undefined;
    // Custom header from extension (undefined = use built-in header)
    customHeader = undefined;
    // Convenience accessors
    get agent() {
        return this.session.agent;
    }
    get sessionManager() {
        return this.session.sessionManager;
    }
    get settingsManager() {
        return this.session.settingsManager;
    }
    constructor(session, options = {}) {
        this.options = options;
        this.session = session;
        this.version = VERSION;
        this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
        this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
        this.headerContainer = new Container();
        this.chatContainer = new Container();
        this.pendingMessagesContainer = new Container();
        this.statusContainer = new Container();
        this.widgetContainerAbove = new Container();
        this.widgetContainerBelow = new Container();
        this.keybindings = KeybindingsManager.create();
        setKeybindings(this.keybindings);
        const editorPaddingX = this.settingsManager.getEditorPaddingX();
        const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
        this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
            paddingX: editorPaddingX,
            autocompleteMaxVisible,
        });
        this.editor = this.defaultEditor;
        this.editorContainer = new Container();
        this.editorContainer.addChild(this.editor);
        this.footerDataProvider = new FooterDataProvider();
        this.footer = new FooterComponent(session, this.footerDataProvider);
        this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
        // Load hide thinking block setting
        this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
        // Register themes from resource loader and initialize
        setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
        initTheme(this.settingsManager.getTheme(), true);
    }
    getAutocompleteSourceTag(sourceInfo) {
        if (!sourceInfo) {
            return undefined;
        }
        const scopePrefix = sourceInfo.scope === "user" ? "u" : sourceInfo.scope === "project" ? "p" : "t";
        const source = sourceInfo.source.trim();
        if (source === "auto" || source === "local" || source === "cli") {
            return scopePrefix;
        }
        if (source.startsWith("npm:")) {
            return `${scopePrefix}:${source}`;
        }
        const gitSource = parseGitUrl(source);
        if (gitSource) {
            const ref = gitSource.ref ? `@${gitSource.ref}` : "";
            return `${scopePrefix}:git:${gitSource.host}/${gitSource.path}${ref}`;
        }
        return scopePrefix;
    }
    prefixAutocompleteDescription(description, sourceInfo) {
        const sourceTag = this.getAutocompleteSourceTag(sourceInfo);
        if (!sourceTag) {
            return description;
        }
        return description ? `[${sourceTag}] ${description}` : `[${sourceTag}]`;
    }
    getBuiltInCommandConflictDiagnostics(extensionRunner) {
        if (!extensionRunner) {
            return [];
        }
        const builtinNames = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
        return extensionRunner
            .getRegisteredCommands()
            .filter((command) => builtinNames.has(command.name))
            .map((command) => ({
            type: "warning",
            message: command.invocationName === command.name
                ? `Extension command '/${command.name}' conflicts with built-in interactive command. Skipping in autocomplete.`
                : `Extension command '/${command.name}' conflicts with built-in interactive command. Available as '/${command.invocationName}'.`,
            path: command.sourceInfo.path,
        }));
    }
    setupAutocomplete(fdPath) {
        // Define commands for autocomplete
        const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
            name: command.name,
            description: command.description,
        }));
        const modelCommand = slashCommands.find((command) => command.name === "model");
        if (modelCommand) {
            modelCommand.getArgumentCompletions = (prefix) => {
                // Get available models (scoped or from registry)
                const models = this.session.scopedModels.length > 0
                    ? this.session.scopedModels.map((s) => s.model)
                    : this.session.modelRegistry.getAvailable();
                if (models.length === 0)
                    return null;
                // Create items with provider/id format
                const items = models.map((m) => ({
                    id: m.id,
                    provider: m.provider,
                    label: `${m.provider}/${m.id}`,
                }));
                // Fuzzy filter by model ID + provider (allows "opus anthropic" to match)
                const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`);
                if (filtered.length === 0)
                    return null;
                return filtered.map((item) => ({
                    value: item.label,
                    label: item.id,
                    description: item.provider,
                }));
            };
        }
        // Convert prompt templates to SlashCommand format for autocomplete
        const templateCommands = this.session.promptTemplates.map((cmd) => ({
            name: cmd.name,
            description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
        }));
        // Convert extension commands to SlashCommand format
        const builtinCommandNames = new Set(slashCommands.map((c) => c.name));
        const extensionCommands = (this.session.extensionRunner?.getRegisteredCommands().filter((cmd) => !builtinCommandNames.has(cmd.name)) ?? []).map((cmd) => ({
            name: cmd.invocationName,
            description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
            getArgumentCompletions: cmd.getArgumentCompletions,
        }));
        // Build skill commands from session.skills (if enabled)
        this.skillCommands.clear();
        const skillCommandList = [];
        if (this.settingsManager.getEnableSkillCommands()) {
            for (const skill of this.session.resourceLoader.getSkills().skills) {
                const commandName = `skill:${skill.name}`;
                this.skillCommands.set(commandName, skill.filePath);
                skillCommandList.push({
                    name: commandName,
                    description: this.prefixAutocompleteDescription(skill.description, skill.sourceInfo),
                });
            }
        }
        // Setup autocomplete
        this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], process.cwd(), fdPath);
        this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
        if (this.editor !== this.defaultEditor) {
            this.editor.setAutocompleteProvider?.(this.autocompleteProvider);
        }
    }
    async init() {
        if (this.isInitialized)
            return;
        // Load changelog (only show new entries, skip for resumed sessions)
        this.changelogMarkdown = this.getChangelogForDisplay();
        // Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
        // Both are needed: fd for autocomplete, rg for grep tool and bash commands
        const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
        this.fdPath = fdPath;
        // Add header container as first child
        this.ui.addChild(this.headerContainer);
        // Add header with keybindings from config (unless silenced)
        if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
            const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
            // Build startup instructions using keybinding hint helpers
            const hint = (keybinding, description) => keyHint(keybinding, description);
            const instructions = [
                hint("app.interrupt", "to interrupt"),
                hint("app.clear", "to clear"),
                rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
                hint("app.exit", "to exit (empty)"),
                hint("app.suspend", "to suspend"),
                keyHint("tui.editor.deleteToLineEnd", "to delete to end"),
                hint("app.thinking.cycle", "to cycle thinking level"),
                rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
                hint("app.model.select", "to select model"),
                hint("app.tools.expand", "to expand tools"),
                hint("app.thinking.toggle", "to expand thinking"),
                hint("app.editor.external", "for external editor"),
                rawKeyHint("/", "for commands"),
                rawKeyHint("!", "to run bash"),
                rawKeyHint("!!", "to run bash (no context)"),
                hint("app.message.followUp", "to queue follow-up"),
                hint("app.message.dequeue", "to edit all queued messages"),
                hint("app.clipboard.pasteImage", "to paste image"),
                rawKeyHint("drop files", "to attach"),
            ].join("\n");
            const onboarding = theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
            this.builtInHeader = new Text(`${logo}\n${instructions}\n\n${onboarding}`, 1, 0);
            // Setup UI layout
            this.headerContainer.addChild(new Spacer(1));
            this.headerContainer.addChild(this.builtInHeader);
            this.headerContainer.addChild(new Spacer(1));
            // Add changelog if provided
            if (this.changelogMarkdown) {
                this.headerContainer.addChild(new DynamicBorder());
                if (this.settingsManager.getCollapseChangelog()) {
                    const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
                    const latestVersion = versionMatch ? versionMatch[1] : this.version;
                    const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
                    this.headerContainer.addChild(new Text(condensedText, 1, 0));
                }
                else {
                    this.headerContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
                    this.headerContainer.addChild(new Spacer(1));
                    this.headerContainer.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()));
                    this.headerContainer.addChild(new Spacer(1));
                }
                this.headerContainer.addChild(new DynamicBorder());
            }
        }
        else {
            // Minimal header when silenced
            this.builtInHeader = new Text("", 0, 0);
            this.headerContainer.addChild(this.builtInHeader);
            if (this.changelogMarkdown) {
                // Still show changelog notification even in silent mode
                this.headerContainer.addChild(new Spacer(1));
                const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
                const latestVersion = versionMatch ? versionMatch[1] : this.version;
                const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
                this.headerContainer.addChild(new Text(condensedText, 1, 0));
            }
        }
        this.ui.addChild(this.chatContainer);
        this.ui.addChild(this.pendingMessagesContainer);
        this.ui.addChild(this.statusContainer);
        this.renderWidgets(); // Initialize with default spacer
        this.ui.addChild(this.widgetContainerAbove);
        this.ui.addChild(this.editorContainer);
        this.ui.addChild(this.widgetContainerBelow);
        this.ui.addChild(this.footer);
        this.ui.setFocus(this.editor);
        this.setupKeyHandlers();
        this.setupEditorSubmitHandler();
        // Start the UI before initializing extensions so session_start handlers can use interactive dialogs
        this.ui.start();
        this.isInitialized = true;
        // Initialize extensions first so resources are shown before messages
        await this.initExtensions();
        // Render initial messages AFTER showing loaded resources
        this.renderInitialMessages();
        // Set terminal title
        this.updateTerminalTitle();
        // Subscribe to agent events
        this.subscribeToAgent();
        // Set up theme file watcher
        onThemeChange(() => {
            this.ui.invalidate();
            this.updateEditorBorderColor();
            this.ui.requestRender();
        });
        // Set up git branch watcher (uses provider instead of footer)
        this.footerDataProvider.onBranchChange(() => {
            this.ui.requestRender();
        });
        // Initialize available provider count for footer display
        await this.updateAvailableProviderCount();
    }
    /**
     * Update terminal title with session name and cwd.
     */
    updateTerminalTitle() {
        const cwdBasename = path.basename(process.cwd());
        const sessionName = this.sessionManager.getSessionName();
        if (sessionName) {
            this.ui.terminal.setTitle(`π - ${sessionName} - ${cwdBasename}`);
        }
        else {
            this.ui.terminal.setTitle(`π - ${cwdBasename}`);
        }
    }
    /**
     * Run the interactive mode. This is the main entry point.
     * Initializes the UI, shows warnings, processes initial messages, and starts the interactive loop.
     */
    async run() {
        await this.init();
        // Start version check asynchronously
        this.checkForNewVersion().then((newVersion) => {
            if (newVersion) {
                this.showNewVersionNotification(newVersion);
            }
        });
        // Start package update check asynchronously
        this.checkForPackageUpdates().then((updates) => {
            if (updates.length > 0) {
                this.showPackageUpdateNotification(updates);
            }
        });
        // Check tmux keyboard setup asynchronously
        this.checkTmuxKeyboardSetup().then((warning) => {
            if (warning) {
                this.showWarning(warning);
            }
        });
        // Show startup warnings
        const { migratedProviders, modelFallbackMessage, initialMessage, initialImages, initialMessages } = this.options;
        if (migratedProviders && migratedProviders.length > 0) {
            this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
        }
        const modelsJsonError = this.session.modelRegistry.getError();
        if (modelsJsonError) {
            this.showError(`models.json error: ${modelsJsonError}`);
        }
        if (modelFallbackMessage) {
            this.showWarning(modelFallbackMessage);
        }
        // Process initial messages
        if (initialMessage) {
            try {
                await this.session.prompt(initialMessage, { images: initialImages });
            }
            catch (error) {
                const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
                this.showError(errorMessage);
            }
        }
        if (initialMessages) {
            for (const message of initialMessages) {
                try {
                    await this.session.prompt(message);
                }
                catch (error) {
                    const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
                    this.showError(errorMessage);
                }
            }
        }
        // Main interactive loop
        while (true) {
            const userInput = await this.getUserInput();
            try {
                await this.session.prompt(userInput);
            }
            catch (error) {
                const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
                this.showError(errorMessage);
            }
        }
    }
    /**
     * Check npm registry for a newer version.
     */
    async checkForNewVersion() {
        if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
            return undefined;
        try {
            const response = await fetch("https://registry.npmjs.org/@mariozechner/pi-coding-agent/latest", {
                signal: AbortSignal.timeout(10000),
            });
            if (!response.ok)
                return undefined;
            const data = (await response.json());
            const latestVersion = data.version;
            if (latestVersion && latestVersion !== this.version) {
                return latestVersion;
            }
            return undefined;
        }
        catch {
            return undefined;
        }
    }
    async checkForPackageUpdates() {
        if (process.env.PI_OFFLINE) {
            return [];
        }
        try {
            const packageManager = new DefaultPackageManager({
                cwd: process.cwd(),
                agentDir: getAgentDir(),
                settingsManager: this.settingsManager,
            });
            const updates = await packageManager.checkForAvailableUpdates();
            return updates.map((update) => update.displayName);
        }
        catch {
            return [];
        }
    }
    async checkTmuxKeyboardSetup() {
        if (!process.env.TMUX)
            return undefined;
        const runTmuxShow = (option) => {
            return new Promise((resolve) => {
                const proc = spawn("tmux", ["show", "-gv", option], {
                    stdio: ["ignore", "pipe", "ignore"],
                });
                let stdout = "";
                const timer = setTimeout(() => {
                    proc.kill();
                    resolve(undefined);
                }, 2000);
                proc.stdout?.on("data", (data) => {
                    stdout += data.toString();
                });
                proc.on("error", () => {
                    clearTimeout(timer);
                    resolve(undefined);
                });
                proc.on("close", (code) => {
                    clearTimeout(timer);
                    resolve(code === 0 ? stdout.trim() : undefined);
                });
            });
        };
        const [extendedKeys, extendedKeysFormat] = await Promise.all([
            runTmuxShow("extended-keys"),
            runTmuxShow("extended-keys-format"),
        ]);
        // If we couldn't query tmux (timeout, sandbox, etc.), don't warn
        if (extendedKeys === undefined)
            return undefined;
        if (extendedKeys !== "on" && extendedKeys !== "always") {
            return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
        }
        if (extendedKeysFormat === "xterm") {
            return "tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.";
        }
        return undefined;
    }
    /**
     * Get changelog entries to display on startup.
     * Only shows new entries since last seen version, skips for resumed sessions.
     */
    getChangelogForDisplay() {
        // Skip changelog for resumed/continued sessions (already have messages)
        if (this.session.state.messages.length > 0) {
            return undefined;
        }
        const lastVersion = this.settingsManager.getLastChangelogVersion();
        const changelogPath = getChangelogPath();
        const entries = parseChangelog(changelogPath);
        if (!lastVersion) {
            // Fresh install - just record the version, don't show changelog
            this.settingsManager.setLastChangelogVersion(VERSION);
            return undefined;
        }
        else {
            const newEntries = getNewEntries(entries, lastVersion);
            if (newEntries.length > 0) {
                this.settingsManager.setLastChangelogVersion(VERSION);
                return newEntries.map((e) => e.content).join("\n\n");
            }
        }
        return undefined;
    }
    getMarkdownThemeWithSettings() {
        return {
            ...getMarkdownTheme(),
            codeBlockIndent: this.settingsManager.getCodeBlockIndent(),
        };
    }
    // =========================================================================
    // Extension System
    // =========================================================================
    formatDisplayPath(p) {
        const home = os.homedir();
        let result = p;
        // Replace home directory with ~
        if (result.startsWith(home)) {
            result = `~${result.slice(home.length)}`;
        }
        return result;
    }
    /**
     * Get a short path relative to the package root for display.
     */
    getShortPath(fullPath, sourceInfo) {
        const source = sourceInfo?.source ?? "";
        const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
        if (npmMatch && source.startsWith("npm:")) {
            return npmMatch[2];
        }
        const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
        if (gitMatch && source.startsWith("git:")) {
            return gitMatch[1];
        }
        return this.formatDisplayPath(fullPath);
    }
    getDisplaySourceInfo(sourceInfo) {
        const source = sourceInfo?.source ?? "local";
        const scope = sourceInfo?.scope ?? "project";
        if (source === "local") {
            if (scope === "user") {
                return { label: "user", color: "muted" };
            }
            if (scope === "project") {
                return { label: "project", color: "muted" };
            }
            if (scope === "temporary") {
                return { label: "path", scopeLabel: "temp", color: "muted" };
            }
            return { label: "path", color: "muted" };
        }
        if (source === "cli") {
            return { label: "path", scopeLabel: scope === "temporary" ? "temp" : undefined, color: "muted" };
        }
        const scopeLabel = scope === "user" ? "user" : scope === "project" ? "project" : scope === "temporary" ? "temp" : undefined;
        return { label: source, scopeLabel, color: "accent" };
    }
    getScopeGroup(sourceInfo) {
        const source = sourceInfo?.source ?? "local";
        const scope = sourceInfo?.scope ?? "project";
        if (source === "cli" || scope === "temporary")
            return "path";
        if (scope === "user")
            return "user";
        if (scope === "project")
            return "project";
        return "path";
    }
    isPackageSource(sourceInfo) {
        const source = sourceInfo?.source ?? "";
        return source.startsWith("npm:") || source.startsWith("git:");
    }
    buildScopeGroups(items) {
        const groups = {
            user: { scope: "user", paths: [], packages: new Map() },
            project: { scope: "project", paths: [], packages: new Map() },
            path: { scope: "path", paths: [], packages: new Map() },
        };
        for (const item of items) {
            const groupKey = this.getScopeGroup(item.sourceInfo);
            const group = groups[groupKey];
            const source = item.sourceInfo?.source ?? "local";
            if (this.isPackageSource(item.sourceInfo)) {
                const list = group.packages.get(source) ?? [];
                list.push(item);
                group.packages.set(source, list);
            }
            else {
                group.paths.push(item);
            }
        }
        return [groups.project, groups.user, groups.path].filter((group) => group.paths.length > 0 || group.packages.size > 0);
    }
    formatScopeGroups(groups, options) {
        const lines = [];
        for (const group of groups) {
            lines.push(`  ${theme.fg("accent", group.scope)}`);
            const sortedPaths = [...group.paths].sort((a, b) => a.path.localeCompare(b.path));
            for (const item of sortedPaths) {
                lines.push(theme.fg("dim", `    ${options.formatPath(item)}`));
            }
            const sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));
            for (const [source, items] of sortedPackages) {
                lines.push(`    ${theme.fg("mdLink", source)}`);
                const sortedPackagePaths = [...items].sort((a, b) => a.path.localeCompare(b.path));
                for (const item of sortedPackagePaths) {
                    lines.push(theme.fg("dim", `      ${options.formatPackagePath(item, source)}`));
                }
            }
        }
        return lines.join("\n");
    }
    findSourceInfoForPath(p, sourceInfos) {
        const exact = sourceInfos.get(p);
        if (exact)
            return exact;
        let current = p;
        while (current.includes("/")) {
            current = current.substring(0, current.lastIndexOf("/"));
            const parent = sourceInfos.get(current);
            if (parent)
                return parent;
        }
        return undefined;
    }
    formatPathWithSource(p, sourceInfo) {
        if (sourceInfo) {
            const shortPath = this.getShortPath(p, sourceInfo);
            const { label, scopeLabel } = this.getDisplaySourceInfo(sourceInfo);
            const labelText = scopeLabel ? `${label} (${scopeLabel})` : label;
            return `${labelText} ${shortPath}`;
        }
        return this.formatDisplayPath(p);
    }
    formatDiagnostics(diagnostics, sourceInfos) {
        const lines = [];
        // Group collision diagnostics by name
        const collisions = new Map();
        const otherDiagnostics = [];
        for (const d of diagnostics) {
            if (d.type === "collision" && d.collision) {
                const list = collisions.get(d.collision.name) ?? [];
                list.push(d);
                collisions.set(d.collision.name, list);
            }
            else {
                otherDiagnostics.push(d);
            }
        }
        // Format collision diagnostics grouped by name
        for (const [name, collisionList] of collisions) {
            const first = collisionList[0]?.collision;
            if (!first)
                continue;
            lines.push(theme.fg("warning", `  "${name}" collision:`));
            lines.push(theme.fg("dim", `    ${theme.fg("success", "✓")} ${this.formatPathWithSource(first.winnerPath, this.findSourceInfoForPath(first.winnerPath, sourceInfos))}`));
            for (const d of collisionList) {
                if (d.collision) {
                    lines.push(theme.fg("dim", `    ${theme.fg("warning", "✗")} ${this.formatPathWithSource(d.collision.loserPath, this.findSourceInfoForPath(d.collision.loserPath, sourceInfos))} (skipped)`));
                }
            }
        }
        for (const d of otherDiagnostics) {
            if (d.path) {
                const formattedPath = this.formatPathWithSource(d.path, this.findSourceInfoForPath(d.path, sourceInfos));
                lines.push(theme.fg(d.type === "error" ? "error" : "warning", `  ${formattedPath}`));
                lines.push(theme.fg(d.type === "error" ? "error" : "warning", `    ${d.message}`));
            }
            else {
                lines.push(theme.fg(d.type === "error" ? "error" : "warning", `  ${d.message}`));
            }
        }
        return lines.join("\n");
    }
    showLoadedResources(options) {
        const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
        const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
        if (!showListing && !showDiagnostics) {
            return;
        }
        const sectionHeader = (name, color = "mdHeading") => theme.fg(color, `[${name}]`);
        const skillsResult = this.session.resourceLoader.getSkills();
        const promptsResult = this.session.resourceLoader.getPrompts();
        const themesResult = this.session.resourceLoader.getThemes();
        const extensions = options?.extensions ??
            this.session.resourceLoader.getExtensions().extensions.map((extension) => ({
                path: extension.path,
                sourceInfo: extension.sourceInfo,
            }));
        const sourceInfos = new Map();
        for (const extension of extensions) {
            if (extension.sourceInfo) {
                sourceInfos.set(extension.path, extension.sourceInfo);
            }
        }
        for (const skill of skillsResult.skills) {
            if (skill.sourceInfo) {
                sourceInfos.set(skill.filePath, skill.sourceInfo);
            }
        }
        for (const prompt of promptsResult.prompts) {
            if (prompt.sourceInfo) {
                sourceInfos.set(prompt.filePath, prompt.sourceInfo);
            }
        }
        for (const loadedTheme of themesResult.themes) {
            if (loadedTheme.sourcePath && loadedTheme.sourceInfo) {
                sourceInfos.set(loadedTheme.sourcePath, loadedTheme.sourceInfo);
            }
        }
        if (showListing) {
            const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
            if (contextFiles.length > 0) {
                this.chatContainer.addChild(new Spacer(1));
                const contextList = contextFiles
                    .map((f) => theme.fg("dim", `  ${this.formatDisplayPath(f.path)}`))
                    .join("\n");
                this.chatContainer.addChild(new Text(`${sectionHeader("Context")}\n${contextList}`, 0, 0));
                this.chatContainer.addChild(new Spacer(1));
            }
            const skills = skillsResult.skills;
            if (skills.length > 0) {
                const groups = this.buildScopeGroups(skills.map((skill) => ({ path: skill.filePath, sourceInfo: skill.sourceInfo })));
                const skillList = this.formatScopeGroups(groups, {
                    formatPath: (item) => this.formatDisplayPath(item.path),
                    formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
                });
                this.chatContainer.addChild(new Text(`${sectionHeader("Skills")}\n${skillList}`, 0, 0));
                this.chatContainer.addChild(new Spacer(1));
            }
            const templates = this.session.promptTemplates;
            if (templates.length > 0) {
                const groups = this.buildScopeGroups(templates.map((template) => ({ path: template.filePath, sourceInfo: template.sourceInfo })));
                const templateByPath = new Map(templates.map((t) => [t.filePath, t]));
                const templateList = this.formatScopeGroups(groups, {
                    formatPath: (item) => {
                        const template = templateByPath.get(item.path);
                        return template ? `/${template.name}` : this.formatDisplayPath(item.path);
                    },
                    formatPackagePath: (item) => {
                        const template = templateByPath.get(item.path);
                        return template ? `/${template.name}` : this.formatDisplayPath(item.path);
                    },
                });
                this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0));
                this.chatContainer.addChild(new Spacer(1));
            }
            if (extensions.length > 0) {
                const groups = this.buildScopeGroups(extensions);
                const extList = this.formatScopeGroups(groups, {
                    formatPath: (item) => this.formatDisplayPath(item.path),
                    formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
                });
                this.chatContainer.addChild(new Text(`${sectionHeader("Extensions", "mdHeading")}\n${extList}`, 0, 0));
                this.chatContainer.addChild(new Spacer(1));
            }
            // Show loaded themes (excluding built-in)
            const loadedThemes = themesResult.themes;
            const customThemes = loadedThemes.filter((t) => t.sourcePath);
            if (customThemes.length > 0) {
                const groups = this.buildScopeGroups(customThemes.map((loadedTheme) => ({
                    path: loadedTheme.sourcePath,
                    sourceInfo: loadedTheme.sourceInfo,
                })));
                const themeList = this.formatScopeGroups(groups, {
                    formatPath: (item) => this.formatDisplayPath(item.path),
                    formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
                });
                this.chatContainer.addChild(new Text(`${sectionHeader("Themes")}\n${themeList}`, 0, 0));
                this.chatContainer.addChild(new Spacer(1));
            }
        }
        if (showDiagnostics) {
            const skillDiagnostics = skillsResult.diagnostics;
            if (skillDiagnostics.length > 0) {
                const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
                this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
                this.chatContainer.addChild(new Spacer(1));
            }
            const promptDiagnostics = promptsResult.diagnostics;
            if (promptDiagnostics.length > 0) {
                const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
                this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
                this.chatContainer.addChild(new Spacer(1));
            }
            const extensionDiagnostics = [];
            const extensionErrors = this.session.resourceLoader.getExtensions().errors;
            if (extensionErrors.length > 0) {
                for (const error of extensionErrors) {
                    extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
                }
            }
            const commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? [];
            extensionDiagnostics.push(...commandDiagnostics);
            extensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner));
            const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? [];
            extensionDiagnostics.push(...shortcutDiagnostics);
            if (extensionDiagnostics.length > 0) {
                const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
                this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
                this.chatContainer.addChild(new Spacer(1));
            }
            const themeDiagnostics = themesResult.diagnostics;
            if (themeDiagnostics.length > 0) {
                const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
                this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
                this.chatContainer.addChild(new Spacer(1));
            }
        }
    }
    /**
     * Initialize the extension system with TUI-based UI context.
     */
    async initExtensions() {
        const uiContext = this.createExtensionUIContext();
        await this.session.bindExtensions({
            uiContext,
            commandContextActions: {
                waitForIdle: () => this.session.agent.waitForIdle(),
                newSession: async (options) => {
                    if (this.loadingAnimation) {
                        this.loadingAnimation.stop();
                        this.loadingAnimation = undefined;
                    }
                    this.statusContainer.clear();
                    // Delegate to AgentSession (handles setup + agent state sync)
                    const success = await this.session.newSession(options);
                    if (!success) {
                        return { cancelled: true };
                    }
                    // Clear UI state
                    this.chatContainer.clear();
                    this.pendingMessagesContainer.clear();
                    this.compactionQueuedMessages = [];
                    this.streamingComponent = undefined;
                    this.streamingMessage = undefined;
                    this.pendingTools.clear();
                    // Render any messages added via setup, or show empty session
                    this.renderInitialMessages();
                    this.ui.requestRender();
                    return { cancelled: false };
                },
                fork: async (entryId) => {
                    const result = await this.session.fork(entryId);
                    if (result.cancelled) {
                        return { cancelled: true };
                    }
                    this.chatContainer.clear();
                    this.renderInitialMessages();
                    this.editor.setText(result.selectedText);
                    this.showStatus("Forked to new session");
                    return { cancelled: false };
                },
                navigateTree: async (targetId, options) => {
                    const result = await this.session.navigateTree(targetId, {
                        summarize: options?.summarize,
                        customInstructions: options?.customInstructions,
                        replaceInstructions: options?.replaceInstructions,
                        label: options?.label,
                    });
                    if (result.cancelled) {
                        return { cancelled: true };
                    }
                    this.chatContainer.clear();
                    this.renderInitialMessages();
                    if (result.editorText && !this.editor.getText().trim()) {
                        this.editor.setText(result.editorText);
                    }
                    this.showStatus("Navigated to selected point");
                    return { cancelled: false };
                },
                switchSession: async (sessionPath) => {
                    await this.handleResumeSession(sessionPath);
                    return { cancelled: false };
                },
                reload: async () => {
                    await this.handleReloadCommand();
                },
            },
            shutdownHandler: () => {
                this.shutdownRequested = true;
                if (!this.session.isStreaming) {
                    void this.shutdown();
                }
            },
            onError: (error) => {
                this.showExtensionError(error.extensionPath, error.error, error.stack);
            },
        });
        setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
        this.setupAutocomplete(this.fdPath);
        const extensionRunner = this.session.extensionRunner;
        if (!extensionRunner) {
            this.showLoadedResources({ extensions: [], force: false });
            return;
        }
        this.setupExtensionShortcuts(extensionRunner);
        this.showLoadedResources({ force: false });
    }
    /**
     * Get a registered tool definition by name (for custom rendering).
     */
    getRegisteredToolDefinition(toolName) {
        return this.session.getToolDefinition(toolName);
    }
    /**
     * Set up keyboard shortcuts registered by extensions.
     */
    setupExtensionShortcuts(extensionRunner) {
        const shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());
        if (shortcuts.size === 0)
            return;
        // Create a context for shortcut handlers
        const createContext = () => ({
            ui: this.createExtensionUIContext(),
            hasUI: true,
            cwd: process.cwd(),
            sessionManager: this.sessionManager,
            modelRegistry: this.session.modelRegistry,
            model: this.session.model,
            isIdle: () => !this.session.isStreaming,
            signal: this.session.agent.signal,
            abort: () => this.session.abort(),
            hasPendingMessages: () => this.session.pendingMessageCount > 0,
            shutdown: () => {
                this.shutdownRequested = true;
            },
            getContextUsage: () => this.session.getContextUsage(),
            compact: (options) => {
                void (async () => {
                    try {
                        const result = await this.session.compact(options?.customInstructions);
                        options?.onComplete?.(result);
                    }
                    catch (error) {
                        const err = error instanceof Error ? error : new Error(String(error));
                        options?.onError?.(err);
                    }
                })();
            },
            getSystemPrompt: () => this.session.systemPrompt,
        });
        // Set up the extension shortcut handler on the default editor
        this.defaultEditor.onExtensionShortcut = (data) => {
            for (const [shortcutStr, shortcut] of shortcuts) {
                // Cast to KeyId - extension shortcuts use the same format
                if (matchesKey(data, shortcutStr)) {
                    // Run handler async, don't block input
                    Promise.resolve(shortcut.handler(createContext())).catch((err) => {
                        this.showError(`Shortcut handler error: ${err instanceof Error ? err.message : String(err)}`);
                    });
                    return true;
                }
            }
            return false;
        };
    }
    /**
     * Set extension status text in the footer.
     */
    setExtensionStatus(key, text) {
        this.footerDataProvider.setExtensionStatus(key, text);
        this.ui.requestRender();
    }
    setHiddenThinkingLabel(label) {
        this.hiddenThinkingLabel = label ?? this.defaultHiddenThinkingLabel;
        for (const child of this.chatContainer.children) {
            if (child instanceof AssistantMessageComponent) {
                child.setHiddenThinkingLabel(this.hiddenThinkingLabel);
            }
        }
        if (this.streamingComponent) {
            this.streamingComponent.setHiddenThinkingLabel(this.hiddenThinkingLabel);
        }
        this.ui.requestRender();
    }
    /**
     * Set an extension widget (string array or custom component).
     */
    setExtensionWidget(key, content, options) {
        const placement = options?.placement ?? "aboveEditor";
        const removeExisting = (map) => {
            const existing = map.get(key);
            if (existing?.dispose)
                existing.dispose();
            map.delete(key);
        };
        removeExisting(this.extensionWidgetsAbove);
        removeExisting(this.extensionWidgetsBelow);
        if (content === undefined) {
            this.renderWidgets();
            return;
        }
        let component;
        if (Array.isArray(content)) {
            // Wrap string array in a Container with Text components
            const container = new Container();
            for (const line of content.slice(0, InteractiveMode.MAX_WIDGET_LINES)) {
                container.addChild(new Text(line, 1, 0));
            }
            if (content.length > InteractiveMode.MAX_WIDGET_LINES) {
                container.addChild(new Text(theme.fg("muted", "... (widget truncated)"), 1, 0));
            }
            component = container;
        }
        else {
            // Factory function - create component
            component = content(this.ui, theme);
        }
        const targetMap = placement === "belowEditor" ? this.extensionWidgetsBelow : this.extensionWidgetsAbove;
        targetMap.set(key, component);
        this.renderWidgets();
    }
    clearExtensionWidgets() {
        for (const widget of this.extensionWidgetsAbove.values()) {
            widget.dispose?.();
        }
        for (const widget of this.extensionWidgetsBelow.values()) {
            widget.dispose?.();
        }
        this.extensionWidgetsAbove.clear();
        this.extensionWidgetsBelow.clear();
        this.renderWidgets();
    }
    resetExtensionUI() {
        if (this.extensionSelector) {
            this.hideExtensionSelector();
        }
        if (this.extensionInput) {
            this.hideExtensionInput();
        }
        if (this.extensionEditor) {
            this.hideExtensionEditor();
        }
        this.ui.hideOverlay();
        this.clearExtensionTerminalInputListeners();
        this.setExtensionFooter(undefined);
        this.setExtensionHeader(undefined);
        this.clearExtensionWidgets();
        this.footerDataProvider.clearExtensionStatuses();
        this.footer.invalidate();
        this.setCustomEditorComponent(undefined);
        this.defaultEditor.onExtensionShortcut = undefined;
        this.updateTerminalTitle();
        if (this.loadingAnimation) {
            this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
        }
        this.setHiddenThinkingLabel();
    }
    // Maximum total widget lines to prevent viewport overflow
    static MAX_WIDGET_LINES = 10;
    /**
     * Render all extension widgets to the widget container.
     */
    renderWidgets() {
        if (!this.widgetContainerAbove || !this.widgetContainerBelow)
            return;
        this.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true);
        this.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);
        this.ui.requestRender();
    }
    renderWidgetContainer(container, widgets, spacerWhenEmpty, leadingSpacer) {
        container.clear();
        if (widgets.size === 0) {
            if (spacerWhenEmpty) {
                container.addChild(new Spacer(1));
            }
            return;
        }
        if (leadingSpacer) {
            container.addChild(new Spacer(1));
        }
        for (const component of widgets.values()) {
            container.addChild(component);
        }
    }
    /**
     * Set a custom footer component, or restore the built-in footer.
     */
    setExtensionFooter(factory) {
        // Dispose existing custom footer
        if (this.customFooter?.dispose) {
            this.customFooter.dispose();
        }
        // Remove current footer from UI
        if (this.customFooter) {
            this.ui.removeChild(this.customFooter);
        }
        else {
            this.ui.removeChild(this.footer);
        }
        if (factory) {
            // Create and add custom footer, passing the data provider
            this.customFooter = factory(this.ui, theme, this.footerDataProvider);
            this.ui.addChild(this.customFooter);
        }
        else {
            // Restore built-in footer
            this.customFooter = undefined;
            this.ui.addChild(this.footer);
        }
        this.ui.requestRender();
    }
    /**
     * Set a custom header component, or restore the built-in header.
     */
    setExtensionHeader(factory) {
        // Header may not be initialized yet if called during early initialization
        if (!this.builtInHeader) {
            return;
        }
        // Dispose existing custom header
        if (this.customHeader?.dispose) {
            this.customHeader.dispose();
        }
        // Find the index of the current header in the header container
        const currentHeader = this.customHeader || this.builtInHeader;
        const index = this.headerContainer.children.indexOf(currentHeader);
        if (factory) {
            // Create and add custom header
            this.customHeader = factory(this.ui, theme);
            if (index !== -1) {
                this.headerContainer.children[index] = this.customHeader;
            }
            else {
                // If not found (e.g. builtInHeader was never added), add at the top
                this.headerContainer.children.unshift(this.customHeader);
            }
        }
        else {
            // Restore built-in header
            this.customHeader = undefined;
            if (index !== -1) {
                this.headerContainer.children[index] = this.builtInHeader;
            }
        }
        this.ui.requestRender();
    }
    addExtensionTerminalInputListener(handler) {
        const unsubscribe = this.ui.addInputListener(handler);
        this.extensionTerminalInputUnsubscribers.add(unsubscribe);
        return () => {
            unsubscribe();
            this.extensionTerminalInputUnsubscribers.delete(unsubscribe);
        };
    }
    clearExtensionTerminalInputListeners() {
        for (const unsubscribe of this.extensionTerminalInputUnsubscribers) {
            unsubscribe();
        }
        this.extensionTerminalInputUnsubscribers.clear();
    }
    /**
     * Create the ExtensionUIContext for extensions.
     */
    createExtensionUIContext() {
        return {
            select: (title, options, opts) => this.showExtensionSelector(title, options, opts),
            confirm: (title, message, opts) => this.showExtensionConfirm(title, message, opts),
            input: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),
            notify: (message, type) => this.showExtensionNotify(message, type),
            onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
            setStatus: (key, text) => this.setExtensionStatus(key, text),
            setWorkingMessage: (message) => {
                if (this.loadingAnimation) {
                    if (message) {
                        this.loadingAnimation.setMessage(message);
                    }
                    else {
                        this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
                    }
                }
                else {
                    // Queue message for when loadingAnimation is created (handles agent_start race)
                    this.pendingWorkingMessage = message;
                }
            },
            setHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),
            setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
            setFooter: (factory) => this.setExtensionFooter(factory),
            setHeader: (factory) => this.setExtensionHeader(factory),
            setTitle: (title) => this.ui.terminal.setTitle(title),
            custom: (factory, options) => this.showExtensionCustom(factory, options),
            pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
            setEditorText: (text) => this.editor.setText(text),
            getEditorText: () => this.editor.getExpandedText?.() ?? this.editor.getText(),
            editor: (title, prefill) => this.showExtensionEditor(title, prefill),
            setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
            get theme() {
                return theme;
            },
            getAllThemes: () => getAvailableThemesWithPaths(),
            getTheme: (name) => getThemeByName(name),
            setTheme: (themeOrName) => {
                if (themeOrName instanceof Theme) {
                    setThemeInstance(themeOrName);
                    this.ui.requestRender();
                    return { success: true };
                }
                const result = setTheme(themeOrName, true);
                if (result.success) {
                    if (this.settingsManager.getTheme() !== themeOrName) {
                        this.settingsManager.setTheme(themeOrName);
                    }
                    this.ui.requestRender();
                }
                return result;
            },
            getToolsExpanded: () => this.toolOutputExpanded,
            setToolsExpanded: (expanded) => this.setToolsExpanded(expanded),
        };
    }
    /**
     * Show a selector for extensions.
     */
    showExtensionSelector(title, options, opts) {
        return new Promise((resolve) => {
            if (opts?.signal?.aborted) {
                resolve(undefined);
                return;
            }
            const onAbort = () => {
                this.hideExtensionSelector();
                resolve(undefined);
            };
            opts?.signal?.addEventListener("abort", onAbort, { once: true });
            this.extensionSelector = new ExtensionSelectorComponent(title, options, (option) => {
                opts?.signal?.removeEventListener("abort", onAbort);
                this.hideExtensionSelector();
                resolve(option);
            }, () => {
                opts?.signal?.removeEventListener("abort", onAbort);
                this.hideExtensionSelector();
                resolve(undefined);
            }, { tui: this.ui, timeout: opts?.timeout });
            this.editorContainer.clear();
            this.editorContainer.addChild(this.extensionSelector);
            this.ui.setFocus(this.extensionSelector);
            this.ui.requestRender();
        });
    }
    /**
     * Hide the extension selector.
     */
    hideExtensionSelector() {
        this.extensionSelector?.dispose();
        this.editorContainer.clear();
        this.editorContainer.addChild(this.editor);
        this.extensionSelector = undefined;
        this.ui.setFocus(this.editor);
        this.ui.requestRender();
    }
    /**
     * Show a confirmation dialog for extensions.
     */
    async showExtensionConfirm(title, message, opts) {
        const result = await this.showExtensionSelector(`${title}\n${message}`, ["Yes", "No"], opts);
        return result === "Yes";
    }
    /**
     * Show a text input for extensions.
     */
    showExtensionInput(title, placeholder, opts) {
        return new Promise((resolve) => {
            if (opts?.signal?.aborted) {
                resolve(undefined);
                return;
            }
            const onAbort = () => {
                this.hideExtensionInput();
                resolve(undefined);
            };
            opts?.signal?.addEventListener("abort", onAbort, { once: true });
            this.extensionInput = new ExtensionInputComponent(title, placeholder, (value) => {
                opts?.signal?.removeEventListener("abort", onAbort);
                this.hideExtensionInput();
                resolve(value);
            }, () => {
                opts?.signal?.removeEventListener("abort", onAbort);
                this.hideExtensionInput();
                resolve(undefined);
            }, { tui: this.ui, timeout: opts?.timeout });
            this.editorContainer.clear();
            this.editorContainer.addChild(this.extensionInput);
            this.ui.setFocus(this.extensionInput);
            this.ui.requestRender();
        });
    }
    /**
     * Hide the extension input.
     */
    hideExtensionInput() {
        this.extensionInput?.dispose();
        this.editorContainer.clear();
        this.editorContainer.addChild(this.editor);
        this.extensionInput = undefined;
        this.ui.setFocus(this.editor);
        this.ui.requestRender();
    }
    /**
     * Show a multi-line editor for extensions (with Ctrl+G support).
     */
    showExtensionEditor(title, prefill) {
        return new Promise((resolve) => {
            this.extensionEditor = new ExtensionEditorComponent(this.ui, this.keybindings, title, prefill, (value) => {
                this.hideExtensionEditor();
                resolve(value);
            }, () => {
                this.hideExtensionEditor();
                resolve(undefined);
            });
            this.editorContainer.clear();
            this.editorContainer.addChild(this.extensionEditor);
            this.ui.setFocus(this.extensionEditor);
            this.ui.requestRender();
        });
    }
    /**
     * Hide the extension editor.
     */
    hideExtensionEditor() {
        this.editorContainer.clear();
        this.editorContainer.addChild(this.editor);
        this.extensionEditor = undefined;
        this.ui.setFocus(this.editor);
        this.ui.requestRender();
    }
    /**
     * Set a custom editor component from an extension.
     * Pass undefined to restore the default editor.
     */
    setCustomEditorComponent(factory) {
        // Save text from current editor before switching
        const currentText = this.editor.getText();
        this.editorContainer.clear();
        if (factory) {
            // Create the custom editor with tui, theme, and keybindings
            const newEditor = factory(this.ui, getEditorTheme(), this.keybindings);
            // Wire up callbacks from the default editor
            newEditor.onSubmit = this.defaultEditor.onSubmit;
            newEditor.onChange = this.defaultEditor.onChange;
            // Copy text from previous editor
            newEditor.setText(currentText);
            // Copy appearance settings if supported
            if (newEditor.borderColor !== undefined) {
                newEditor.borderColor = this.defaultEditor.borderColor;
            }
            if (newEditor.setPaddingX !== undefined) {
                newEditor.setPaddingX(this.defaultEditor.getPaddingX());
            }
            // Set autocomplete if supported
            if (newEditor.setAutocompleteProvider && this.autocompleteProvider) {
                newEditor.setAutocompleteProvider(this.autocompleteProvider);
            }
            // If extending CustomEditor, copy app-level handlers
            // Use duck typing since instanceof fails across jiti module boundaries
            const customEditor = newEditor;
            if ("actionHandlers" in customEditor && customEditor.actionHandlers instanceof Map) {
                if (!customEditor.onEscape) {
                    customEditor.onEscape = () => this.defaultEditor.onEscape?.();
                }
                if (!customEditor.onCtrlD) {
                    customEditor.onCtrlD = () => this.defaultEditor.onCtrlD?.();
                }
                if (!customEditor.onPasteImage) {
                    customEditor.onPasteImage = () => this.defaultEditor.onPasteImage?.();
                }
                if (!customEditor.onExtensionShortcut) {
                    customEditor.onExtensionShortcut = (data) => this.defaultEditor.onExtensionShortcut?.(data);
                }
                // Copy action handlers (clear, suspend, model switching, etc.)
                for (const [action, handler] of this.defaultEditor.actionHandlers) {
                    customEditor.actionHandlers.set(action, handler);
                }
            }
            this.editor = newEditor;
        }
        else {
            // Restore default editor with text from custom editor
            this.defaultEditor.setText(currentText);
            this.editor = this.defaultEditor;
        }
        this.editorContainer.addChild(this.editor);
        this.ui.setFocus(this.editor);
        this.ui.requestRender();
    }
    /**
     * Show a notification for extensions.
     */
    showExtensionNotify(message, type) {
        if (type === "error") {
            this.showError(message);
        }
        else if (type === "warning") {
            this.showWarning(message);
        }
        else {
            this.showStatus(message);
        }
    }
    /** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */
    async showExtensionCustom(factory, options) {
        const savedText = this.editor.getText();
        const isOverlay = options?.overlay ?? false;
        const restoreEditor = () => {
            this.editorContainer.clear();
            this.editorContainer.addChild(this.editor);
            this.editor.setText(savedText);
            this.ui.setFocus(this.editor);
            this.ui.requestRender();
        };
        return new Promise((resolve, reject) => {
            let component;
            let closed = false;
            const close = (result) => {
                if (closed)
                    return;
                closed = true;
                if (isOverlay)
                    this.ui.hideOverlay();
                else
                    restoreEditor();
                // Note: both branches above already call requestRender
                resolve(result);
                try {
                    component?.dispose?.();
                }
                catch {
                    /* ignore dispose errors */
                }
            };
            Promise.resolve(factory(this.ui, theme, this.keybindings, close))
                .then((c) => {
                if (closed)
                    return;
                component = c;
                if (isOverlay) {
                    // Resolve overlay options - can be static or dynamic function
                    const resolveOptions = () => {
                        if (options?.overlayOptions) {
                            const opts = typeof options.overlayOptions === "function"
                                ? options.overlayOptions()
                                : options.overlayOptions;
                            return opts;
                        }
                        // Fallback: use component's width property if available
                        const w = component.width;
                        return w ? { width: w } : undefined;
                    };
                    const handle = this.ui.showOverlay(component, resolveOptions());
                    // Expose handle to caller for visibility control
                    options?.onHandle?.(handle);
                }
                else {
                    this.editorContainer.clear();
                    this.editorContainer.addChild(component);
                    this.ui.setFocus(component);
                    this.ui.requestRender();
                }
            })
                .catch((err) => {
                if (closed)
                    return;
                if (!isOverlay)
                    restoreEditor();
                reject(err);
            });
        });
    }
    /**
     * Show an extension error in the UI.
     */
    showExtensionError(extensionPath, error, stack) {
        const errorMsg = `Extension "${extensionPath}" error: ${error}`;
        const errorText = new Text(theme.fg("error", errorMsg), 1, 0);
        this.chatContainer.addChild(errorText);
        if (stack) {
            // Show stack trace in dim color, indented
            const stackLines = stack
                .split("\n")
                .slice(1) // Skip first line (duplicates error message)
                .map((line) => theme.fg("dim", `  ${line.trim()}`))
                .join("\n");
            if (stackLines) {
                this.chatContainer.addChild(new Text(stackLines, 1, 0));
            }
        }
        this.ui.requestRender();
    }
    // =========================================================================
    // Key Handlers
    // =========================================================================
    setupKeyHandlers() {
        // Set up handlers on defaultEditor - they use this.editor for text access
        // so they work correctly regardless of which editor is active
        this.defaultEditor.onEscape = () => {
            if (this.loadingAnimation) {
                this.restoreQueuedMessagesToEditor({ abort: true });
            }
            else if (this.session.isBashRunning) {
                this.session.abortBash();
            }
            else if (this.isBashMode) {
                this.editor.setText("");
                this.isBashMode = false;
                this.updateEditorBorderColor();
            }
            else if (!this.editor.getText().trim()) {
                // Double-escape with empty editor triggers /tree, /fork, or nothing based on setting
                const action = this.settingsManager.getDoubleEscapeAction();
                if (action !== "none") {
                    const now = Date.now();
                    if (now - this.lastEscapeTime < 500) {
                        if (action === "tree") {
                            this.showTreeSelector();
                        }
                        else {
                            this.showUserMessageSelector();
                        }
                        this.lastEscapeTime = 0;
                    }
                    else {
                        this.lastEscapeTime = now;
                    }
                }
            }
        };
        // Register app action handlers
        this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
        this.defaultEditor.onCtrlD = () => this.handleCtrlD();
        this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
        this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());
        this.defaultEditor.onAction("app.model.cycleForward", () => this.cycleModel("forward"));
        this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
        // Global debug handler on TUI (works regardless of focus)
        this.ui.onDebug = () => this.handleDebugCommand();
        this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
        this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
        this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
        this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
        this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
        this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
        this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
        this.defaultEditor.onAction("app.session.tree", () => this.showTreeSelector());
        this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
        this.defaultEditor.onAction("app.session.resume", () => this.showSessionSelector());
        this.defaultEditor.onChange = (text) => {
            const wasBashMode = this.isBashMode;
            this.isBashMode = text.trimStart().startsWith("!");
            if (wasBashMode !== this.isBashMode) {
                this.updateEditorBorderColor();
            }
        };
        // Handle clipboard image paste (triggered on Ctrl+V)
        this.defaultEditor.onPasteImage = () => {
            this.handleClipboardImagePaste();
        };
    }
    async handleClipboardImagePaste() {
        try {
            const image = await readClipboardImage();
            if (!image) {
                return;
            }
            // Write to temp file
            const tmpDir = os.tmpdir();
            const ext = extensionForImageMimeType(image.mimeType) ?? "png";
            const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
            const filePath = path.join(tmpDir, fileName);
            fs.writeFileSync(filePath, Buffer.from(image.bytes));
            // Insert file path directly
            this.editor.insertTextAtCursor?.(filePath);
            this.ui.requestRender();
        }
        catch {
            // Silently ignore clipboard errors (may not have permission, etc.)
        }
    }
    setupEditorSubmitHandler() {
        this.defaultEditor.onSubmit = async (text) => {
            text = text.trim();
            if (!text)
                return;
            // Handle commands
            if (text === "/settings") {
                this.showSettingsSelector();
                this.editor.setText("");
                return;
            }
            if (text === "/scoped-models") {
                this.editor.setText("");
                await this.showModelsSelector();
                return;
            }
            if (text === "/model" || text.startsWith("/model ")) {
                const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
                this.editor.setText("");
                await this.handleModelCommand(searchTerm);
                return;
            }
            if (text.startsWith("/export")) {
                await this.handleExportCommand(text);
                this.editor.setText("");
                return;
            }
            if (text.startsWith("/import")) {
                await this.handleImportCommand(text);
                this.editor.setText("");
                return;
            }
            if (text === "/share") {
                await this.handleShareCommand();
                this.editor.setText("");
                return;
            }
            if (text === "/copy") {
                await this.handleCopyCommand();
                this.editor.setText("");
                return;
            }
            if (text === "/name" || text.startsWith("/name ")) {
                this.handleNameCommand(text);
                this.editor.setText("");
                return;
            }
            if (text === "/session") {
                this.handleSessionCommand();
                this.editor.setText("");
                return;
            }
            if (text === "/changelog") {
                this.handleChangelogCommand();
                this.editor.setText("");
                return;
            }
            if (text === "/hotkeys") {
                this.handleHotkeysCommand();
                this.editor.setText("");
                return;
            }
            if (text === "/fork") {
                this.showUserMessageSelector();
                this.editor.setText("");
                return;
            }
            if (text === "/tree") {
                this.showTreeSelector();
                this.editor.setText("");
                return;
            }
            if (text === "/login") {
                this.showOAuthSelector("login");
                this.editor.setText("");
                return;
            }
            if (text === "/logout") {
                this.showOAuthSelector("logout");
                this.editor.setText("");
                return;
            }
            if (text === "/new") {
                this.editor.setText("");
                await this.handleClearCommand();
                return;
            }
            if (text === "/compact" || text.startsWith("/compact ")) {
                const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
                this.editor.setText("");
                await this.handleCompactCommand(customInstructions);
                return;
            }
            if (text === "/reload") {
                this.editor.setText("");
                await this.handleReloadCommand();
                return;
            }
            if (text === "/debug") {
                this.handleDebugCommand();
                this.editor.setText("");
                return;
            }
            if (text === "/arminsayshi") {
                this.handleArminSaysHi();
                this.editor.setText("");
                return;
            }
            if (text === "/resume") {
                this.showSessionSelector();
                this.editor.setText("");
                return;
            }
            if (text === "/quit") {
                this.editor.setText("");
                await this.shutdown();
                return;
            }
            // Handle bash command (! for normal, !! for excluded from context)
            if (text.startsWith("!")) {
                const isExcluded = text.startsWith("!!");
                const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
                if (command) {
                    if (this.session.isBashRunning) {
                        this.showWarning("A bash command is already running. Press Esc to cancel it first.");
                        this.editor.setText(text);
                        return;
                    }
                    this.editor.addToHistory?.(text);
                    await this.handleBashCommand(command, isExcluded);
                    this.isBashMode = false;
                    this.updateEditorBorderColor();
                    return;
                }
            }
            // Queue input during compaction (extension commands execute immediately)
            if (this.session.isCompacting) {
                if (this.isExtensionCommand(text)) {
                    this.editor.addToHistory?.(text);
                    this.editor.setText("");
                    await this.session.prompt(text);
                }
                else {
                    this.queueCompactionMessage(text, "steer");
                }
                return;
            }
            // If streaming, use prompt() with steer behavior
            // This handles extension commands (execute immediately), prompt template expansion, and queueing
            if (this.session.isStreaming) {
                this.editor.addToHistory?.(text);
                this.editor.setText("");
                await this.session.prompt(text, { streamingBehavior: "steer" });
                this.updatePendingMessagesDisplay();
                this.ui.requestRender();
                return;
            }
            // Normal message submission
            // First, move any pending bash components to chat
            this.flushPendingBashComponents();
            if (this.onInputCallback) {
                this.onInputCallback(text);
            }
            this.editor.addToHistory?.(text);
        };
    }
    subscribeToAgent() {
        this.unsubscribe = this.session.subscribe(async (event) => {
            await this.handleEvent(event);
        });
    }
    async handleEvent(event) {
        if (!this.isInitialized) {
            await this.init();
        }
        this.footer.invalidate();
        switch (event.type) {
            case "agent_start":
                // Restore main escape handler if retry handler is still active
                // (retry success event fires later, but we need main handler now)
                if (this.retryEscapeHandler) {
                    this.defaultEditor.onEscape = this.retryEscapeHandler;
                    this.retryEscapeHandler = undefined;
                }
                if (this.retryLoader) {
                    this.retryLoader.stop();
                    this.retryLoader = undefined;
                }
                if (this.loadingAnimation) {
                    this.loadingAnimation.stop();
                }
                this.statusContainer.clear();
                this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
                this.statusContainer.addChild(this.loadingAnimation);
                // Apply any pending working message queued before loader existed
                if (this.pendingWorkingMessage !== undefined) {
                    if (this.pendingWorkingMessage) {
                        this.loadingAnimation.setMessage(this.pendingWorkingMessage);
                    }
                    this.pendingWorkingMessage = undefined;
                }
                this.ui.requestRender();
                break;
            case "queue_update":
                this.updatePendingMessagesDisplay();
                this.ui.requestRender();
                break;
            case "message_start":
                if (event.message.role === "custom") {
                    this.addMessageToChat(event.message);
                    this.ui.requestRender();
                }
                else if (event.message.role === "user") {
                    this.addMessageToChat(event.message);
                    this.updatePendingMessagesDisplay();
                    this.ui.requestRender();
                }
                else if (event.message.role === "assistant") {
                    this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
                    this.streamingMessage = event.message;
                    this.chatContainer.addChild(this.streamingComponent);
                    this.streamingComponent.updateContent(this.streamingMessage);
                    this.ui.requestRender();
                }
                break;
            case "message_update":
                if (this.streamingComponent && event.message.role === "assistant") {
                    this.streamingMessage = event.message;
                    this.streamingComponent.updateContent(this.streamingMessage);
                    for (const content of this.streamingMessage.content) {
                        if (content.type === "toolCall") {
                            if (!this.pendingTools.has(content.id)) {
                                const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
                                    showImages: this.settingsManager.getShowImages(),
                                }, this.getRegisteredToolDefinition(content.name), this.ui);
                                component.setExpanded(this.toolOutputExpanded);
                                this.chatContainer.addChild(component);
                                this.pendingTools.set(content.id, component);
                            }
                            else {
                                const component = this.pendingTools.get(content.id);
                                if (component) {
                                    component.updateArgs(content.arguments);
                                }
                            }
                        }
                    }
                    this.ui.requestRender();
                }
                break;
            case "message_end":
                if (event.message.role === "user")
                    break;
                if (this.streamingComponent && event.message.role === "assistant") {
                    this.streamingMessage = event.message;
                    let errorMessage;
                    if (this.streamingMessage.stopReason === "aborted") {
                        const retryAttempt = this.session.retryAttempt;
                        errorMessage =
                            retryAttempt > 0
                                ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
                                : "Operation aborted";
                        this.streamingMessage.errorMessage = errorMessage;
                    }
                    this.streamingComponent.updateContent(this.streamingMessage);
                    if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
                        if (!errorMessage) {
                            errorMessage = this.streamingMessage.errorMessage || "Error";
                        }
                        for (const [, component] of this.pendingTools.entries()) {
                            component.updateResult({
                                content: [{ type: "text", text: errorMessage }],
                                isError: true,
                            });
                        }
                        this.pendingTools.clear();
                    }
                    else {
                        // Args are now complete - trigger diff computation for edit tools
                        for (const [, component] of this.pendingTools.entries()) {
                            component.setArgsComplete();
                        }
                    }
                    this.streamingComponent = undefined;
                    this.streamingMessage = undefined;
                    this.footer.invalidate();
                }
                this.ui.requestRender();
                break;
            case "tool_execution_start": {
                let component = this.pendingTools.get(event.toolCallId);
                if (!component) {
                    component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
                        showImages: this.settingsManager.getShowImages(),
                    }, this.getRegisteredToolDefinition(event.toolName), this.ui);
                    component.setExpanded(this.toolOutputExpanded);
                    this.chatContainer.addChild(component);
                    this.pendingTools.set(event.toolCallId, component);
                }
                component.markExecutionStarted();
                this.ui.requestRender();
                break;
            }
            case "tool_execution_update": {
                const component = this.pendingTools.get(event.toolCallId);
                if (component) {
                    component.updateResult({ ...event.partialResult, isError: false }, true);
                    this.ui.requestRender();
                }
                break;
            }
            case "tool_execution_end": {
                const component = this.pendingTools.get(event.toolCallId);
                if (component) {
                    component.updateResult({ ...event.result, isError: event.isError });
                    this.pendingTools.delete(event.toolCallId);
                    this.ui.requestRender();
                }
                break;
            }
            case "agent_end":
                if (this.loadingAnimation) {
                    this.loadingAnimation.stop();
                    this.loadingAnimation = undefined;
                    this.statusContainer.clear();
                }
                if (this.streamingComponent) {
                    this.chatContainer.removeChild(this.streamingComponent);
                    this.streamingComponent = undefined;
                    this.streamingMessage = undefined;
                }
                this.pendingTools.clear();
                await this.checkShutdownRequested();
                this.ui.requestRender();
                break;
            case "compaction_start": {
                // Keep editor active; submissions are queued during compaction.
                this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
                this.defaultEditor.onEscape = () => {
                    this.session.abortCompaction();
                };
                this.statusContainer.clear();
                const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
                const label = event.reason === "manual"
                    ? `Compacting context... ${cancelHint}`
                    : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`;
                this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
                this.statusContainer.addChild(this.autoCompactionLoader);
                this.ui.requestRender();
                break;
            }
            case "compaction_end": {
                if (this.autoCompactionEscapeHandler) {
                    this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
                    this.autoCompactionEscapeHandler = undefined;
                }
                if (this.autoCompactionLoader) {
                    this.autoCompactionLoader.stop();
                    this.autoCompactionLoader = undefined;
                    this.statusContainer.clear();
                }
                if (event.aborted) {
                    if (event.reason === "manual") {
                        this.showError("Compaction cancelled");
                    }
                    else {
                        this.showStatus("Auto-compaction cancelled");
                    }
                }
                else if (event.result) {
                    this.chatContainer.clear();
                    this.rebuildChatFromMessages();
                    this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
                    this.footer.invalidate();
                }
                else if (event.errorMessage) {
                    if (event.reason === "manual") {
                        this.showError(event.errorMessage);
                    }
                    else {
                        this.chatContainer.addChild(new Spacer(1));
                        this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
                    }
                }
                void this.flushCompactionQueue({ willRetry: event.willRetry });
                this.ui.requestRender();
                break;
            }
            case "auto_retry_start": {
                // Set up escape to abort retry
                this.retryEscapeHandler = this.defaultEditor.onEscape;
                this.defaultEditor.onEscape = () => {
                    this.session.abortRetry();
                };
                // Show retry indicator
                this.statusContainer.clear();
                const delaySeconds = Math.round(event.delayMs / 1000);
                this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${keyText("app.interrupt")} to cancel)`);
                this.statusContainer.addChild(this.retryLoader);
                this.ui.requestRender();
                break;
            }
            case "auto_retry_end": {
                // Restore escape handler
                if (this.retryEscapeHandler) {
                    this.defaultEditor.onEscape = this.retryEscapeHandler;
                    this.retryEscapeHandler = undefined;
                }
                // Stop loader
                if (this.retryLoader) {
                    this.retryLoader.stop();
                    this.retryLoader = undefined;
                    this.statusContainer.clear();
                }
                // Show error only on final failure (success shows normal response)
                if (!event.success) {
                    this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
                }
                this.ui.requestRender();
                break;
            }
        }
    }
    /** Extract text content from a user message */
    getUserMessageText(message) {
        if (message.role !== "user")
            return "";
        const textBlocks = typeof message.content === "string"
            ? [{ type: "text", text: message.content }]
            : message.content.filter((c) => c.type === "text");
        return textBlocks.map((c) => c.text).join("");
    }
    /**
     * Show a status message in the chat.
     *
     * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
     * we update the previous status line instead of appending new ones to avoid log spam.
     */
    showStatus(message) {
        const children = this.chatContainer.children;
        const last = children.length > 0 ? children[children.length - 1] : undefined;
        const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
        if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
            this.lastStatusText.setText(theme.fg("dim", message));
            this.ui.requestRender();
            return;
        }
        const spacer = new Spacer(1);
        const text = new Text(theme.fg("dim", message), 1, 0);
        this.chatContainer.addChild(spacer);
        this.chatContainer.addChild(text);
        this.lastStatusSpacer = spacer;
        this.lastStatusText = text;
        this.ui.requestRender();
    }
    addMessageToChat(message, options) {
        switch (message.role) {
            case "bashExecution": {
                const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
                if (message.output) {
                    component.appendOutput(message.output);
                }
                component.setComplete(message.exitCode, message.cancelled, message.truncated ? { truncated: true } : undefined, message.fullOutputPath);
                this.chatContainer.addChild(component);
                break;
            }
            case "custom": {
                if (message.display) {
                    const renderer = this.session.extensionRunner?.getMessageRenderer(message.customType);
                    const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings());
                    component.setExpanded(this.toolOutputExpanded);
                    this.chatContainer.addChild(component);
                }
                break;
            }
            case "compactionSummary": {
                this.chatContainer.addChild(new Spacer(1));
                const component = new CompactionSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
                component.setExpanded(this.toolOutputExpanded);
                this.chatContainer.addChild(component);
                break;
            }
            case "branchSummary": {
                this.chatContainer.addChild(new Spacer(1));
                const component = new BranchSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
                component.setExpanded(this.toolOutputExpanded);
                this.chatContainer.addChild(component);
                break;
            }
            case "user": {
                const textContent = this.getUserMessageText(message);
                if (textContent) {
                    const skillBlock = parseSkillBlock(textContent);
                    if (skillBlock) {
                        // Render skill block (collapsible)
                        this.chatContainer.addChild(new Spacer(1));
                        const component = new SkillInvocationMessageComponent(skillBlock, this.getMarkdownThemeWithSettings());
                        component.setExpanded(this.toolOutputExpanded);
                        this.chatContainer.addChild(component);
                        // Render user message separately if present
                        if (skillBlock.userMessage) {
                            const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings());
                            this.chatContainer.addChild(userComponent);
                        }
                    }
                    else {
                        const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
                        this.chatContainer.addChild(userComponent);
                    }
                    if (options?.populateHistory) {
                        this.editor.addToHistory?.(textContent);
                    }
                }
                break;
            }
            case "assistant": {
                const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
                this.chatContainer.addChild(assistantComponent);
                break;
            }
            case "toolResult": {
                // Tool results are rendered inline with tool calls, handled separately
                break;
            }
            default: {
                const _exhaustive = message;
            }
        }
    }
    /**
     * Render session context to chat. Used for initial load and rebuild after compaction.
     * @param sessionContext Session context to render
     * @param options.updateFooter Update footer state
     * @param options.populateHistory Add user messages to editor history
     */
    renderSessionContext(sessionContext, options = {}) {
        this.pendingTools.clear();
        if (options.updateFooter) {
            this.footer.invalidate();
            this.updateEditorBorderColor();
        }
        for (const message of sessionContext.messages) {
            // Assistant messages need special handling for tool calls
            if (message.role === "assistant") {
                this.addMessageToChat(message);
                // Render tool call components
                for (const content of message.content) {
                    if (content.type === "toolCall") {
                        const component = new ToolExecutionComponent(content.name, content.id, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(content.name), this.ui);
                        component.setExpanded(this.toolOutputExpanded);
                        this.chatContainer.addChild(component);
                        if (message.stopReason === "aborted" || message.stopReason === "error") {
                            let errorMessage;
                            if (message.stopReason === "aborted") {
                                const retryAttempt = this.session.retryAttempt;
                                errorMessage =
                                    retryAttempt > 0
                                        ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
                                        : "Operation aborted";
                            }
                            else {
                                errorMessage = message.errorMessage || "Error";
                            }
                            component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
                        }
                        else {
                            this.pendingTools.set(content.id, component);
                        }
                    }
                }
            }
            else if (message.role === "toolResult") {
                // Match tool results to pending tool components
                const component = this.pendingTools.get(message.toolCallId);
                if (component) {
                    component.updateResult(message);
                    this.pendingTools.delete(message.toolCallId);
                }
            }
            else {
                // All other messages use standard rendering
                this.addMessageToChat(message, options);
            }
        }
        this.pendingTools.clear();
        this.ui.requestRender();
    }
    renderInitialMessages() {
        // Get aligned messages and entries from session context
        const context = this.sessionManager.buildSessionContext();
        this.renderSessionContext(context, {
            updateFooter: true,
            populateHistory: true,
        });
        // Show compaction info if session was compacted
        const allEntries = this.sessionManager.getEntries();
        const compactionCount = allEntries.filter((e) => e.type === "compaction").length;
        if (compactionCount > 0) {
            const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
            this.showStatus(`Session compacted ${times}`);
        }
    }
    async getUserInput() {
        return new Promise((resolve) => {
            this.onInputCallback = (text) => {
                this.onInputCallback = undefined;
                resolve(text);
            };
        });
    }
    rebuildChatFromMessages() {
        this.chatContainer.clear();
        const context = this.sessionManager.buildSessionContext();
        this.renderSessionContext(context);
    }
    // =========================================================================
    // Key handlers
    // =========================================================================
    handleCtrlC() {
        const now = Date.now();
        if (now - this.lastSigintTime < 500) {
            void this.shutdown();
        }
        else {
            this.clearEditor();
            this.lastSigintTime = now;
        }
    }
    handleCtrlD() {
        // Only called when editor is empty (enforced by CustomEditor)
        void this.shutdown();
    }
    /**
     * Gracefully shutdown the agent.
     * Emits shutdown event to extensions, then exits.
     */
    isShuttingDown = false;
    async shutdown() {
        if (this.isShuttingDown)
            return;
        this.isShuttingDown = true;
        // Emit shutdown event to extensions
        const extensionRunner = this.session.extensionRunner;
        if (extensionRunner?.hasHandlers("session_shutdown")) {
            await extensionRunner.emit({
                type: "session_shutdown",
            });
        }
        // Wait for any pending renders to complete
        // requestRender() uses process.nextTick(), so we wait one tick
        await new Promise((resolve) => process.nextTick(resolve));
        // Drain any in-flight Kitty key release events before stopping.
        // This prevents escape sequences from leaking to the parent shell over slow SSH.
        await this.ui.terminal.drainInput(1000);
        this.stop();
        process.exit(0);
    }
    /**
     * Check if shutdown was requested and perform shutdown if so.
     */
    async checkShutdownRequested() {
        if (!this.shutdownRequested)
            return;
        await this.shutdown();
    }
    handleCtrlZ() {
        // Keep the event loop alive while suspended. Without this, stopping the TUI
        // can leave Node with no ref'ed handles, causing the process to exit on fg
        // before the SIGCONT handler gets a chance to restore the terminal.
        const suspendKeepAlive = setInterval(() => { }, 2 ** 30);
        // Ignore SIGINT while suspended so Ctrl+C in the terminal does not
        // kill the backgrounded process. The handler is removed on resume.
        const ignoreSigint = () => { };
        process.on("SIGINT", ignoreSigint);
        // Set up handler to restore TUI when resumed
        process.once("SIGCONT", () => {
            clearInterval(suspendKeepAlive);
            process.removeListener("SIGINT", ignoreSigint);
            this.ui.start();
            this.ui.requestRender(true);
        });
        try {
            // Stop the TUI (restore terminal to normal mode)
            this.ui.stop();
            // Send SIGTSTP to process group (pid=0 means all processes in group)
            process.kill(0, "SIGTSTP");
        }
        catch (error) {
            clearInterval(suspendKeepAlive);
            process.removeListener("SIGINT", ignoreSigint);
            throw error;
        }
    }
    async handleFollowUp() {
        const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
        if (!text)
            return;
        // Queue input during compaction (extension commands execute immediately)
        if (this.session.isCompacting) {
            if (this.isExtensionCommand(text)) {
                this.editor.addToHistory?.(text);
                this.editor.setText("");
                await this.session.prompt(text);
            }
            else {
                this.queueCompactionMessage(text, "followUp");
            }
            return;
        }
        // Alt+Enter queues a follow-up message (waits until agent finishes)
        // This handles extension commands (execute immediately), prompt template expansion, and queueing
        if (this.session.isStreaming) {
            this.editor.addToHistory?.(text);
            this.editor.setText("");
            await this.session.prompt(text, { streamingBehavior: "followUp" });
            this.updatePendingMessagesDisplay();
            this.ui.requestRender();
        }
        // If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
        else if (this.editor.onSubmit) {
            this.editor.onSubmit(text);
        }
    }
    handleDequeue() {
        const restored = this.restoreQueuedMessagesToEditor();
        if (restored === 0) {
            this.showStatus("No queued messages to restore");
        }
        else {
            this.showStatus(`Restored ${restored} queued message${restored > 1 ? "s" : ""} to editor`);
        }
    }
    updateEditorBorderColor() {
        if (this.isBashMode) {
            this.editor.borderColor = theme.getBashModeBorderColor();
        }
        else {
            const level = this.session.thinkingLevel || "off";
            this.editor.borderColor = theme.getThinkingBorderColor(level);
        }
        this.ui.requestRender();
    }
    cycleThinkingLevel() {
        const newLevel = this.session.cycleThinkingLevel();
        if (newLevel === undefined) {
            this.showStatus("Current model does not support thinking");
        }
        else {
            this.footer.invalidate();
            this.updateEditorBorderColor();
            this.showStatus(`Thinking level: ${newLevel}`);
        }
    }
    async cycleModel(direction) {
        try {
            const result = await this.session.cycleModel(direction);
            if (result === undefined) {
                const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
                this.showStatus(msg);
            }
            else {
                this.footer.invalidate();
                this.updateEditorBorderColor();
                const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
                this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
            }
        }
        catch (error) {
            this.showError(error instanceof Error ? error.message : String(error));
        }
    }
    toggleToolOutputExpansion() {
        this.setToolsExpanded(!this.toolOutputExpanded);
    }
    setToolsExpanded(expanded) {
        this.toolOutputExpanded = expanded;
        for (const child of this.chatContainer.children) {
            if (isExpandable(child)) {
                child.setExpanded(expanded);
            }
        }
        this.ui.requestRender();
    }
    toggleThinkingBlockVisibility() {
        this.hideThinkingBlock = !this.hideThinkingBlock;
        this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
        // Rebuild chat from session messages
        this.chatContainer.clear();
        this.rebuildChatFromMessages();
        // If streaming, re-add the streaming component with updated visibility and re-render
        if (this.streamingComponent && this.streamingMessage) {
            this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);
            this.streamingComponent.updateContent(this.streamingMessage);
            this.chatContainer.addChild(this.streamingComponent);
        }
        this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
    }
    openExternalEditor() {
        // Determine editor (respect $VISUAL, then $EDITOR)
        const editorCmd = process.env.VISUAL || process.env.EDITOR;
        if (!editorCmd) {
            this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
            return;
        }
        const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
        const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
        try {
            // Write current content to temp file
            fs.writeFileSync(tmpFile, currentText, "utf-8");
            // Stop TUI to release terminal
            this.ui.stop();
            // Split by space to support editor arguments (e.g., "code --wait")
            const [editor, ...editorArgs] = editorCmd.split(" ");
            // Spawn editor synchronously with inherited stdio for interactive editing
            const result = spawnSync(editor, [...editorArgs, tmpFile], {
                stdio: "inherit",
                shell: process.platform === "win32",
            });
            // On successful exit (status 0), replace editor content
            if (result.status === 0) {
                const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
                this.editor.setText(newContent);
            }
            // On non-zero exit, keep original text (no action needed)
        }
        finally {
            // Clean up temp file
            try {
                fs.unlinkSync(tmpFile);
            }
            catch {
                // Ignore cleanup errors
            }
            // Restart TUI
            this.ui.start();
            // Force full re-render since external editor uses alternate screen
            this.ui.requestRender(true);
        }
    }
    // =========================================================================
    // UI helpers
    // =========================================================================
    clearEditor() {
        this.editor.setText("");
        this.ui.requestRender();
    }
    showError(errorMessage) {
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
        this.ui.requestRender();
    }
    showWarning(warningMessage) {
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
        this.ui.requestRender();
    }
    showNewVersionNotification(newVersion) {
        const action = theme.fg("accent", getUpdateInstruction("@mariozechner/pi-coding-agent"));
        const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
        const changelogUrl = theme.fg("accent", "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md");
        const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl;
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
        this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
        this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
        this.ui.requestRender();
    }
    showPackageUpdateNotification(packages) {
        const action = theme.fg("accent", `${APP_NAME} update`);
        const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action;
        const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n");
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
        this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`, 1, 0));
        this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
        this.ui.requestRender();
    }
    /**
     * Get all queued messages (read-only).
     * Combines session queue and compaction queue.
     */
    getAllQueuedMessages() {
        return {
            steering: [
                ...this.session.getSteeringMessages(),
                ...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
            ],
            followUp: [
                ...this.session.getFollowUpMessages(),
                ...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
            ],
        };
    }
    /**
     * Clear all queued messages and return their contents.
     * Clears both session queue and compaction queue.
     */
    clearAllQueues() {
        const { steering, followUp } = this.session.clearQueue();
        const compactionSteering = this.compactionQueuedMessages
            .filter((msg) => msg.mode === "steer")
            .map((msg) => msg.text);
        const compactionFollowUp = this.compactionQueuedMessages
            .filter((msg) => msg.mode === "followUp")
            .map((msg) => msg.text);
        this.compactionQueuedMessages = [];
        return {
            steering: [...steering, ...compactionSteering],
            followUp: [...followUp, ...compactionFollowUp],
        };
    }
    updatePendingMessagesDisplay() {
        this.pendingMessagesContainer.clear();
        const { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();
        if (steeringMessages.length > 0 || followUpMessages.length > 0) {
            this.pendingMessagesContainer.addChild(new Spacer(1));
            for (const message of steeringMessages) {
                const text = theme.fg("dim", `Steering: ${message}`);
                this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
            }
            for (const message of followUpMessages) {
                const text = theme.fg("dim", `Follow-up: ${message}`);
                this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
            }
            const dequeueHint = this.getAppKeyDisplay("app.message.dequeue");
            const hintText = theme.fg("dim", `↳ ${dequeueHint} to edit all queued messages`);
            this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
        }
    }
    restoreQueuedMessagesToEditor(options) {
        const { steering, followUp } = this.clearAllQueues();
        const allQueued = [...steering, ...followUp];
        if (allQueued.length === 0) {
            this.updatePendingMessagesDisplay();
            if (options?.abort) {
                this.agent.abort();
            }
            return 0;
        }
        const queuedText = allQueued.join("\n\n");
        const currentText = options?.currentText ?? this.editor.getText();
        const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
        this.editor.setText(combinedText);
        this.updatePendingMessagesDisplay();
        if (options?.abort) {
            this.agent.abort();
        }
        return allQueued.length;
    }
    queueCompactionMessage(text, mode) {
        this.compactionQueuedMessages.push({ text, mode });
        this.editor.addToHistory?.(text);
        this.editor.setText("");
        this.updatePendingMessagesDisplay();
        this.showStatus("Queued message for after compaction");
    }
    isExtensionCommand(text) {
        if (!text.startsWith("/"))
            return false;
        const extensionRunner = this.session.extensionRunner;
        if (!extensionRunner)
            return false;
        const spaceIndex = text.indexOf(" ");
        const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
        return !!extensionRunner.getCommand(commandName);
    }
    async flushCompactionQueue(options) {
        if (this.compactionQueuedMessages.length === 0) {
            return;
        }
        const queuedMessages = [...this.compactionQueuedMessages];
        this.compactionQueuedMessages = [];
        this.updatePendingMessagesDisplay();
        const restoreQueue = (error) => {
            this.session.clearQueue();
            this.compactionQueuedMessages = queuedMessages;
            this.updatePendingMessagesDisplay();
            this.showError(`Failed to send queued message${queuedMessages.length > 1 ? "s" : ""}: ${error instanceof Error ? error.message : String(error)}`);
        };
        try {
            if (options?.willRetry) {
                // When retry is pending, queue messages for the retry turn
                for (const message of queuedMessages) {
                    if (this.isExtensionCommand(message.text)) {
                        await this.session.prompt(message.text);
                    }
                    else if (message.mode === "followUp") {
                        await this.session.followUp(message.text);
                    }
                    else {
                        await this.session.steer(message.text);
                    }
                }
                this.updatePendingMessagesDisplay();
                return;
            }
            // Find first non-extension-command message to use as prompt
            const firstPromptIndex = queuedMessages.findIndex((message) => !this.isExtensionCommand(message.text));
            if (firstPromptIndex === -1) {
                // All extension commands - execute them all
                for (const message of queuedMessages) {
                    await this.session.prompt(message.text);
                }
                return;
            }
            // Execute any extension commands before the first prompt
            const preCommands = queuedMessages.slice(0, firstPromptIndex);
            const firstPrompt = queuedMessages[firstPromptIndex];
            const rest = queuedMessages.slice(firstPromptIndex + 1);
            for (const message of preCommands) {
                await this.session.prompt(message.text);
            }
            // Send first prompt (starts streaming)
            const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
                restoreQueue(error);
            });
            // Queue remaining messages
            for (const message of rest) {
                if (this.isExtensionCommand(message.text)) {
                    await this.session.prompt(message.text);
                }
                else if (message.mode === "followUp") {
                    await this.session.followUp(message.text);
                }
                else {
                    await this.session.steer(message.text);
                }
            }
            this.updatePendingMessagesDisplay();
            void promptPromise;
        }
        catch (error) {
            restoreQueue(error);
        }
    }
    /** Move pending bash components from pending area to chat */
    flushPendingBashComponents() {
        for (const component of this.pendingBashComponents) {
            this.pendingMessagesContainer.removeChild(component);
            this.chatContainer.addChild(component);
        }
        this.pendingBashComponents = [];
    }
    // =========================================================================
    // Selectors
    // =========================================================================
    /**
     * Shows a selector component in place of the editor.
     * @param create Factory that receives a `done` callback and returns the component and focus target
     */
    showSelector(create) {
        const done = () => {
            this.editorContainer.clear();
            this.editorContainer.addChild(this.editor);
            this.ui.setFocus(this.editor);
        };
        const { component, focus } = create(done);
        this.editorContainer.clear();
        this.editorContainer.addChild(component);
        this.ui.setFocus(focus);
        this.ui.requestRender();
    }
    showSettingsSelector() {
        this.showSelector((done) => {
            const selector = new SettingsSelectorComponent({
                autoCompact: this.session.autoCompactionEnabled,
                showImages: this.settingsManager.getShowImages(),
                autoResizeImages: this.settingsManager.getImageAutoResize(),
                blockImages: this.settingsManager.getBlockImages(),
                enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
                steeringMode: this.session.steeringMode,
                followUpMode: this.session.followUpMode,
                transport: this.settingsManager.getTransport(),
                thinkingLevel: this.session.thinkingLevel,
                availableThinkingLevels: this.session.getAvailableThinkingLevels(),
                currentTheme: this.settingsManager.getTheme() || "dark",
                availableThemes: getAvailableThemes(),
                hideThinkingBlock: this.hideThinkingBlock,
                collapseChangelog: this.settingsManager.getCollapseChangelog(),
                doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
                treeFilterMode: this.settingsManager.getTreeFilterMode(),
                showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
                editorPaddingX: this.settingsManager.getEditorPaddingX(),
                autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
                quietStartup: this.settingsManager.getQuietStartup(),
                clearOnShrink: this.settingsManager.getClearOnShrink(),
            }, {
                onAutoCompactChange: (enabled) => {
                    this.session.setAutoCompactionEnabled(enabled);
                    this.footer.setAutoCompactEnabled(enabled);
                },
                onShowImagesChange: (enabled) => {
                    this.settingsManager.setShowImages(enabled);
                    for (const child of this.chatContainer.children) {
                        if (child instanceof ToolExecutionComponent) {
                            child.setShowImages(enabled);
                        }
                    }
                },
                onAutoResizeImagesChange: (enabled) => {
                    this.settingsManager.setImageAutoResize(enabled);
                },
                onBlockImagesChange: (blocked) => {
                    this.settingsManager.setBlockImages(blocked);
                },
                onEnableSkillCommandsChange: (enabled) => {
                    this.settingsManager.setEnableSkillCommands(enabled);
                    this.setupAutocomplete(this.fdPath);
                },
                onSteeringModeChange: (mode) => {
                    this.session.setSteeringMode(mode);
                },
                onFollowUpModeChange: (mode) => {
                    this.session.setFollowUpMode(mode);
                },
                onTransportChange: (transport) => {
                    this.settingsManager.setTransport(transport);
                    this.session.agent.setTransport(transport);
                },
                onThinkingLevelChange: (level) => {
                    this.session.setThinkingLevel(level);
                    this.footer.invalidate();
                    this.updateEditorBorderColor();
                },
                onThemeChange: (themeName) => {
                    const result = setTheme(themeName, true);
                    this.settingsManager.setTheme(themeName);
                    this.ui.invalidate();
                    if (!result.success) {
                        this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`);
                    }
                },
                onThemePreview: (themeName) => {
                    const result = setTheme(themeName, true);
                    if (result.success) {
                        this.ui.invalidate();
                        this.ui.requestRender();
                    }
                },
                onHideThinkingBlockChange: (hidden) => {
                    this.hideThinkingBlock = hidden;
                    this.settingsManager.setHideThinkingBlock(hidden);
                    for (const child of this.chatContainer.children) {
                        if (child instanceof AssistantMessageComponent) {
                            child.setHideThinkingBlock(hidden);
                        }
                    }
                    this.chatContainer.clear();
                    this.rebuildChatFromMessages();
                },
                onCollapseChangelogChange: (collapsed) => {
                    this.settingsManager.setCollapseChangelog(collapsed);
                },
                onQuietStartupChange: (enabled) => {
                    this.settingsManager.setQuietStartup(enabled);
                },
                onDoubleEscapeActionChange: (action) => {
                    this.settingsManager.setDoubleEscapeAction(action);
                },
                onTreeFilterModeChange: (mode) => {
                    this.settingsManager.setTreeFilterMode(mode);
                },
                onShowHardwareCursorChange: (enabled) => {
                    this.settingsManager.setShowHardwareCursor(enabled);
                    this.ui.setShowHardwareCursor(enabled);
                },
                onEditorPaddingXChange: (padding) => {
                    this.settingsManager.setEditorPaddingX(padding);
                    this.defaultEditor.setPaddingX(padding);
                    if (this.editor !== this.defaultEditor && this.editor.setPaddingX !== undefined) {
                        this.editor.setPaddingX(padding);
                    }
                },
                onAutocompleteMaxVisibleChange: (maxVisible) => {
                    this.settingsManager.setAutocompleteMaxVisible(maxVisible);
                    this.defaultEditor.setAutocompleteMaxVisible(maxVisible);
                    if (this.editor !== this.defaultEditor && this.editor.setAutocompleteMaxVisible !== undefined) {
                        this.editor.setAutocompleteMaxVisible(maxVisible);
                    }
                },
                onClearOnShrinkChange: (enabled) => {
                    this.settingsManager.setClearOnShrink(enabled);
                    this.ui.setClearOnShrink(enabled);
                },
                onCancel: () => {
                    done();
                    this.ui.requestRender();
                },
            });
            return { component: selector, focus: selector.getSettingsList() };
        });
    }
    async handleModelCommand(searchTerm) {
        if (!searchTerm) {
            this.showModelSelector();
            return;
        }
        const model = await this.findExactModelMatch(searchTerm);
        if (model) {
            try {
                await this.session.setModel(model);
                this.footer.invalidate();
                this.updateEditorBorderColor();
                this.showStatus(`Model: ${model.id}`);
                this.checkDaxnutsEasterEgg(model);
            }
            catch (error) {
                this.showError(error instanceof Error ? error.message : String(error));
            }
            return;
        }
        this.showModelSelector(searchTerm);
    }
    async findExactModelMatch(searchTerm) {
        const models = await this.getModelCandidates();
        return findExactModelReferenceMatch(searchTerm, models);
    }
    async getModelCandidates() {
        if (this.session.scopedModels.length > 0) {
            return this.session.scopedModels.map((scoped) => scoped.model);
        }
        this.session.modelRegistry.refresh();
        try {
            return await this.session.modelRegistry.getAvailable();
        }
        catch {
            return [];
        }
    }
    /** Update the footer's available provider count from current model candidates */
    async updateAvailableProviderCount() {
        const models = await this.getModelCandidates();
        const uniqueProviders = new Set(models.map((m) => m.provider));
        this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
    }
    showModelSelector(initialSearchInput) {
        this.showSelector((done) => {
            const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
                try {
                    await this.session.setModel(model);
                    this.footer.invalidate();
                    this.updateEditorBorderColor();
                    done();
                    this.showStatus(`Model: ${model.id}`);
                    this.checkDaxnutsEasterEgg(model);
                }
                catch (error) {
                    done();
                    this.showError(error instanceof Error ? error.message : String(error));
                }
            }, () => {
                done();
                this.ui.requestRender();
            }, initialSearchInput);
            return { component: selector, focus: selector };
        });
    }
    async showModelsSelector() {
        // Get all available models
        this.session.modelRegistry.refresh();
        const allModels = this.session.modelRegistry.getAvailable();
        if (allModels.length === 0) {
            this.showStatus("No models available");
            return;
        }
        // Check if session has scoped models (from previous session-only changes or CLI --models)
        const sessionScopedModels = this.session.scopedModels;
        const hasSessionScope = sessionScopedModels.length > 0;
        // Build enabled model IDs from session state or settings
        const enabledModelIds = new Set();
        let hasFilter = false;
        if (hasSessionScope) {
            // Use current session's scoped models
            for (const sm of sessionScopedModels) {
                enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);
            }
            hasFilter = true;
        }
        else {
            // Fall back to settings
            const patterns = this.settingsManager.getEnabledModels();
            if (patterns !== undefined && patterns.length > 0) {
                hasFilter = true;
                const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
                for (const sm of scopedModels) {
                    enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`);
                }
            }
        }
        // Track current enabled state (session-only until persisted)
        const currentEnabledIds = new Set(enabledModelIds);
        let currentHasFilter = hasFilter;
        // Helper to update session's scoped models (session-only, no persist)
        const updateSessionModels = async (enabledIds) => {
            if (enabledIds.size > 0 && enabledIds.size < allModels.length) {
                const newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry);
                this.session.setScopedModels(newScopedModels.map((sm) => ({
                    model: sm.model,
                    thinkingLevel: sm.thinkingLevel,
                })));
            }
            else {
                // All enabled or none enabled = no filter
                this.session.setScopedModels([]);
            }
            await this.updateAvailableProviderCount();
            this.ui.requestRender();
        };
        this.showSelector((done) => {
            const selector = new ScopedModelsSelectorComponent({
                allModels,
                enabledModelIds: currentEnabledIds,
                hasEnabledModelsFilter: currentHasFilter,
            }, {
                onModelToggle: async (modelId, enabled) => {
                    if (enabled) {
                        currentEnabledIds.add(modelId);
                    }
                    else {
                        currentEnabledIds.delete(modelId);
                    }
                    currentHasFilter = true;
                    await updateSessionModels(currentEnabledIds);
                },
                onEnableAll: async (allModelIds) => {
                    currentEnabledIds.clear();
                    for (const id of allModelIds) {
                        currentEnabledIds.add(id);
                    }
                    currentHasFilter = false;
                    await updateSessionModels(currentEnabledIds);
                },
                onClearAll: async () => {
                    currentEnabledIds.clear();
                    currentHasFilter = true;
                    await updateSessionModels(currentEnabledIds);
                },
                onToggleProvider: async (_provider, modelIds, enabled) => {
                    for (const id of modelIds) {
                        if (enabled) {
                            currentEnabledIds.add(id);
                        }
                        else {
                            currentEnabledIds.delete(id);
                        }
                    }
                    currentHasFilter = true;
                    await updateSessionModels(currentEnabledIds);
                },
                onPersist: (enabledIds) => {
                    // Persist to settings
                    const newPatterns = enabledIds.length === allModels.length
                        ? undefined // All enabled = clear filter
                        : enabledIds;
                    this.settingsManager.setEnabledModels(newPatterns);
                    this.showStatus("Model selection saved to settings");
                },
                onCancel: () => {
                    done();
                    this.ui.requestRender();
                },
            });
            return { component: selector, focus: selector };
        });
    }
    showUserMessageSelector() {
        const userMessages = this.session.getUserMessagesForForking();
        if (userMessages.length === 0) {
            this.showStatus("No messages to fork from");
            return;
        }
        this.showSelector((done) => {
            const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
                const result = await this.session.fork(entryId);
                if (result.cancelled) {
                    // Extension cancelled the fork
                    done();
                    this.ui.requestRender();
                    return;
                }
                this.chatContainer.clear();
                this.renderInitialMessages();
                this.editor.setText(result.selectedText);
                done();
                this.showStatus("Branched to new session");
            }, () => {
                done();
                this.ui.requestRender();
            });
            return { component: selector, focus: selector.getMessageList() };
        });
    }
    showTreeSelector(initialSelectedId) {
        const tree = this.sessionManager.getTree();
        const realLeafId = this.sessionManager.getLeafId();
        const initialFilterMode = this.settingsManager.getTreeFilterMode();
        if (tree.length === 0) {
            this.showStatus("No entries in session");
            return;
        }
        this.showSelector((done) => {
            const selector = new TreeSelectorComponent(tree, realLeafId, this.ui.terminal.rows, async (entryId) => {
                // Selecting the current leaf is a no-op (already there)
                if (entryId === realLeafId) {
                    done();
                    this.showStatus("Already at this point");
                    return;
                }
                // Ask about summarization
                done(); // Close selector first
                // Loop until user makes a complete choice or cancels to tree
                let wantsSummary = false;
                let customInstructions;
                // Check if we should skip the prompt (user preference to always default to no summary)
                if (!this.settingsManager.getBranchSummarySkipPrompt()) {
                    while (true) {
                        const summaryChoice = await this.showExtensionSelector("Summarize branch?", [
                            "No summary",
                            "Summarize",
                            "Summarize with custom prompt",
                        ]);
                        if (summaryChoice === undefined) {
                            // User pressed escape - re-show tree selector with same selection
                            this.showTreeSelector(entryId);
                            return;
                        }
                        wantsSummary = summaryChoice !== "No summary";
                        if (summaryChoice === "Summarize with custom prompt") {
                            customInstructions = await this.showExtensionEditor("Custom summarization instructions");
                            if (customInstructions === undefined) {
                                // User cancelled - loop back to summary selector
                                continue;
                            }
                        }
                        // User made a complete choice
                        break;
                    }
                }
                // Set up escape handler and loader if summarizing
                let summaryLoader;
                const originalOnEscape = this.defaultEditor.onEscape;
                if (wantsSummary) {
                    this.defaultEditor.onEscape = () => {
                        this.session.abortBranchSummary();
                    };
                    this.chatContainer.addChild(new Spacer(1));
                    summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
                    this.statusContainer.addChild(summaryLoader);
                    this.ui.requestRender();
                }
                try {
                    const result = await this.session.navigateTree(entryId, {
                        summarize: wantsSummary,
                        customInstructions,
                    });
                    if (result.aborted) {
                        // Summarization aborted - re-show tree selector with same selection
                        this.showStatus("Branch summarization cancelled");
                        this.showTreeSelector(entryId);
                        return;
                    }
                    if (result.cancelled) {
                        this.showStatus("Navigation cancelled");
                        return;
                    }
                    // Update UI
                    this.chatContainer.clear();
                    this.renderInitialMessages();
                    if (result.editorText && !this.editor.getText().trim()) {
                        this.editor.setText(result.editorText);
                    }
                    this.showStatus("Navigated to selected point");
                }
                catch (error) {
                    this.showError(error instanceof Error ? error.message : String(error));
                }
                finally {
                    if (summaryLoader) {
                        summaryLoader.stop();
                        this.statusContainer.clear();
                    }
                    this.defaultEditor.onEscape = originalOnEscape;
                }
            }, () => {
                done();
                this.ui.requestRender();
            }, (entryId, label) => {
                this.sessionManager.appendLabelChange(entryId, label);
                this.ui.requestRender();
            }, initialSelectedId, initialFilterMode);
            return { component: selector, focus: selector };
        });
    }
    showSessionSelector() {
        this.showSelector((done) => {
            const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), SessionManager.listAll, async (sessionPath) => {
                done();
                await this.handleResumeSession(sessionPath);
            }, () => {
                done();
                this.ui.requestRender();
            }, () => {
                void this.shutdown();
            }, () => this.ui.requestRender(), {
                renameSession: async (sessionFilePath, nextName) => {
                    const next = (nextName ?? "").trim();
                    if (!next)
                        return;
                    const mgr = SessionManager.open(sessionFilePath);
                    mgr.appendSessionInfo(next);
                },
                showRenameHint: true,
                keybindings: this.keybindings,
            }, this.sessionManager.getSessionFile());
            return { component: selector, focus: selector };
        });
    }
    async handleResumeSession(sessionPath) {
        // Stop loading animation
        if (this.loadingAnimation) {
            this.loadingAnimation.stop();
            this.loadingAnimation = undefined;
        }
        this.statusContainer.clear();
        // Clear UI state
        this.pendingMessagesContainer.clear();
        this.compactionQueuedMessages = [];
        this.streamingComponent = undefined;
        this.streamingMessage = undefined;
        this.pendingTools.clear();
        // Switch session via AgentSession (emits extension session events)
        await this.session.switchSession(sessionPath);
        // Clear and re-render the chat
        this.chatContainer.clear();
        this.renderInitialMessages();
        this.showStatus("Resumed session");
    }
    async showOAuthSelector(mode) {
        if (mode === "logout") {
            const providers = this.session.modelRegistry.authStorage.list();
            const loggedInProviders = providers.filter((p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth");
            if (loggedInProviders.length === 0) {
                this.showStatus("No OAuth providers logged in. Use /login first.");
                return;
            }
        }
        this.showSelector((done) => {
            const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (providerId) => {
                done();
                if (mode === "login") {
                    await this.showLoginDialog(providerId);
                }
                else {
                    // Logout flow
                    const providerInfo = this.session.modelRegistry.authStorage
                        .getOAuthProviders()
                        .find((p) => p.id === providerId);
                    const providerName = providerInfo?.name || providerId;
                    try {
                        this.session.modelRegistry.authStorage.logout(providerId);
                        this.session.modelRegistry.refresh();
                        await this.updateAvailableProviderCount();
                        this.showStatus(`Logged out of ${providerName}`);
                    }
                    catch (error) {
                        this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
                    }
                }
            }, () => {
                done();
                this.ui.requestRender();
            });
            return { component: selector, focus: selector };
        });
    }
    async showLoginDialog(providerId) {
        const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
        const providerName = providerInfo?.name || providerId;
        // Providers that use callback servers (can paste redirect URL)
        const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
        // Create login dialog component
        const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
            // Completion handled below
        });
        // Show dialog in editor container
        this.editorContainer.clear();
        this.editorContainer.addChild(dialog);
        this.ui.setFocus(dialog);
        this.ui.requestRender();
        // Promise for manual code input (racing with callback server)
        let manualCodeResolve;
        let manualCodeReject;
        const manualCodePromise = new Promise((resolve, reject) => {
            manualCodeResolve = resolve;
            manualCodeReject = reject;
        });
        // Restore editor helper
        const restoreEditor = () => {
            this.editorContainer.clear();
            this.editorContainer.addChild(this.editor);
            this.ui.setFocus(this.editor);
            this.ui.requestRender();
        };
        try {
            await this.session.modelRegistry.authStorage.login(providerId, {
                onAuth: (info) => {
                    dialog.showAuth(info.url, info.instructions);
                    if (usesCallbackServer) {
                        // Show input for manual paste, racing with callback
                        dialog
                            .showManualInput("Paste redirect URL below, or complete login in browser:")
                            .then((value) => {
                            if (value && manualCodeResolve) {
                                manualCodeResolve(value);
                                manualCodeResolve = undefined;
                            }
                        })
                            .catch(() => {
                            if (manualCodeReject) {
                                manualCodeReject(new Error("Login cancelled"));
                                manualCodeReject = undefined;
                            }
                        });
                    }
                    else if (providerId === "github-copilot") {
                        // GitHub Copilot polls after onAuth
                        dialog.showWaiting("Waiting for browser authentication...");
                    }
                    // For Anthropic: onPrompt is called immediately after
                },
                onPrompt: async (prompt) => {
                    return dialog.showPrompt(prompt.message, prompt.placeholder);
                },
                onProgress: (message) => {
                    dialog.showProgress(message);
                },
                onManualCodeInput: () => manualCodePromise,
                signal: dialog.signal,
            });
            // Success
            restoreEditor();
            this.session.modelRegistry.refresh();
            await this.updateAvailableProviderCount();
            this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
        }
        catch (error) {
            restoreEditor();
            const errorMsg = error instanceof Error ? error.message : String(error);
            if (errorMsg !== "Login cancelled") {
                this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
            }
        }
    }
    // =========================================================================
    // Command handlers
    // =========================================================================
    async handleReloadCommand() {
        if (this.session.isStreaming) {
            this.showWarning("Wait for the current response to finish before reloading.");
            return;
        }
        if (this.session.isCompacting) {
            this.showWarning("Wait for compaction to finish before reloading.");
            return;
        }
        this.resetExtensionUI();
        const loader = new BorderedLoader(this.ui, theme, "Reloading keybindings, extensions, skills, prompts, themes...", {
            cancellable: false,
        });
        const previousEditor = this.editor;
        this.editorContainer.clear();
        this.editorContainer.addChild(loader);
        this.ui.setFocus(loader);
        this.ui.requestRender();
        const dismissLoader = (editor) => {
            loader.dispose();
            this.editorContainer.clear();
            this.editorContainer.addChild(editor);
            this.ui.setFocus(editor);
            this.ui.requestRender();
        };
        try {
            await this.session.reload();
            this.keybindings.reload();
            setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
            this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
            const themeName = this.settingsManager.getTheme();
            const themeResult = themeName ? setTheme(themeName, true) : { success: true };
            if (!themeResult.success) {
                this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`);
            }
            const editorPaddingX = this.settingsManager.getEditorPaddingX();
            const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
            this.defaultEditor.setPaddingX(editorPaddingX);
            this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
            if (this.editor !== this.defaultEditor) {
                this.editor.setPaddingX?.(editorPaddingX);
                this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
            }
            this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
            this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
            this.setupAutocomplete(this.fdPath);
            const runner = this.session.extensionRunner;
            if (runner) {
                this.setupExtensionShortcuts(runner);
            }
            this.rebuildChatFromMessages();
            dismissLoader(this.editor);
            this.showLoadedResources({
                force: false,
                showDiagnosticsWhenQuiet: true,
            });
            const modelsJsonError = this.session.modelRegistry.getError();
            if (modelsJsonError) {
                this.showError(`models.json error: ${modelsJsonError}`);
            }
            this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes");
        }
        catch (error) {
            dismissLoader(previousEditor);
            this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    async handleExportCommand(text) {
        const parts = text.split(/\s+/);
        const outputPath = parts.length > 1 ? parts[1] : undefined;
        try {
            if (outputPath?.endsWith(".jsonl")) {
                const filePath = this.session.exportToJsonl(outputPath);
                this.showStatus(`Session exported to: ${filePath}`);
            }
            else {
                const filePath = await this.session.exportToHtml(outputPath);
                this.showStatus(`Session exported to: ${filePath}`);
            }
        }
        catch (error) {
            this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
        }
    }
    async handleImportCommand(text) {
        const parts = text.split(/\s+/);
        if (parts.length < 2 || !parts[1]) {
            this.showError("Usage: /import <path.jsonl>");
            return;
        }
        const inputPath = parts[1];
        const confirmed = await this.showExtensionConfirm("Import session", `Replace current session with ${inputPath}?`);
        if (!confirmed) {
            this.showStatus("Import cancelled");
            return;
        }
        try {
            // Stop loading animation
            if (this.loadingAnimation) {
                this.loadingAnimation.stop();
                this.loadingAnimation = undefined;
            }
            this.statusContainer.clear();
            // Clear UI state
            this.pendingMessagesContainer.clear();
            this.compactionQueuedMessages = [];
            this.streamingComponent = undefined;
            this.streamingMessage = undefined;
            this.pendingTools.clear();
            const success = await this.session.importFromJsonl(inputPath);
            if (!success) {
                this.showWarning("Import cancelled");
                return;
            }
            // Clear and re-render the chat
            this.chatContainer.clear();
            this.renderInitialMessages();
            this.showStatus(`Session imported from: ${inputPath}`);
        }
        catch (error) {
            this.showError(`Failed to import session: ${error instanceof Error ? error.message : "Unknown error"}`);
        }
    }
    async handleShareCommand() {
        // Check if gh is available and logged in
        try {
            const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
            if (authResult.status !== 0) {
                this.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
                return;
            }
        }
        catch {
            this.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
            return;
        }
        // Export to a temp file
        const tmpFile = path.join(os.tmpdir(), "session.html");
        try {
            await this.session.exportToHtml(tmpFile);
        }
        catch (error) {
            this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
            return;
        }
        // Show cancellable loader, replacing the editor
        const loader = new BorderedLoader(this.ui, theme, "Creating gist...");
        this.editorContainer.clear();
        this.editorContainer.addChild(loader);
        this.ui.setFocus(loader);
        this.ui.requestRender();
        const restoreEditor = () => {
            loader.dispose();
            this.editorContainer.clear();
            this.editorContainer.addChild(this.editor);
            this.ui.setFocus(this.editor);
            try {
                fs.unlinkSync(tmpFile);
            }
            catch {
                // Ignore cleanup errors
            }
        };
        // Create a secret gist asynchronously
        let proc = null;
        loader.onAbort = () => {
            proc?.kill();
            restoreEditor();
            this.showStatus("Share cancelled");
        };
        try {
            const result = await new Promise((resolve) => {
                proc = spawn("gh", ["gist", "create", "--public=false", tmpFile]);
                let stdout = "";
                let stderr = "";
                proc.stdout?.on("data", (data) => {
                    stdout += data.toString();
                });
                proc.stderr?.on("data", (data) => {
                    stderr += data.toString();
                });
                proc.on("close", (code) => resolve({ stdout, stderr, code }));
            });
            if (loader.signal.aborted)
                return;
            restoreEditor();
            if (result.code !== 0) {
                const errorMsg = result.stderr?.trim() || "Unknown error";
                this.showError(`Failed to create gist: ${errorMsg}`);
                return;
            }
            // Extract gist ID from the URL returned by gh
            // gh returns something like: https://gist.github.com/username/GIST_ID
            const gistUrl = result.stdout?.trim();
            const gistId = gistUrl?.split("/").pop();
            if (!gistId) {
                this.showError("Failed to parse gist ID from gh output");
                return;
            }
            // Create the preview URL
            const previewUrl = getShareViewerUrl(gistId);
            this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
        }
        catch (error) {
            if (!loader.signal.aborted) {
                restoreEditor();
                this.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
            }
        }
    }
    async handleCopyCommand() {
        const text = this.session.getLastAssistantText();
        if (!text) {
            this.showError("No agent messages to copy yet.");
            return;
        }
        try {
            await copyToClipboard(text);
            this.showStatus("Copied last agent message to clipboard");
        }
        catch (error) {
            this.showError(error instanceof Error ? error.message : String(error));
        }
    }
    handleNameCommand(text) {
        const name = text.replace(/^\/name\s*/, "").trim();
        if (!name) {
            const currentName = this.sessionManager.getSessionName();
            if (currentName) {
                this.chatContainer.addChild(new Spacer(1));
                this.chatContainer.addChild(new Text(theme.fg("dim", `Session name: ${currentName}`), 1, 0));
            }
            else {
                this.showWarning("Usage: /name <name>");
            }
            this.ui.requestRender();
            return;
        }
        this.sessionManager.appendSessionInfo(name);
        this.updateTerminalTitle();
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
        this.ui.requestRender();
    }
    handleSessionCommand() {
        const stats = this.session.getSessionStats();
        const sessionName = this.sessionManager.getSessionName();
        let info = `${theme.bold("Session Info")}\n\n`;
        if (sessionName) {
            info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
        }
        info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
        info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
        info += `${theme.bold("Messages")}\n`;
        info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
        info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
        info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;
        info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
        info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
        info += `${theme.bold("Tokens")}\n`;
        info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
        info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
        if (stats.tokens.cacheRead > 0) {
            info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
        }
        if (stats.tokens.cacheWrite > 0) {
            info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
        }
        info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
        if (stats.cost > 0) {
            info += `\n${theme.bold("Cost")}\n`;
            info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
        }
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new Text(info, 1, 0));
        this.ui.requestRender();
    }
    handleChangelogCommand() {
        const changelogPath = getChangelogPath();
        const allEntries = parseChangelog(changelogPath);
        const changelogMarkdown = allEntries.length > 0
            ? allEntries
                .reverse()
                .map((e) => e.content)
                .join("\n\n")
            : "No changelog entries found.";
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new DynamicBorder());
        this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));
        this.chatContainer.addChild(new DynamicBorder());
        this.ui.requestRender();
    }
    /**
     * Capitalize keybinding for display (e.g., "ctrl+c" -> "Ctrl+C").
     */
    capitalizeKey(key) {
        return key
            .split("/")
            .map((k) => k
            .split("+")
            .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
            .join("+"))
            .join("/");
    }
    /**
     * Get capitalized display string for an app keybinding action.
     */
    getAppKeyDisplay(action) {
        return this.capitalizeKey(keyText(action));
    }
    /**
     * Get capitalized display string for an editor keybinding action.
     */
    getEditorKeyDisplay(action) {
        return this.capitalizeKey(keyText(action));
    }
    handleHotkeysCommand() {
        // Navigation keybindings
        const cursorUp = this.getEditorKeyDisplay("tui.editor.cursorUp");
        const cursorDown = this.getEditorKeyDisplay("tui.editor.cursorDown");
        const cursorLeft = this.getEditorKeyDisplay("tui.editor.cursorLeft");
        const cursorRight = this.getEditorKeyDisplay("tui.editor.cursorRight");
        const cursorWordLeft = this.getEditorKeyDisplay("tui.editor.cursorWordLeft");
        const cursorWordRight = this.getEditorKeyDisplay("tui.editor.cursorWordRight");
        const cursorLineStart = this.getEditorKeyDisplay("tui.editor.cursorLineStart");
        const cursorLineEnd = this.getEditorKeyDisplay("tui.editor.cursorLineEnd");
        const jumpForward = this.getEditorKeyDisplay("tui.editor.jumpForward");
        const jumpBackward = this.getEditorKeyDisplay("tui.editor.jumpBackward");
        const pageUp = this.getEditorKeyDisplay("tui.editor.pageUp");
        const pageDown = this.getEditorKeyDisplay("tui.editor.pageDown");
        // Editing keybindings
        const submit = this.getEditorKeyDisplay("tui.input.submit");
        const newLine = this.getEditorKeyDisplay("tui.input.newLine");
        const deleteWordBackward = this.getEditorKeyDisplay("tui.editor.deleteWordBackward");
        const deleteWordForward = this.getEditorKeyDisplay("tui.editor.deleteWordForward");
        const deleteToLineStart = this.getEditorKeyDisplay("tui.editor.deleteToLineStart");
        const deleteToLineEnd = this.getEditorKeyDisplay("tui.editor.deleteToLineEnd");
        const yank = this.getEditorKeyDisplay("tui.editor.yank");
        const yankPop = this.getEditorKeyDisplay("tui.editor.yankPop");
        const undo = this.getEditorKeyDisplay("tui.editor.undo");
        const tab = this.getEditorKeyDisplay("tui.input.tab");
        // App keybindings
        const interrupt = this.getAppKeyDisplay("app.interrupt");
        const clear = this.getAppKeyDisplay("app.clear");
        const exit = this.getAppKeyDisplay("app.exit");
        const suspend = this.getAppKeyDisplay("app.suspend");
        const cycleThinkingLevel = this.getAppKeyDisplay("app.thinking.cycle");
        const cycleModelForward = this.getAppKeyDisplay("app.model.cycleForward");
        const selectModel = this.getAppKeyDisplay("app.model.select");
        const expandTools = this.getAppKeyDisplay("app.tools.expand");
        const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
        const externalEditor = this.getAppKeyDisplay("app.editor.external");
        const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
        const followUp = this.getAppKeyDisplay("app.message.followUp");
        const dequeue = this.getAppKeyDisplay("app.message.dequeue");
        const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
        let hotkeys = `
**Navigation**
| Key | Action |
|-----|--------|
| \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | Move cursor / browse history (Up when empty) |
| \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
| \`${cursorLineStart}\` | Start of line |
| \`${cursorLineEnd}\` | End of line |
| \`${jumpForward}\` | Jump forward to character |
| \`${jumpBackward}\` | Jump backward to character |
| \`${pageUp}\` / \`${pageDown}\` | Scroll by page |

**Editing**
| Key | Action |
|-----|--------|
| \`${submit}\` | Send message |
| \`${newLine}\` | New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""} |
| \`${deleteWordBackward}\` | Delete word backwards |
| \`${deleteWordForward}\` | Delete word forwards |
| \`${deleteToLineStart}\` | Delete to start of line |
| \`${deleteToLineEnd}\` | Delete to end of line |
| \`${yank}\` | Paste the most-recently-deleted text |
| \`${yankPop}\` | Cycle through the deleted text after pasting |
| \`${undo}\` | Undo |

**Other**
| Key | Action |
|-----|--------|
| \`${tab}\` | Path completion / accept autocomplete |
| \`${interrupt}\` | Cancel autocomplete / abort streaming |
| \`${clear}\` | Clear editor (first) / exit (second) |
| \`${exit}\` | Exit (when editor is empty) |
| \`${suspend}\` | Suspend to background |
| \`${cycleThinkingLevel}\` | Cycle thinking level |
| \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
| \`${selectModel}\` | Open model selector |
| \`${expandTools}\` | Toggle tool output expansion |
| \`${toggleThinking}\` | Toggle thinking block visibility |
| \`${externalEditor}\` | Edit message in external editor |
| \`${followUp}\` | Queue follow-up message |
| \`${dequeue}\` | Restore queued messages |
| \`${pasteImage}\` | Paste image from clipboard |
| \`/\` | Slash commands |
| \`!\` | Run bash command |
| \`!!\` | Run bash command (excluded from context) |
`;
        // Add extension-registered shortcuts
        const extensionRunner = this.session.extensionRunner;
        if (extensionRunner) {
            const shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());
            if (shortcuts.size > 0) {
                hotkeys += `
**Extensions**
| Key | Action |
|-----|--------|
`;
                for (const [key, shortcut] of shortcuts) {
                    const description = shortcut.description ?? shortcut.extensionPath;
                    const keyDisplay = key.replace(/\b\w/g, (c) => c.toUpperCase());
                    hotkeys += `| \`${keyDisplay}\` | ${description} |\n`;
                }
            }
        }
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new DynamicBorder());
        this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));
        this.chatContainer.addChild(new DynamicBorder());
        this.ui.requestRender();
    }
    async handleClearCommand() {
        // Stop loading animation
        if (this.loadingAnimation) {
            this.loadingAnimation.stop();
            this.loadingAnimation = undefined;
        }
        this.statusContainer.clear();
        // New session via session (emits extension session events)
        await this.session.newSession();
        // Clear UI state
        this.headerContainer.clear();
        this.chatContainer.clear();
        this.pendingMessagesContainer.clear();
        this.compactionQueuedMessages = [];
        this.streamingComponent = undefined;
        this.streamingMessage = undefined;
        this.pendingTools.clear();
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
        this.ui.requestRender();
    }
    handleDebugCommand() {
        const width = this.ui.terminal.columns;
        const height = this.ui.terminal.rows;
        const allLines = this.ui.render(width);
        const debugLogPath = getDebugLogPath();
        const debugData = [
            `Debug output at ${new Date().toISOString()}`,
            `Terminal: ${width}x${height}`,
            `Total lines: ${allLines.length}`,
            "",
            "=== All rendered lines with visible widths ===",
            ...allLines.map((line, idx) => {
                const vw = visibleWidth(line);
                const escaped = JSON.stringify(line);
                return `[${idx}] (w=${vw}) ${escaped}`;
            }),
            "",
            "=== Agent messages (JSONL) ===",
            ...this.session.messages.map((msg) => JSON.stringify(msg)),
            "",
        ].join("\n");
        fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
        fs.writeFileSync(debugLogPath, debugData);
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, 1, 1));
        this.ui.requestRender();
    }
    handleArminSaysHi() {
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new ArminComponent(this.ui));
        this.ui.requestRender();
    }
    handleDaxnuts() {
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new DaxnutsComponent(this.ui));
        this.ui.requestRender();
    }
    checkDaxnutsEasterEgg(model) {
        if (model.provider === "opencode" && model.id.toLowerCase().includes("kimi-k2.5")) {
            this.handleDaxnuts();
        }
    }
    async handleBashCommand(command, excludeFromContext = false) {
        const extensionRunner = this.session.extensionRunner;
        // Emit user_bash event to let extensions intercept
        const eventResult = extensionRunner
            ? await extensionRunner.emitUserBash({
                type: "user_bash",
                command,
                excludeFromContext,
                cwd: process.cwd(),
            })
            : undefined;
        // If extension returned a full result, use it directly
        if (eventResult?.result) {
            const result = eventResult.result;
            // Create UI component for display
            this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
            if (this.session.isStreaming) {
                this.pendingMessagesContainer.addChild(this.bashComponent);
                this.pendingBashComponents.push(this.bashComponent);
            }
            else {
                this.chatContainer.addChild(this.bashComponent);
            }
            // Show output and complete
            if (result.output) {
                this.bashComponent.appendOutput(result.output);
            }
            this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
            // Record the result in session
            this.session.recordBashResult(command, result, { excludeFromContext });
            this.bashComponent = undefined;
            this.ui.requestRender();
            return;
        }
        // Normal execution path (possibly with custom operations)
        const isDeferred = this.session.isStreaming;
        this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
        if (isDeferred) {
            // Show in pending area when agent is streaming
            this.pendingMessagesContainer.addChild(this.bashComponent);
            this.pendingBashComponents.push(this.bashComponent);
        }
        else {
            // Show in chat immediately when agent is idle
            this.chatContainer.addChild(this.bashComponent);
        }
        this.ui.requestRender();
        try {
            const result = await this.session.executeBash(command, (chunk) => {
                if (this.bashComponent) {
                    this.bashComponent.appendOutput(chunk);
                    this.ui.requestRender();
                }
            }, { excludeFromContext, operations: eventResult?.operations });
            if (this.bashComponent) {
                this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
            }
        }
        catch (error) {
            if (this.bashComponent) {
                this.bashComponent.setComplete(undefined, false);
            }
            this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
        }
        this.bashComponent = undefined;
        this.ui.requestRender();
    }
    async handleCompactCommand(customInstructions) {
        const entries = this.sessionManager.getEntries();
        const messageCount = entries.filter((e) => e.type === "message").length;
        if (messageCount < 2) {
            this.showWarning("Nothing to compact (no messages yet)");
            return;
        }
        if (this.loadingAnimation) {
            this.loadingAnimation.stop();
            this.loadingAnimation = undefined;
        }
        this.statusContainer.clear();
        try {
            await this.session.compact(customInstructions);
        }
        catch {
            // Ignore, will be emitted as an event
        }
    }
    stop() {
        if (this.loadingAnimation) {
            this.loadingAnimation.stop();
            this.loadingAnimation = undefined;
        }
        this.clearExtensionTerminalInputListeners();
        this.footer.dispose();
        this.footerDataProvider.dispose();
        if (this.unsubscribe) {
            this.unsubscribe();
        }
        if (this.isInitialized) {
            this.ui.stop();
            this.isInitialized = false;
        }
    }
}
//# sourceMappingURL=interactive-mode.js.map