/**
 * 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 { getProviders, } from "@earendil-works/pi-ai";
import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
import chalk from "chalk";
import { spawn, spawnSync } from "child_process";
import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
import { parseSkillBlock } from "../../core/agent-session.js";
import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
import { FooterDataProvider } from "../../core/footer-data-provider.js";
import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
import { KeybindingsManager } from "../../core/keybindings.js";
import { createCompactionSummaryMessage } from "../../core/messages.js";
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
import { DefaultPackageManager } from "../../core/package-manager.js";
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
import { SessionManager } from "../../core/session-manager.js";
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.js";
import { getChangelogPath, getNewEntries, normalizeChangelogLinks, 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 { getCwdRelativePath } from "../../utils/paths.js";
import { getPiUserAgent } from "../../utils/pi-user-agent.js";
import { killTrackedDetachedChildren } from "../../utils/shell.js";
import { ensureTool } from "../../utils/tools-manager.js";
import { checkForNewPiVersion } from "../../utils/version-check.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 { CountdownTimer } from "./components/countdown-timer.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 { EarendilAnnouncementComponent } from "./components/earendil-announcement.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 { formatKeyText, keyDisplayText, 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 { TrustSelectorComponent } from "./components/trust-selector.js";
import { UserMessageComponent } from "./components/user-message.js";
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
import { detectTerminalBackgroundTheme, getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
function isExpandable(obj) {
    return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
}
class ExpandableText extends Text {
    getCollapsedText;
    getExpandedText;
    constructor(getCollapsedText, getExpandedText, expanded = false, paddingX = 0, paddingY = 0) {
        super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY);
        this.getCollapsedText = getCollapsedText;
        this.getExpandedText = getExpandedText;
    }
    setExpanded(expanded) {
        this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
    }
}
const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
function isDeadTerminalError(error) {
    if (!error || typeof error !== "object" || !("code" in error)) {
        return false;
    }
    const code = error.code;
    return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
}
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
function isAnthropicSubscriptionAuthKey(apiKey) {
    return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
}
function isUnknownModel(model) {
    return !!model && model.provider === "unknown" && model.id === "unknown" && model.api === "unknown";
}
function quoteIfNeeded(value) {
    if (value.length > 0 && !/[^a-zA-Z0-9_\-./~:@]/.test(value)) {
        return value;
    }
    return `'${value.replace(/'/g, `'\\''`)}'`;
}
export function formatResumeCommand(sessionManager) {
    if (!process.stdout.isTTY)
        return undefined;
    if (!sessionManager.isPersisted())
        return undefined;
    const sessionFile = sessionManager.getSessionFile();
    if (!sessionFile || !fs.existsSync(sessionFile))
        return undefined;
    const args = [APP_NAME];
    if (!sessionManager.usesDefaultSessionDir()) {
        args.push("--session-dir", quoteIfNeeded(sessionManager.getSessionDir()));
    }
    args.push("--session", sessionManager.getSessionId());
    return args.join(" ");
}
function hasDefaultModelProvider(providerId) {
    return providerId in defaultModelPerProvider;
}
const BEDROCK_PROVIDER_ID = "amazon-bedrock";
const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
    if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
        return true;
    }
    if (builtInProviderIds.has(providerId)) {
        return false;
    }
    return !oauthProviderIds.has(providerId);
}
export class InteractiveMode {
    runtimeHost;
    ui;
    chatContainer;
    pendingMessagesContainer;
    statusContainer;
    defaultEditor;
    editor;
    editorComponentFactory;
    autocompleteProvider;
    autocompleteProviderWrappers = [];
    fdPath;
    editorContainer;
    footer;
    footerDataProvider;
    // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
    keybindings;
    version;
    isInitialized = false;
    onInputCallback;
    pendingUserInputs = [];
    loadingAnimation = undefined;
    workingMessage = undefined;
    workingVisible = true;
    workingIndicatorOptions = undefined;
    defaultWorkingMessage = "Working...";
    defaultHiddenThinkingLabel = "Thinking...";
    hiddenThinkingLabel = this.defaultHiddenThinkingLabel;
    lastSigintTime = 0;
    lastEscapeTime = 0;
    changelogMarkdown = undefined;
    startupNoticesShown = false;
    anthropicSubscriptionWarningShown = false;
    // 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;
    signalCleanupHandlers = [];
    // 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;
    retryCountdown = 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;
    options;
    autoTrustOnReloadCwd;
    // Convenience accessors
    get session() {
        return this.runtimeHost.session;
    }
    get agent() {
        return this.session.agent;
    }
    get sessionManager() {
        return this.session.sessionManager;
    }
    get settingsManager() {
        return this.session.settingsManager;
    }
    constructor(runtimeHost, options = {}) {
        this.runtimeHost = runtimeHost;
        this.options = options;
        this.autoTrustOnReloadCwd = options.autoTrustOnReloadCwd;
        this.runtimeHost.setBeforeSessionInvalidate(() => {
            this.resetExtensionUI();
        });
        this.runtimeHost.setRebindSession(async () => {
            await this.rebindCurrentSession();
        });
        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.sessionManager.getCwd());
        this.footer = new FooterComponent(this.session, this.footerDataProvider);
        this.footer.setAutoCompactEnabled(this.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);
    }
    async detectThemeIfUnset() {
        if (this.settingsManager.getTheme()) {
            return;
        }
        const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 });
        const result = setTheme(detection.theme, true);
        if (!result.success) {
            return;
        }
        if (detection.confidence === "high") {
            this.settingsManager.setTheme(detection.theme);
            await this.settingsManager.flush();
        }
        this.updateEditorBorderColor();
        this.ui.requestRender();
    }
    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) {
        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,
        }));
    }
    createBaseAutocompleteProvider() {
        // 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),
            ...(cmd.argumentHint && { argumentHint: cmd.argumentHint }),
        }));
        // 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),
                });
            }
        }
        return new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], this.sessionManager.getCwd(), this.fdPath);
    }
    setupAutocompleteProvider() {
        let provider = this.createBaseAutocompleteProvider();
        const triggerCharacters = [];
        for (const wrapProvider of this.autocompleteProviderWrappers) {
            provider = wrapProvider(provider);
            triggerCharacters.push(...(provider.triggerCharacters ?? []));
        }
        if (triggerCharacters.length > 0) {
            provider.triggerCharacters = [...new Set(triggerCharacters)];
        }
        this.autocompleteProvider = provider;
        this.defaultEditor.setAutocompleteProvider(provider);
        if (this.editor !== this.defaultEditor) {
            this.editor.setAutocompleteProvider?.(provider);
        }
    }
    showStartupNoticesIfNeeded() {
        if (this.startupNoticesShown) {
            return;
        }
        this.startupNoticesShown = true;
        if (!this.changelogMarkdown) {
            return;
        }
        if (this.chatContainer.children.length > 0) {
            this.chatContainer.addChild(new Spacer(1));
        }
        this.chatContainer.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.chatContainer.addChild(new Text(condensedText, 1, 0));
        }
        else {
            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(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()));
            this.chatContainer.addChild(new Spacer(1));
        }
        this.chatContainer.addChild(new DynamicBorder());
    }
    async init() {
        if (this.isInitialized)
            return;
        this.registerSignalHandlers();
        // 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;
        if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) {
            const modelList = this.session.scopedModels
                .map((sm) => {
                const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : "";
                return `${sm.model.id}${thinkingStr}`;
            })
                .join(", ");
            const cycleKeys = this.keybindings.getKeys("app.model.cycleForward");
            const cycleHint = cycleKeys.length > 0
                ? theme.fg("muted", ` (${formatKeyText(cycleKeys.join("/"), { capitalize: true })} to cycle)`)
                : "";
            console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
        }
        // Add header container as first child. Populate it after detectThemeIfUnset.
        this.ui.addChild(this.headerContainer);
        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;
        await this.detectThemeIfUnset();
        // 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 expandedInstructions = [
                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 compactInstructions = [
                hint("app.interrupt", "interrupt"),
                rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"),
                rawKeyHint("/", "commands"),
                rawKeyHint("!", "bash"),
                hint("app.tools.expand", "more"),
            ].join(theme.fg("muted", " · "));
            const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
            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 ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
            // Setup UI layout
            this.headerContainer.addChild(new Spacer(1));
            this.headerContainer.addChild(this.builtInHeader);
            this.headerContainer.addChild(new Spacer(1));
        }
        else {
            // Minimal header when silenced
            this.builtInHeader = new Text("", 0, 0);
            this.headerContainer.addChild(this.builtInHeader);
        }
        this.ui.requestRender();
        // Initialize extensions first so resources are shown before messages
        await this.rebindCurrentSession();
        // Render initial messages AFTER showing loaded resources
        this.renderInitialMessages();
        // 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(this.sessionManager.getCwd());
        const sessionName = this.sessionManager.getSessionName();
        if (sessionName) {
            this.ui.terminal.setTitle(`${APP_TITLE} - ${sessionName} - ${cwdBasename}`);
        }
        else {
            this.ui.terminal.setTitle(`${APP_TITLE} - ${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
        checkForNewPiVersion(this.version).then((newRelease) => {
            if (newRelease) {
                this.showNewVersionNotification(newRelease);
            }
        });
        // 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);
        }
        void this.maybeWarnAboutAnthropicSubscriptionAuth();
        // 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);
            }
        }
    }
    async checkForPackageUpdates() {
        if (process.env.PI_OFFLINE) {
            return [];
        }
        try {
            const packageManager = new DefaultPackageManager({
                cwd: this.sessionManager.getCwd(),
                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 - record the version, send telemetry, don't show changelog
            this.settingsManager.setLastChangelogVersion(VERSION);
            this.reportInstallTelemetry(VERSION);
            return undefined;
        }
        const newEntries = getNewEntries(entries, lastVersion);
        if (newEntries.length > 0) {
            this.settingsManager.setLastChangelogVersion(VERSION);
            this.reportInstallTelemetry(VERSION);
            return newEntries.map((e) => normalizeChangelogLinks(e.content, e)).join("\n\n");
        }
        return undefined;
    }
    reportInstallTelemetry(version) {
        if (process.env.PI_OFFLINE) {
            return;
        }
        if (!isInstallTelemetryEnabled(this.settingsManager)) {
            return;
        }
        void fetch(`https://pi.dev/api/report-install?version=${encodeURIComponent(version)}`, {
            headers: {
                "User-Agent": getPiUserAgent(version),
            },
            signal: AbortSignal.timeout(5000),
        })
            .then(() => undefined)
            .catch(() => 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;
    }
    formatExtensionDisplayPath(path) {
        let result = this.formatDisplayPath(path);
        result = result.replace(/\/index\.ts$/, "").replace(/\/index\.js$/, "");
        return result;
    }
    formatContextPath(p) {
        const cwd = path.resolve(this.sessionManager.getCwd());
        const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);
        const relativePath = getCwdRelativePath(absolutePath, cwd);
        if (relativePath !== undefined) {
            return relativePath;
        }
        return this.formatDisplayPath(absolutePath);
    }
    getStartupExpansionState() {
        return this.options.verbose || this.toolOutputExpanded;
    }
    /**
     * Get a short path relative to the package root for display.
     */
    getShortPath(fullPath, sourceInfo) {
        const baseDir = sourceInfo?.baseDir;
        if (baseDir && this.isPackageSource(sourceInfo)) {
            const relativePath = path.relative(path.resolve(baseDir), path.resolve(fullPath));
            if (relativePath &&
                relativePath !== "." &&
                !relativePath.startsWith("..") &&
                !relativePath.startsWith(`..${path.sep}`) &&
                !path.isAbsolute(relativePath)) {
                return relativePath.replace(/\\/g, "/");
            }
        }
        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);
    }
    getCompactPathLabel(resourcePath, sourceInfo) {
        const shortPath = this.getShortPath(resourcePath, sourceInfo);
        const normalizedPath = shortPath.replace(/\\/g, "/");
        const segments = normalizedPath.split("/").filter((segment) => segment.length > 0 && segment !== "~");
        if (segments.length > 0) {
            return segments[segments.length - 1];
        }
        return shortPath;
    }
    getCompactPackageSourceLabel(sourceInfo) {
        const source = sourceInfo?.source ?? "";
        if (source.startsWith("npm:")) {
            return source.slice("npm:".length) || source;
        }
        const gitSource = parseGitUrl(source);
        if (gitSource) {
            return gitSource.path || source;
        }
        return source;
    }
    getCompactExtensionLabel(resourcePath, sourceInfo) {
        if (!this.isPackageSource(sourceInfo)) {
            return this.getCompactPathLabel(resourcePath, sourceInfo);
        }
        const sourceLabel = this.getCompactPackageSourceLabel(sourceInfo);
        if (!sourceLabel) {
            return this.getCompactPathLabel(resourcePath, sourceInfo);
        }
        const shortPath = this.getShortPath(resourcePath, sourceInfo).replace(/\\/g, "/");
        const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
        const parsedPath = path.posix.parse(packagePath);
        if (parsedPath.name === "index") {
            return !parsedPath.dir || parsedPath.dir === "." ? sourceLabel : `${sourceLabel}:${parsedPath.dir}`;
        }
        return `${sourceLabel}:${packagePath}`;
    }
    getCompactDisplayPathSegments(resourcePath) {
        return this.formatDisplayPath(resourcePath)
            .replace(/\\/g, "/")
            .split("/")
            .filter((segment) => segment.length > 0 && segment !== "~");
    }
    getCompactNonPackageExtensionLabel(resourcePath, index, allPaths) {
        const segments = allPaths[index]?.segments;
        if (!segments || segments.length === 0) {
            return this.getCompactPathLabel(resourcePath);
        }
        for (let segmentCount = 1; segmentCount <= segments.length; segmentCount += 1) {
            const candidate = segments.slice(-segmentCount).join("/");
            const isUnique = allPaths.every((item, itemIndex) => {
                if (itemIndex === index) {
                    return true;
                }
                return item.segments.slice(-segmentCount).join("/") !== candidate;
            });
            if (isUnique) {
                return candidate;
            }
        }
        return segments.join("/");
    }
    getCompactExtensionLabels(extensions) {
        const nonPackageExtensions = extensions
            .map((extension) => {
            const segments = this.getCompactDisplayPathSegments(extension.path);
            const lastSegment = segments[segments.length - 1];
            if (segments.length > 1 && (lastSegment === "index.ts" || lastSegment === "index.js")) {
                segments.pop();
            }
            return {
                path: extension.path,
                sourceInfo: extension.sourceInfo,
                segments,
            };
        })
            .filter((extension) => !this.isPackageSource(extension.sourceInfo));
        return extensions.map((extension) => {
            if (this.isPackageSource(extension.sourceInfo)) {
                return this.getCompactExtensionLabel(extension.path, extension.sourceInfo);
            }
            const nonPackageIndex = nonPackageExtensions.findIndex((item) => item.path === extension.path);
            if (nonPackageIndex === -1) {
                return this.getCompactPathLabel(extension.path, extension.sourceInfo);
            }
            return this.getCompactNonPackageExtensionLabel(extension.path, nonPackageIndex, nonPackageExtensions);
        });
    }
    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 formatCompactList = (items, options) => {
            const labels = items.map((item) => item.trim()).filter((item) => item.length > 0);
            if (options?.sort !== false) {
                labels.sort((a, b) => a.localeCompare(b));
            }
            return theme.fg("dim", `  ${labels.join(", ")}`);
        };
        const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
            const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
            this.chatContainer.addChild(section);
            this.chatContainer.addChild(new Spacer(1));
        };
        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");
                const contextCompactList = formatCompactList(contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)), { sort: false });
                addLoadedSection("Context", contextCompactList, contextList);
            }
            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),
                });
                const skillCompactList = formatCompactList(skills.map((skill) => skill.name));
                addLoadedSection("Skills", skillCompactList, skillList);
            }
            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);
                    },
                });
                const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
                addLoadedSection("Prompts", promptCompactList, templateList);
            }
            if (extensions.length > 0) {
                const groups = this.buildScopeGroups(extensions);
                const extList = this.formatScopeGroups(groups, {
                    formatPath: (item) => this.formatExtensionDisplayPath(item.path),
                    formatPackagePath: (item) => this.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),
                });
                const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));
                addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading");
            }
            // 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),
                });
                const themeCompactList = formatCompactList(customThemes.map((loadedTheme) => loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo)));
                addLoadedSection("Themes", themeCompactList, themeList);
            }
        }
        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 bindCurrentSessionExtensions() {
        const uiContext = this.createExtensionUIContext();
        await this.session.bindExtensions({
            uiContext,
            mode: "tui",
            abortHandler: () => {
                this.restoreQueuedMessagesToEditor({ abort: true });
            },
            commandContextActions: {
                waitForIdle: () => this.session.agent.waitForIdle(),
                newSession: async (options) => {
                    if (this.loadingAnimation) {
                        this.loadingAnimation.stop();
                        this.loadingAnimation = undefined;
                    }
                    this.statusContainer.clear();
                    try {
                        const result = await this.runtimeHost.newSession(options);
                        if (!result.cancelled) {
                            this.renderCurrentSessionState();
                            this.ui.requestRender();
                        }
                        return result;
                    }
                    catch (error) {
                        return this.handleFatalRuntimeError("Failed to create session", error);
                    }
                },
                fork: async (entryId, options) => {
                    try {
                        const result = await this.runtimeHost.fork(entryId, options);
                        if (!result.cancelled) {
                            this.renderCurrentSessionState();
                            this.editor.setText(result.selectedText ?? "");
                            this.showStatus("Forked to new session");
                        }
                        return { cancelled: result.cancelled };
                    }
                    catch (error) {
                        return this.handleFatalRuntimeError("Failed to fork session", error);
                    }
                },
                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");
                    void this.flushCompactionQueue({ willRetry: false });
                    return { cancelled: false };
                },
                switchSession: async (sessionPath, options) => {
                    return this.handleResumeSession(sessionPath, options);
                },
                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.setupAutocompleteProvider();
        const extensionRunner = this.session.extensionRunner;
        this.setupExtensionShortcuts(extensionRunner);
        this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
        this.showStartupNoticesIfNeeded();
    }
    applyRuntimeSettings() {
        configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
        this.footer.setSession(this.session);
        this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
        this.footerDataProvider.setCwd(this.sessionManager.getCwd());
        this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
        this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
        this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
        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);
        }
    }
    async rebindCurrentSession() {
        this.unsubscribe?.();
        this.unsubscribe = undefined;
        this.applyRuntimeSettings();
        await this.bindCurrentSessionExtensions();
        this.subscribeToAgent();
        await this.updateAvailableProviderCount();
        this.updateEditorBorderColor();
        this.updateTerminalTitle();
    }
    async handleFatalRuntimeError(prefix, error) {
        const message = error instanceof Error ? error.message : String(error);
        this.showError(`${prefix}: ${message}`);
        stopThemeWatcher();
        this.stop();
        process.exit(1);
    }
    renderCurrentSessionState() {
        this.chatContainer.clear();
        this.pendingMessagesContainer.clear();
        this.compactionQueuedMessages = [];
        this.streamingComponent = undefined;
        this.streamingMessage = undefined;
        this.pendingTools.clear();
        this.renderInitialMessages();
    }
    /**
     * 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(),
            mode: "tui",
            hasUI: true,
            cwd: this.sessionManager.getCwd(),
            sessionManager: this.sessionManager,
            modelRegistry: this.session.modelRegistry,
            model: this.session.model,
            isIdle: () => !this.session.isStreaming,
            isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
            signal: this.session.agent.signal,
            abort: () => {
                this.restoreQueuedMessagesToEditor({ abort: true });
            },
            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();
    }
    getWorkingLoaderMessage() {
        return this.workingMessage ?? this.defaultWorkingMessage;
    }
    createWorkingLoader() {
        return new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.getWorkingLoaderMessage(), this.workingIndicatorOptions);
    }
    stopWorkingLoader() {
        if (this.loadingAnimation) {
            this.loadingAnimation.stop();
            this.loadingAnimation = undefined;
        }
        this.statusContainer.clear();
    }
    setWorkingVisible(visible) {
        this.workingVisible = visible;
        if (!visible) {
            this.stopWorkingLoader();
            this.ui.requestRender();
            return;
        }
        if (this.session.isStreaming && !this.loadingAnimation) {
            this.statusContainer.clear();
            this.loadingAnimation = this.createWorkingLoader();
            this.statusContainer.addChild(this.loadingAnimation);
        }
        this.ui.requestRender();
    }
    setWorkingIndicator(options) {
        this.workingIndicatorOptions = options;
        this.loadingAnimation?.setIndicator(options);
        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.autocompleteProviderWrappers = [];
        this.setCustomEditorComponent(undefined);
        this.setupAutocompleteProvider();
        this.defaultEditor.onExtensionShortcut = undefined;
        this.updateTerminalTitle();
        this.workingMessage = undefined;
        this.workingVisible = true;
        this.setWorkingIndicator();
        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 (isExpandable(this.customHeader)) {
                this.customHeader.setExpanded(this.toolOutputExpanded);
            }
            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 (isExpandable(this.builtInHeader)) {
                this.builtInHeader.setExpanded(this.toolOutputExpanded);
            }
            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.
     */
    createProjectTrustContext(cwd) {
        const ui = this.createExtensionUIContext();
        return {
            cwd,
            mode: "tui",
            hasUI: true,
            ui: {
                select: ui.select,
                confirm: ui.confirm,
                input: ui.input,
                notify: ui.notify,
            },
        };
    }
    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) => {
                this.workingMessage = message;
                if (this.loadingAnimation) {
                    this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
                }
            },
            setWorkingVisible: (visible) => this.setWorkingVisible(visible),
            setWorkingIndicator: (options) => this.setWorkingIndicator(options),
            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),
            addAutocompleteProvider: (factory) => {
                this.autocompleteProviderWrappers.push(factory);
                this.setupAutocompleteProvider();
            },
            setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
            getEditorComponent: () => this.editorComponentFactory,
            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, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() });
            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";
    }
    async promptForMissingSessionCwd(error) {
        const confirmed = await this.showExtensionConfirm("Session cwd not found", formatMissingSessionCwdPrompt(error.issue));
        return confirmed ? error.issue.fallbackCwd : undefined;
    }
    /**
     * 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) {
        this.editorComponentFactory = 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.session.isStreaming) {
                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 === "/export" || text.startsWith("/export ")) {
                await this.handleExportCommand(text);
                this.editor.setText("");
                return;
            }
            if (text === "/import" || 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 === "/clone") {
                this.editor.setText("");
                await this.handleCloneCommand();
                return;
            }
            if (text === "/tree") {
                this.showTreeSelector();
                this.editor.setText("");
                return;
            }
            if (text === "/trust") {
                this.showTrustSelector();
                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 === "/dementedelves") {
                this.handleDementedDelves();
                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);
            }
            else {
                this.pendingUserInputs.push(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":
                this.pendingTools.clear();
                if (this.settingsManager.getShowTerminalProgress()) {
                    this.ui.terminal.setProgress(true);
                }
                // 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.retryCountdown) {
                    this.retryCountdown.dispose();
                    this.retryCountdown = undefined;
                }
                if (this.retryLoader) {
                    this.retryLoader.stop();
                    this.retryLoader = undefined;
                }
                this.stopWorkingLoader();
                if (this.workingVisible) {
                    this.loadingAnimation = this.createWorkingLoader();
                    this.statusContainer.addChild(this.loadingAnimation);
                }
                this.ui.requestRender();
                break;
            case "queue_update":
                this.updatePendingMessagesDisplay();
                this.ui.requestRender();
                break;
            case "session_info_changed":
                this.updateTerminalTitle();
                this.footer.invalidate();
                this.ui.requestRender();
                break;
            case "thinking_level_changed":
                this.footer.invalidate();
                this.updateEditorBorderColor();
                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(),
                                    imageWidthCells: this.settingsManager.getImageWidthCells(),
                                }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
                                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(),
                        imageWidthCells: this.settingsManager.getImageWidthCells(),
                    }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
                    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.settingsManager.getShowTerminalProgress()) {
                    this.ui.terminal.setProgress(false);
                }
                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": {
                if (this.settingsManager.getShowTerminalProgress()) {
                    this.ui.terminal.setProgress(true);
                }
                // 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.settingsManager.getShowTerminalProgress()) {
                    this.ui.terminal.setProgress(false);
                }
                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();
                this.retryCountdown?.dispose();
                const retryMessage = (seconds) => `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText("app.interrupt")} to cancel)`;
                this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), retryMessage(Math.ceil(event.delayMs / 1000)));
                this.retryCountdown = new CountdownTimer(event.delayMs, this.ui, (seconds) => {
                    this.retryLoader?.setMessage(retryMessage(seconds));
                }, () => {
                    this.retryCountdown = undefined;
                });
                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;
                }
                if (this.retryCountdown) {
                    this.retryCountdown.dispose();
                    this.retryCountdown = 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) {
                    if (this.chatContainer.children.length > 0) {
                        this.chatContainer.addChild(new Spacer(1));
                    }
                    const skillBlock = parseSkillBlock(textContent);
                    if (skillBlock) {
                        // Render skill block (collapsible)
                        const component = new SkillInvocationMessageComponent(skillBlock, this.getMarkdownThemeWithSettings());
                        component.setExpanded(this.toolOutputExpanded);
                        this.chatContainer.addChild(component);
                        // Render user message separately if present
                        if (skillBlock.userMessage) {
                            this.chatContainer.addChild(new Spacer(1));
                            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();
        const renderedPendingTools = new Map();
        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(),
                            imageWidthCells: this.settingsManager.getImageWidthCells(),
                        }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
                        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 {
                            renderedPendingTools.set(content.id, component);
                        }
                    }
                }
            }
            else if (message.role === "toolResult") {
                // Match tool results to pending tool components
                const component = renderedPendingTools.get(message.toolCallId);
                if (component) {
                    component.updateResult(message);
                    renderedPendingTools.delete(message.toolCallId);
                }
            }
            else {
                // All other messages use standard rendering
                this.addMessageToChat(message, options);
            }
        }
        for (const [toolCallId, component] of renderedPendingTools) {
            this.pendingTools.set(toolCallId, component);
        }
        this.ui.requestRender();
    }
    renderInitialMessages() {
        // Get aligned messages and entries from session context
        const context = this.sessionManager.buildSessionContext();
        this.renderSessionContext(context, {
            updateFooter: true,
            populateHistory: true,
        });
        this.renderProjectTrustWarningIfNeeded();
        // 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}`);
        }
    }
    renderProjectTrustWarningIfNeeded() {
        if (this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(this.sessionManager.getCwd())) {
            return;
        }
        if (this.chatContainer.children.length > 0) {
            this.chatContainer.addChild(new Spacer(1));
        }
        this.chatContainer.addChild(new Text(theme.fg("warning", "This project is not trusted. Project .pi resources and packages are ignored. Use /trust to save a trust decision, then restart pi."), 1, 0));
    }
    async getUserInput() {
        const queuedInput = this.pendingUserInputs.shift();
        if (queuedInput !== undefined) {
            return queuedInput;
        }
        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.
     * Stops the TUI before emitting shutdown events so extension UI cleanup cannot
     * repaint the final frame while the process is exiting.
     */
    isShuttingDown = false;
    async shutdown(options) {
        if (this.isShuttingDown)
            return;
        this.isShuttingDown = true;
        // Keep signal handlers registered until terminal cleanup has completed.
        // `signal-exit` checks the listener list during the same SIGTERM/SIGHUP
        // dispatch and re-sends the signal if only its own listeners remain.
        if (options?.fromSignal) {
            // Signal-triggered shutdown (SIGTERM/SIGHUP). Emit extension cleanup
            // (session_shutdown) BEFORE touching the terminal. Extension teardown
            // such as removing sockets does not write to the tty, so it must not be
            // skipped if a later terminal-restore write fails on a dead or stalled
            // terminal. If the terminal is gone, the restore writes below emit EIO,
            // which the stdout/stderr error handler turns into emergencyTerminalExit;
            // the render loop is already idle, so this cannot hot-spin (see #4144).
            await this.runtimeHost.dispose();
            await this.ui.terminal.drainInput(1000);
            this.stop();
            process.exit(0);
        }
        // Interactive quit (Ctrl+D, Ctrl+C, /quit, extension shutdown()). Stop the
        // TUI before emitting shutdown events so extension UI cleanup cannot repaint
        // the final frame while the process is exiting.
        // 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();
        await this.runtimeHost.dispose();
        const resumeCommand = formatResumeCommand(this.sessionManager);
        if (resumeCommand) {
            process.stdout.write(`${chalk.dim("To resume this session:")} ${resumeCommand}\n`);
        }
        process.exit(0);
    }
    emergencyTerminalExit() {
        this.isShuttingDown = true;
        this.unregisterSignalHandlers();
        killTrackedDetachedChildren();
        // The terminal is gone. Do not run normal shutdown because TUI and
        // extension cleanup can write restore sequences and re-trigger EIO.
        process.exit(129);
    }
    /**
     * Last-resort handler for uncaught exceptions. The TUI puts stdin into raw
     * mode and hides the cursor; without this handler, an uncaught throw from
     * anywhere (e.g. an extension's async `ChildProcess.on("exit")` callback)
     * tears down the process while leaving the terminal in raw mode with no
     * cursor, requiring `stty sane && reset` to recover.
     *
     * Unlike emergencyTerminalExit, the terminal is still alive here, so we
     * call ui.stop() to restore cooked mode, the cursor, and disable bracketed
     * paste / Kitty / modifyOtherKeys sequences.
     */
    uncaughtCrash(error) {
        if (this.isShuttingDown) {
            process.exit(1);
        }
        this.isShuttingDown = true;
        try {
            this.unregisterSignalHandlers();
        }
        catch { }
        try {
            killTrackedDetachedChildren();
        }
        catch { }
        try {
            this.ui.stop();
        }
        catch { }
        console.error("pi exiting due to uncaughtException:");
        console.error(error);
        process.exit(1);
    }
    /**
     * Check if shutdown was requested and perform shutdown if so.
     */
    async checkShutdownRequested() {
        if (!this.shutdownRequested)
            return;
        await this.shutdown();
    }
    registerSignalHandlers() {
        this.unregisterSignalHandlers();
        const signals = ["SIGTERM"];
        if (process.platform !== "win32") {
            signals.push("SIGHUP");
        }
        for (const signal of signals) {
            const handler = () => {
                // SIGHUP no longer hard-exits: graceful shutdown emits session_shutdown
                // first, then attempts terminal restore. A genuinely dead terminal
                // surfaces as an EIO on the restore writes, which the stdout/stderr
                // error handler converts into emergencyTerminalExit (see #4144, #5080).
                killTrackedDetachedChildren();
                void this.shutdown({ fromSignal: true });
            };
            process.prependListener(signal, handler);
            this.signalCleanupHandlers.push(() => process.off(signal, handler));
        }
        const terminalErrorHandler = (error) => {
            if (isDeadTerminalError(error)) {
                this.emergencyTerminalExit();
            }
            throw error;
        };
        process.stdout.on("error", terminalErrorHandler);
        process.stderr.on("error", terminalErrorHandler);
        this.signalCleanupHandlers.push(() => process.stdout.off("error", terminalErrorHandler));
        this.signalCleanupHandlers.push(() => process.stderr.off("error", terminalErrorHandler));
        // Restore the terminal before the process dies on any uncaught throw.
        // Without this, an unhandled exception from extension code (or anywhere
        // in pi) leaves the terminal in raw mode with no cursor.
        const uncaughtExceptionHandler = (error) => this.uncaughtCrash(error);
        process.prependListener("uncaughtException", uncaughtExceptionHandler);
        this.signalCleanupHandlers.push(() => process.off("uncaughtException", uncaughtExceptionHandler));
    }
    unregisterSignalHandlers() {
        for (const cleanup of this.signalCleanupHandlers) {
            cleanup();
        }
        this.signalCleanupHandlers = [];
    }
    handleCtrlZ() {
        if (process.platform === "win32") {
            this.showStatus("Suspend to background is not supported on Windows");
            return;
        }
        // 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.setText("");
            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}`);
                void this.maybeWarnAboutAnthropicSubscriptionAuth(result.model);
            }
        }
        catch (error) {
            this.showError(error instanceof Error ? error.message : String(error));
        }
    }
    toggleToolOutputExpansion() {
        this.setToolsExpanded(!this.toolOutputExpanded);
    }
    setToolsExpanded(expanded) {
        this.toolOutputExpanded = expanded;
        const activeHeader = this.customHeader ?? this.builtInHeader;
        if (isExpandable(activeHeader)) {
            activeHeader.setExpanded(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"}`);
    }
    async 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(" ");
            process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`);
            // Do not use spawnSync here. On Windows, synchronous child_process calls can keep
            // Node/libuv's console input read active after ui.stop() pauses stdin, racing
            // vim/nvim for the console input buffer until Ctrl+C cancels the pending read.
            const status = await new Promise((resolve) => {
                const child = spawn(editor, [...editorArgs, tmpFile], {
                    stdio: "inherit",
                    shell: process.platform === "win32",
                });
                child.on("error", () => resolve(null));
                child.on("close", (code) => resolve(code));
            });
            // On successful exit (status 0), replace editor content
            if (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.chatContainer.addChild(new Spacer(1));
        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(release) {
        const action = theme.fg("accent", `${APP_NAME} update`);
        const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action;
        const changelogUrl = "https://pi.dev/changelog";
        const changelogLink = getCapabilities().hyperlinks
            ? hyperlink(theme.fg("accent", "open changelog"), changelogUrl)
            : theme.fg("accent", changelogUrl);
        const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
        const note = release.note?.trim();
        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}`, 1, 0));
        if (note) {
            this.chatContainer.addChild(new Spacer(1));
            this.chatContainer.addChild(new Markdown(note, 1, 0, this.getMarkdownThemeWithSettings(), {
                color: (text) => theme.fg("muted", text),
            }));
            this.chatContainer.addChild(new Spacer(1));
        }
        this.chatContainer.addChild(new Text(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;
        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(),
                imageWidthCells: this.settingsManager.getImageWidthCells(),
                autoResizeImages: this.settingsManager.getImageAutoResize(),
                blockImages: this.settingsManager.getBlockImages(),
                enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
                steeringMode: this.session.steeringMode,
                followUpMode: this.session.followUpMode,
                transport: this.settingsManager.getTransport(),
                httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
                thinkingLevel: this.session.thinkingLevel,
                availableThinkingLevels: this.session.getAvailableThinkingLevels(),
                currentTheme: this.settingsManager.getTheme() || "dark",
                availableThemes: getAvailableThemes(),
                hideThinkingBlock: this.hideThinkingBlock,
                collapseChangelog: this.settingsManager.getCollapseChangelog(),
                enableInstallTelemetry: this.settingsManager.getEnableInstallTelemetry(),
                doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
                treeFilterMode: this.settingsManager.getTreeFilterMode(),
                showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
                defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
                editorPaddingX: this.settingsManager.getEditorPaddingX(),
                autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
                quietStartup: this.settingsManager.getQuietStartup(),
                clearOnShrink: this.settingsManager.getClearOnShrink(),
                showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
                warnings: this.settingsManager.getWarnings(),
            }, {
                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);
                        }
                    }
                },
                onImageWidthCellsChange: (width) => {
                    this.settingsManager.setImageWidthCells(width);
                    for (const child of this.chatContainer.children) {
                        if (child instanceof ToolExecutionComponent) {
                            child.setImageWidthCells(width);
                        }
                    }
                },
                onAutoResizeImagesChange: (enabled) => {
                    this.settingsManager.setImageAutoResize(enabled);
                },
                onBlockImagesChange: (blocked) => {
                    this.settingsManager.setBlockImages(blocked);
                },
                onEnableSkillCommandsChange: (enabled) => {
                    this.settingsManager.setEnableSkillCommands(enabled);
                    this.setupAutocompleteProvider();
                },
                onSteeringModeChange: (mode) => {
                    this.session.setSteeringMode(mode);
                },
                onFollowUpModeChange: (mode) => {
                    this.session.setFollowUpMode(mode);
                },
                onTransportChange: (transport) => {
                    this.settingsManager.setTransport(transport);
                    this.session.agent.transport = transport;
                },
                onHttpIdleTimeoutMsChange: (timeoutMs) => {
                    this.settingsManager.setHttpIdleTimeoutMs(timeoutMs);
                    configureHttpDispatcher(timeoutMs);
                    this.showStatus(`HTTP idle timeout: ${formatHttpIdleTimeoutMs(timeoutMs)}`);
                },
                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);
                },
                onEnableInstallTelemetryChange: (enabled) => {
                    this.settingsManager.setEnableInstallTelemetry(enabled);
                },
                onQuietStartupChange: (enabled) => {
                    this.settingsManager.setQuietStartup(enabled);
                },
                onDefaultProjectTrustChange: (defaultProjectTrust) => {
                    this.settingsManager.setDefaultProjectTrust(defaultProjectTrust);
                },
                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);
                },
                onShowTerminalProgressChange: (enabled) => {
                    this.settingsManager.setShowTerminalProgress(enabled);
                },
                onWarningsChange: (warnings) => {
                    this.settingsManager.setWarnings(warnings);
                },
                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}`);
                void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
                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);
    }
    async maybeWarnAboutAnthropicSubscriptionAuth(model = this.session.model) {
        if (this.settingsManager.getWarnings().anthropicExtraUsage === false) {
            return;
        }
        if (this.anthropicSubscriptionWarningShown) {
            return;
        }
        if (!model || model.provider !== "anthropic") {
            return;
        }
        const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
        if (storedCredential?.type === "oauth") {
            this.anthropicSubscriptionWarningShown = true;
            this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
            return;
        }
        try {
            const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
            if (!isAnthropicSubscriptionAuthKey(apiKey)) {
                return;
            }
            this.anthropicSubscriptionWarningShown = true;
            this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
        }
        catch {
            // Ignore auth lookup failures for warning-only checks.
        }
    }
    maybeSaveImplicitProjectTrustAfterReload() {
        const cwd = this.sessionManager.getCwd();
        if (this.autoTrustOnReloadCwd !== cwd) {
            return false;
        }
        if (!this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(cwd)) {
            return false;
        }
        const trustStore = new ProjectTrustStore(this.runtimeHost.services.agentDir);
        try {
            if (trustStore.get(cwd) !== null) {
                this.autoTrustOnReloadCwd = undefined;
                return false;
            }
            trustStore.set(cwd, true);
            this.autoTrustOnReloadCwd = undefined;
            return true;
        }
        catch (error) {
            this.showWarning(`Could not save project trust after reload: ${error instanceof Error ? error.message : String(error)}`);
            return false;
        }
    }
    showTrustSelector() {
        const cwd = this.sessionManager.getCwd();
        const trustStore = new ProjectTrustStore(this.runtimeHost.services.agentDir);
        const savedDecision = trustStore.getEntry(cwd);
        this.showSelector((done) => {
            const selector = new TrustSelectorComponent({
                cwd,
                savedDecision,
                projectTrusted: this.settingsManager.isProjectTrusted(),
                onSelect: (selection) => {
                    trustStore.setMany(selection.updates);
                    done();
                    this.showStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
                },
                onCancel: () => {
                    done();
                    this.ui.requestRender();
                },
            });
            return { component: selector, focus: selector };
        });
    }
    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}`);
                    void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
                    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
        let currentEnabledIds = null;
        if (hasSessionScope) {
            // Use current session's scoped models
            currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
        }
        else {
            // Fall back to settings
            const patterns = this.settingsManager.getEnabledModels();
            if (patterns !== undefined && patterns.length > 0) {
                const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
                currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
            }
        }
        // Helper to update session's scoped models (session-only, no persist)
        const updateSessionModels = async (enabledIds) => {
            currentEnabledIds = enabledIds === null ? null : [...enabledIds];
            if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
                const newScopedModels = await resolveModelScope(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,
            }, {
                onChange: async (enabledIds) => {
                    await updateSessionModels(enabledIds);
                },
                onPersist: (enabledIds) => {
                    // Persist to settings
                    const newPatterns = enabledIds === null || enabledIds.length === allModels.length
                        ? undefined // All enabled = clear filter
                        : enabledIds;
                    this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
                    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;
        }
        const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
        this.showSelector((done) => {
            const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
                try {
                    const result = await this.runtimeHost.fork(entryId);
                    if (result.cancelled) {
                        done();
                        this.ui.requestRender();
                        return;
                    }
                    this.renderCurrentSessionState();
                    this.editor.setText(result.selectedText ?? "");
                    done();
                    this.showStatus("Forked to new session");
                }
                catch (error) {
                    done();
                    this.showError(error instanceof Error ? error.message : String(error));
                }
            }, () => {
                done();
                this.ui.requestRender();
            }, initialSelectedId);
            return { component: selector, focus: selector.getMessageList() };
        });
    }
    async handleCloneCommand() {
        const leafId = this.sessionManager.getLeafId();
        if (!leafId) {
            this.showStatus("Nothing to clone yet");
            return;
        }
        try {
            const result = await this.runtimeHost.fork(leafId, { position: "at" });
            if (result.cancelled) {
                this.ui.requestRender();
                return;
            }
            this.renderCurrentSessionState();
            this.editor.setText("");
            this.showStatus("Cloned to new session");
        }
        catch (error) {
            this.showError(error instanceof Error ? error.message : String(error));
        }
    }
    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");
                    void this.flushCompactionQueue({ willRetry: false });
                }
                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), (onProgress) => this.sessionManager.usesDefaultSessionDir()
                ? SessionManager.listAll(onProgress)
                : SessionManager.listAll(this.sessionManager.getSessionDir(), onProgress), 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, options) {
        if (this.loadingAnimation) {
            this.loadingAnimation.stop();
            this.loadingAnimation = undefined;
        }
        this.statusContainer.clear();
        try {
            const result = await this.runtimeHost.switchSession(sessionPath, {
                withSession: options?.withSession,
                projectTrustContextFactory: (cwd) => this.createProjectTrustContext(cwd),
            });
            if (result.cancelled) {
                return result;
            }
            this.renderCurrentSessionState();
            this.showStatus("Resumed session");
            return result;
        }
        catch (error) {
            if (error instanceof MissingSessionCwdError) {
                const selectedCwd = await this.promptForMissingSessionCwd(error);
                if (!selectedCwd) {
                    this.showStatus("Resume cancelled");
                    return { cancelled: true };
                }
                const result = await this.runtimeHost.switchSession(sessionPath, {
                    cwdOverride: selectedCwd,
                    withSession: options?.withSession,
                    projectTrustContextFactory: (cwd) => this.createProjectTrustContext(cwd),
                });
                if (result.cancelled) {
                    return result;
                }
                this.renderCurrentSessionState();
                this.showStatus("Resumed session in current cwd");
                return result;
            }
            return this.handleFatalRuntimeError("Failed to resume session", error);
        }
    }
    getLoginProviderOptions(authType) {
        const authStorage = this.session.modelRegistry.authStorage;
        const oauthProviders = authStorage.getOAuthProviders();
        const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
        const options = oauthProviders.map((provider) => ({
            id: provider.id,
            name: provider.name,
            authType: "oauth",
        }));
        const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
        for (const providerId of modelProviders) {
            if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
                continue;
            }
            options.push({
                id: providerId,
                name: this.session.modelRegistry.getProviderDisplayName(providerId),
                authType: "api_key",
            });
        }
        const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
        return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
    }
    getLogoutProviderOptions() {
        const authStorage = this.session.modelRegistry.authStorage;
        const options = [];
        for (const providerId of authStorage.list()) {
            const credential = authStorage.get(providerId);
            if (!credential) {
                continue;
            }
            options.push({
                id: providerId,
                name: this.session.modelRegistry.getProviderDisplayName(providerId),
                authType: credential.type,
            });
        }
        return options.sort((a, b) => a.name.localeCompare(b.name));
    }
    showLoginAuthTypeSelector() {
        const subscriptionLabel = "Use a subscription";
        const apiKeyLabel = "Use an API key";
        this.showSelector((done) => {
            const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
                done();
                const authType = option === subscriptionLabel ? "oauth" : "api_key";
                this.showLoginProviderSelector(authType);
            }, () => {
                done();
                this.ui.requestRender();
            });
            return { component: selector, focus: selector };
        });
    }
    showLoginProviderSelector(authType) {
        const providerOptions = this.getLoginProviderOptions(authType);
        if (providerOptions.length === 0) {
            this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
            return;
        }
        this.showSelector((done) => {
            const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
                done();
                const providerOption = providerOptions.find((provider) => provider.id === providerId);
                if (!providerOption) {
                    return;
                }
                if (providerOption.authType === "oauth") {
                    await this.showLoginDialog(providerOption.id, providerOption.name);
                }
                else if (providerOption.id === BEDROCK_PROVIDER_ID) {
                    this.showBedrockSetupDialog(providerOption.id, providerOption.name);
                }
                else {
                    await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
                }
            }, () => {
                done();
                this.showLoginAuthTypeSelector();
            }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
            return { component: selector, focus: selector };
        });
    }
    async showOAuthSelector(mode) {
        if (mode === "login") {
            this.showLoginAuthTypeSelector();
            return;
        }
        const providerOptions = this.getLogoutProviderOptions();
        if (providerOptions.length === 0) {
            this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
            return;
        }
        this.showSelector((done) => {
            const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
                done();
                const providerOption = providerOptions.find((provider) => provider.id === providerId);
                if (!providerOption) {
                    return;
                }
                try {
                    this.session.modelRegistry.authStorage.logout(providerOption.id);
                    this.session.modelRegistry.refresh();
                    await this.updateAvailableProviderCount();
                    const message = providerOption.authType === "oauth"
                        ? `Logged out of ${providerOption.name}`
                        : `Removed stored API key for ${providerOption.name}. Environment variables and models.json config are unchanged.`;
                    this.showStatus(message);
                }
                catch (error) {
                    this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
                }
            }, () => {
                done();
                this.ui.requestRender();
            });
            return { component: selector, focus: selector };
        });
    }
    async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
        this.session.modelRegistry.refresh();
        const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
        let selectedModel;
        let selectionError;
        if (isUnknownModel(previousModel)) {
            const availableModels = this.session.modelRegistry.getAvailable();
            const providerModels = availableModels.filter((model) => model.provider === providerId);
            if (!hasDefaultModelProvider(providerId)) {
                selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
            }
            else if (providerModels.length === 0) {
                selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
            }
            else {
                const defaultModelId = defaultModelPerProvider[providerId];
                selectedModel = providerModels.find((model) => model.id === defaultModelId);
                if (!selectedModel) {
                    selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
                }
                else {
                    try {
                        await this.session.setModel(selectedModel);
                    }
                    catch (error) {
                        selectedModel = undefined;
                        const errorMessage = error instanceof Error ? error.message : String(error);
                        selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
                    }
                }
            }
        }
        await this.updateAvailableProviderCount();
        this.footer.invalidate();
        this.updateEditorBorderColor();
        if (selectedModel) {
            this.showStatus(`${actionLabel}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
            void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
            this.checkDaxnutsEasterEgg(selectedModel);
        }
        else {
            this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}`);
            if (selectionError) {
                this.showError(selectionError);
            }
            else {
                void this.maybeWarnAboutAnthropicSubscriptionAuth();
            }
        }
    }
    showBedrockSetupDialog(providerId, providerName) {
        const restoreEditor = () => {
            this.editorContainer.clear();
            this.editorContainer.addChild(this.editor);
            this.ui.setFocus(this.editor);
            this.ui.requestRender();
        };
        const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
        dialog.showInfo([
            theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
            theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
            theme.fg("muted", "See:"),
            theme.fg("accent", `  ${path.join(getDocsPath(), "providers.md")}`),
        ]);
        this.editorContainer.clear();
        this.editorContainer.addChild(dialog);
        this.ui.setFocus(dialog);
        this.ui.requestRender();
    }
    async showApiKeyLoginDialog(providerId, providerName) {
        const previousModel = this.session.model;
        const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
            // Completion handled below
        }, providerName);
        this.editorContainer.clear();
        this.editorContainer.addChild(dialog);
        this.ui.setFocus(dialog);
        this.ui.requestRender();
        const restoreEditor = () => {
            this.editorContainer.clear();
            this.editorContainer.addChild(this.editor);
            this.ui.setFocus(this.editor);
            this.ui.requestRender();
        };
        try {
            const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
            if (!apiKey) {
                throw new Error("API key cannot be empty.");
            }
            this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
            restoreEditor();
            await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
        }
        catch (error) {
            restoreEditor();
            const errorMsg = error instanceof Error ? error.message : String(error);
            if (errorMsg !== "Login cancelled") {
                this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
            }
        }
    }
    showOAuthLoginSelect(dialog, prompt) {
        return new Promise((resolve) => {
            const restoreDialog = () => {
                this.editorContainer.clear();
                this.editorContainer.addChild(dialog);
                this.ui.setFocus(dialog);
                this.ui.requestRender();
            };
            const labels = prompt.options.map((option) => option.label);
            const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
                restoreDialog();
                resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
            }, () => {
                restoreDialog();
                resolve(undefined);
            });
            this.editorContainer.clear();
            this.editorContainer.addChild(selector);
            this.ui.setFocus(selector);
            this.ui.requestRender();
        });
    }
    async showLoginDialog(providerId, providerName) {
        const providerInfo = this.session.modelRegistry.authStorage
            .getOAuthProviders()
            .find((provider) => provider.id === providerId);
        const previousModel = this.session.model;
        // 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
        }, providerName);
        // 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;
                            }
                        });
                    }
                    // For Anthropic: onPrompt is called immediately after
                },
                onDeviceCode: (info) => {
                    dialog.showDeviceCode(info);
                    dialog.showWaiting("Waiting for authentication...");
                },
                onPrompt: async (prompt) => {
                    return dialog.showPrompt(prompt.message, prompt.placeholder);
                },
                onProgress: (message) => {
                    dialog.showProgress(message);
                },
                onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
                onManualCodeInput: () => manualCodePromise,
                signal: dialog.signal,
            });
            // Success
            restoreEditor();
            await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
        }
        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 reloadBox = new Container();
        const borderColor = (s) => theme.fg("border", s);
        reloadBox.addChild(new DynamicBorder(borderColor));
        reloadBox.addChild(new Spacer(1));
        reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes..."), 1, 0));
        reloadBox.addChild(new Spacer(1));
        reloadBox.addChild(new DynamicBorder(borderColor));
        const previousEditor = this.editor;
        this.editorContainer.clear();
        this.editorContainer.addChild(reloadBox);
        this.ui.setFocus(reloadBox);
        this.ui.requestRender(true);
        await new Promise((resolve) => process.nextTick(resolve));
        const dismissReloadBox = (editor) => {
            this.editorContainer.clear();
            this.editorContainer.addChild(editor);
            this.ui.setFocus(editor);
            this.ui.requestRender();
        };
        try {
            await this.session.reload();
            configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
            this.keybindings.reload();
            const activeHeader = this.customHeader ?? this.builtInHeader;
            if (isExpandable(activeHeader)) {
                activeHeader.setExpanded(this.toolOutputExpanded);
            }
            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.setupAutocompleteProvider();
            const runner = this.session.extensionRunner;
            this.setupExtensionShortcuts(runner);
            this.rebuildChatFromMessages();
            dismissReloadBox(this.editor);
            this.showLoadedResources({
                force: false,
                showDiagnosticsWhenQuiet: true,
            });
            const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload();
            const modelsJsonError = this.session.modelRegistry.getError();
            if (modelsJsonError) {
                this.showError(`models.json error: ${modelsJsonError}`);
            }
            this.showStatus(savedImplicitProjectTrust
                ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust"
                : "Reloaded keybindings, extensions, skills, prompts, themes");
        }
        catch (error) {
            dismissReloadBox(previousEditor);
            this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    async handleExportCommand(text) {
        const outputPath = this.getPathCommandArgument(text, "/export");
        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"}`);
        }
    }
    getPathCommandArgument(text, command) {
        if (text === command) {
            return undefined;
        }
        if (!text.startsWith(`${command} `)) {
            return undefined;
        }
        const argsString = text.slice(command.length + 1).trimStart();
        if (!argsString) {
            return undefined;
        }
        const firstChar = argsString[0];
        if (firstChar === '"' || firstChar === "'") {
            const closingQuoteIndex = argsString.indexOf(firstChar, 1);
            if (closingQuoteIndex < 0) {
                return undefined;
            }
            return argsString.slice(1, closingQuoteIndex);
        }
        const firstWhitespaceIndex = argsString.search(/\s/);
        if (firstWhitespaceIndex < 0) {
            return argsString;
        }
        return argsString.slice(0, firstWhitespaceIndex);
    }
    async handleImportCommand(text) {
        const inputPath = this.getPathCommandArgument(text, "/import");
        if (!inputPath) {
            this.showError("Usage: /import <path.jsonl>");
            return;
        }
        const confirmed = await this.showExtensionConfirm("Import session", `Replace current session with ${inputPath}?`);
        if (!confirmed) {
            this.showStatus("Import cancelled");
            return;
        }
        try {
            if (this.loadingAnimation) {
                this.loadingAnimation.stop();
                this.loadingAnimation = undefined;
            }
            this.statusContainer.clear();
            const result = await this.runtimeHost.importFromJsonl(inputPath);
            if (result.cancelled) {
                this.showStatus("Import cancelled");
                return;
            }
            this.renderCurrentSessionState();
            this.showStatus(`Session imported from: ${inputPath}`);
        }
        catch (error) {
            if (error instanceof MissingSessionCwdError) {
                const selectedCwd = await this.promptForMissingSessionCwd(error);
                if (!selectedCwd) {
                    this.showStatus("Import cancelled");
                    return;
                }
                const result = await this.runtimeHost.importFromJsonl(inputPath, selectedCwd);
                if (result.cancelled) {
                    this.showStatus("Import cancelled");
                    return;
                }
                this.renderCurrentSessionState();
                this.showStatus(`Session imported from: ${inputPath}`);
                return;
            }
            if (error instanceof SessionImportFileNotFoundError) {
                this.showError(`Failed to import session: ${error.message}`);
                return;
            }
            await this.handleFatalRuntimeError("Failed to import session", 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.session.setSessionName(name);
        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) => normalizeChangelogLinks(e.content, e))
                .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();
    }
    /**
     * Get capitalized display string for an app keybinding action.
     */
    getAppKeyDisplay(action) {
        return keyDisplayText(action);
    }
    /**
     * Get capitalized display string for an editor keybinding action.
     */
    getEditorKeyDisplay(action) {
        return keyDisplayText(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 |
| \`${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;
        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 = formatKeyText(key, { capitalize: true });
                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() {
        if (this.loadingAnimation) {
            this.loadingAnimation.stop();
            this.loadingAnimation = undefined;
        }
        this.statusContainer.clear();
        try {
            const result = await this.runtimeHost.newSession();
            if (result.cancelled) {
                return;
            }
            this.renderCurrentSessionState();
            this.chatContainer.addChild(new Spacer(1));
            this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
            this.ui.requestRender();
        }
        catch (error) {
            await this.handleFatalRuntimeError("Failed to create session", error);
        }
    }
    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();
    }
    handleDementedDelves() {
        this.chatContainer.addChild(new Spacer(1));
        this.chatContainer.addChild(new EarendilAnnouncementComponent());
        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 = await extensionRunner.emitUserBash({
            type: "user_bash",
            command,
            excludeFromContext,
            cwd: this.sessionManager.getCwd(),
        });
        // 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.settingsManager.getShowTerminalProgress()) {
            this.ui.terminal.setProgress(false);
        }
        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;
        }
        this.unregisterSignalHandlers();
    }
}
//# sourceMappingURL=interactive-mode.js.map