/**
 * Extension runner - executes extensions and manages their lifecycle.
 */
import { theme } from "../../modes/interactive/theme/theme.js";
// Keybindings for these actions cannot be overridden by extensions
const RESERVED_ACTIONS_FOR_EXTENSION_CONFLICTS = [
    "interrupt",
    "clear",
    "exit",
    "suspend",
    "cycleThinkingLevel",
    "cycleModelForward",
    "cycleModelBackward",
    "selectModel",
    "expandTools",
    "toggleThinking",
    "externalEditor",
    "followUp",
    "submit",
    "selectConfirm",
    "selectCancel",
    "copy",
    "deleteToLineEnd",
];
const buildBuiltinKeybindings = (effectiveKeybindings) => {
    const builtinKeybindings = {};
    for (const [action, keys] of Object.entries(effectiveKeybindings)) {
        const keyAction = action;
        const keyList = Array.isArray(keys) ? keys : [keys];
        const restrictOverride = RESERVED_ACTIONS_FOR_EXTENSION_CONFLICTS.includes(keyAction);
        for (const key of keyList) {
            const normalizedKey = key.toLowerCase();
            builtinKeybindings[normalizedKey] = {
                action: keyAction,
                restrictOverride: restrictOverride,
            };
        }
    }
    return builtinKeybindings;
};
const noOpUIContext = {
    select: async () => undefined,
    confirm: async () => false,
    input: async () => undefined,
    notify: () => { },
    onTerminalInput: () => () => { },
    setStatus: () => { },
    setWorkingMessage: () => { },
    setWidget: () => { },
    setFooter: () => { },
    setHeader: () => { },
    setTitle: () => { },
    setHiddenThinkingLabel: () => { },
    custom: async () => undefined,
    pasteToEditor: () => { },
    setEditorText: () => { },
    getEditorText: () => "",
    editor: async () => undefined,
    setEditorComponent: () => { },
    get theme() {
        return theme;
    },
    getAllThemes: () => [],
    getTheme: () => undefined,
    setTheme: (_theme) => ({ success: false, error: "UI not available" }),
    getToolsExpanded: () => false,
    setToolsExpanded: () => { },
};
export class ExtensionRunner {
    constructor(extensions, runtime, cwd, sessionManager, modelRegistry) {
        this.errorListeners = new Set();
        this.getModel = () => undefined;
        this.isIdleFn = () => true;
        this.waitForIdleFn = async () => { };
        this.abortFn = () => { };
        this.hasPendingMessagesFn = () => false;
        this.getContextUsageFn = () => undefined;
        this.compactFn = () => { };
        this.getSystemPromptFn = () => "";
        this.newSessionHandler = async () => {
            throw new Error("Command context not yet bound: newSession is unavailable during early lifecycle");
        };
        this.forkHandler = async () => {
            throw new Error("Command context not yet bound: fork is unavailable during early lifecycle");
        };
        this.navigateTreeHandler = async () => {
            throw new Error("Command context not yet bound: navigateTree is unavailable during early lifecycle");
        };
        this.switchSessionHandler = async () => {
            throw new Error("Command context not yet bound: switchSession is unavailable during early lifecycle");
        };
        this.reloadHandler = async () => {
            throw new Error("Command context not yet bound: reload is unavailable during early lifecycle");
        };
        this.shutdownHandler = () => { };
        this.shortcutDiagnostics = [];
        this.commandDiagnostics = [];
        this.extensions = extensions;
        this.runtime = runtime;
        this.uiContext = noOpUIContext;
        this.cwd = cwd;
        this.sessionManager = sessionManager;
        this.modelRegistry = modelRegistry;
    }
    bindCore(actions, contextActions) {
        // Copy actions into the shared runtime (all extension APIs reference this)
        this.runtime.sendMessage = actions.sendMessage;
        this.runtime.sendUserMessage = actions.sendUserMessage;
        this.runtime.retryLastTurn = actions.retryLastTurn;
        this.runtime.appendEntry = actions.appendEntry;
        this.runtime.setSessionName = actions.setSessionName;
        this.runtime.getSessionName = actions.getSessionName;
        this.runtime.setLabel = actions.setLabel;
        this.runtime.getActiveTools = actions.getActiveTools;
        this.runtime.getAllTools = actions.getAllTools;
        this.runtime.setActiveTools = actions.setActiveTools;
        this.runtime.refreshTools = actions.refreshTools;
        this.runtime.getCommands = actions.getCommands;
        this.runtime.setModel = actions.setModel;
        this.runtime.getThinkingLevel = actions.getThinkingLevel;
        this.runtime.setThinkingLevel = actions.setThinkingLevel;
        // Context actions (required)
        this.getModel = contextActions.getModel;
        this.isIdleFn = contextActions.isIdle;
        this.abortFn = contextActions.abort;
        this.hasPendingMessagesFn = contextActions.hasPendingMessages;
        this.shutdownHandler = contextActions.shutdown;
        this.getContextUsageFn = contextActions.getContextUsage;
        this.compactFn = contextActions.compact;
        this.getSystemPromptFn = contextActions.getSystemPrompt;
        // Flush provider registrations queued during extension loading
        for (const { name, config } of this.runtime.pendingProviderRegistrations) {
            this.modelRegistry.registerProvider(name, config);
        }
        this.runtime.pendingProviderRegistrations = [];
        // From this point on, provider registration/unregistration takes effect immediately
        // without requiring a /reload.
        this.runtime.registerProvider = (name, config) => this.modelRegistry.registerProvider(name, config);
        this.runtime.unregisterProvider = (name) => this.modelRegistry.unregisterProvider(name);
    }
    bindCommandContext(actions) {
        if (actions) {
            this.waitForIdleFn = actions.waitForIdle;
            this.newSessionHandler = actions.newSession;
            this.forkHandler = actions.fork;
            this.navigateTreeHandler = actions.navigateTree;
            this.switchSessionHandler = actions.switchSession;
            this.reloadHandler = actions.reload;
            return;
        }
        this.waitForIdleFn = async () => { };
        this.newSessionHandler = async () => ({ cancelled: false });
        this.forkHandler = async () => ({ cancelled: false });
        this.navigateTreeHandler = async () => ({ cancelled: false });
        this.switchSessionHandler = async () => ({ cancelled: false });
        this.reloadHandler = async () => { };
    }
    setUIContext(uiContext) {
        this.uiContext = uiContext ?? noOpUIContext;
    }
    getUIContext() {
        return this.uiContext;
    }
    hasUI() {
        return this.uiContext !== noOpUIContext;
    }
    getExtensionPaths() {
        return this.extensions.map((e) => e.path);
    }
    /** Get all registered tools from all extensions (first registration per name wins). */
    getAllRegisteredTools() {
        const toolsByName = new Map();
        for (const ext of this.extensions) {
            for (const tool of ext.tools.values()) {
                if (!toolsByName.has(tool.definition.name)) {
                    toolsByName.set(tool.definition.name, tool);
                }
            }
        }
        return Array.from(toolsByName.values());
    }
    /** Get a tool definition by name. Returns undefined if not found. */
    getToolDefinition(toolName) {
        for (const ext of this.extensions) {
            const tool = ext.tools.get(toolName);
            if (tool) {
                return tool.definition;
            }
        }
        return undefined;
    }
    getFlags() {
        const allFlags = new Map();
        for (const ext of this.extensions) {
            for (const [name, flag] of ext.flags) {
                if (!allFlags.has(name)) {
                    allFlags.set(name, flag);
                }
            }
        }
        return allFlags;
    }
    setFlagValue(name, value) {
        this.runtime.flagValues.set(name, value);
    }
    getFlagValues() {
        return new Map(this.runtime.flagValues);
    }
    getShortcuts(effectiveKeybindings) {
        this.shortcutDiagnostics = [];
        const builtinKeybindings = buildBuiltinKeybindings(effectiveKeybindings);
        const extensionShortcuts = new Map();
        const addDiagnostic = (message, extensionPath) => {
            this.shortcutDiagnostics.push({ type: "warning", message, path: extensionPath });
            if (!this.hasUI()) {
                console.warn(message);
            }
        };
        for (const ext of this.extensions) {
            for (const [key, shortcut] of ext.shortcuts) {
                const normalizedKey = key.toLowerCase();
                const builtInKeybinding = builtinKeybindings[normalizedKey];
                if (builtInKeybinding?.restrictOverride === true) {
                    addDiagnostic(`Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`, shortcut.extensionPath);
                    continue;
                }
                if (builtInKeybinding?.restrictOverride === false) {
                    addDiagnostic(`Extension shortcut conflict: '${key}' is built-in shortcut for ${builtInKeybinding.action} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`, shortcut.extensionPath);
                }
                const existingExtensionShortcut = extensionShortcuts.get(normalizedKey);
                if (existingExtensionShortcut) {
                    addDiagnostic(`Extension shortcut conflict: '${key}' registered by both ${existingExtensionShortcut.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`, shortcut.extensionPath);
                }
                extensionShortcuts.set(normalizedKey, shortcut);
            }
        }
        return extensionShortcuts;
    }
    getShortcutDiagnostics() {
        return this.shortcutDiagnostics;
    }
    onError(listener) {
        this.errorListeners.add(listener);
        return () => this.errorListeners.delete(listener);
    }
    emitError(error) {
        for (const listener of this.errorListeners) {
            listener(error);
        }
    }
    hasHandlers(eventType) {
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get(eventType);
            if (handlers && handlers.length > 0) {
                return true;
            }
        }
        return false;
    }
    getMessageRenderer(customType) {
        for (const ext of this.extensions) {
            const renderer = ext.messageRenderers.get(customType);
            if (renderer) {
                return renderer;
            }
        }
        return undefined;
    }
    getRegisteredCommands(reserved) {
        this.commandDiagnostics = [];
        const commands = [];
        const commandOwners = new Map();
        for (const ext of this.extensions) {
            for (const command of ext.commands.values()) {
                if (reserved?.has(command.name)) {
                    const message = `Extension command '${command.name}' from ${ext.path} conflicts with built-in commands. Skipping.`;
                    this.commandDiagnostics.push({ type: "warning", message, path: ext.path });
                    if (!this.hasUI()) {
                        console.warn(message);
                    }
                    continue;
                }
                const existingOwner = commandOwners.get(command.name);
                if (existingOwner) {
                    const message = `Extension command '${command.name}' from ${ext.path} conflicts with ${existingOwner}. Skipping.`;
                    this.commandDiagnostics.push({ type: "warning", message, path: ext.path });
                    if (!this.hasUI()) {
                        console.warn(message);
                    }
                    continue;
                }
                commandOwners.set(command.name, ext.path);
                commands.push(command);
            }
        }
        return commands;
    }
    getCommandDiagnostics() {
        return this.commandDiagnostics;
    }
    getRegisteredCommandsWithPaths() {
        const result = [];
        for (const ext of this.extensions) {
            for (const command of ext.commands.values()) {
                result.push({ command, extensionPath: ext.path });
            }
        }
        return result;
    }
    getCommand(name) {
        for (const ext of this.extensions) {
            const command = ext.commands.get(name);
            if (command) {
                return command;
            }
        }
        return undefined;
    }
    /**
     * Request a graceful shutdown. Called by extension tools and event handlers.
     * The actual shutdown behavior is provided by the mode via bindExtensions().
     */
    shutdown() {
        this.shutdownHandler();
    }
    /**
     * Create an ExtensionContext for use in event handlers and tool execution.
     * Context values are resolved at call time, so changes via bindCore/bindUI are reflected.
     */
    createContext() {
        const getModel = this.getModel;
        return {
            ui: this.uiContext,
            hasUI: this.hasUI(),
            cwd: this.cwd,
            sessionManager: this.sessionManager,
            modelRegistry: this.modelRegistry,
            get model() {
                return getModel();
            },
            isIdle: () => this.isIdleFn(),
            abort: () => this.abortFn(),
            hasPendingMessages: () => this.hasPendingMessagesFn(),
            shutdown: () => this.shutdownHandler(),
            getContextUsage: () => this.getContextUsageFn(),
            compact: (options) => this.compactFn(options),
            getSystemPrompt: () => this.getSystemPromptFn(),
        };
    }
    createCommandContext() {
        return {
            ...this.createContext(),
            waitForIdle: () => this.waitForIdleFn(),
            newSession: (options) => this.newSessionHandler(options),
            fork: (entryId) => this.forkHandler(entryId),
            navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
            switchSession: (sessionPath) => this.switchSessionHandler(sessionPath),
            reload: () => this.reloadHandler(),
        };
    }
    isSessionBeforeEvent(event) {
        return (event.type === "session_before_switch" ||
            event.type === "session_before_fork" ||
            event.type === "session_before_compact" ||
            event.type === "session_before_tree");
    }
    /**
     * Shared handler invocation loop.
     *
     * Iterates every handler registered for `eventType` across all extensions,
     * calling each inside a try/catch that emits an ExtensionError on failure.
     *
     * `getEvent` builds the event object for each handler call — callers that
     * mutate state between calls (e.g. context, before_provider_request) supply
     * a function; callers with a fixed event can pass a constant.
     *
     * `processResult` receives each handler's return value and the owning
     * extension's path. It returns `{ done: true }` to short-circuit
     * or `{ done: false }` to keep iterating.
     */
    async invokeHandlers(eventType, getEvent, processResult) {
        const ctx = this.createContext();
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get(eventType);
            if (!handlers || handlers.length === 0)
                continue;
            for (const handler of handlers) {
                try {
                    const event = getEvent();
                    const handlerResult = await handler(event, ctx);
                    const action = processResult(handlerResult, ext.path);
                    if (action.done)
                        return;
                }
                catch (err) {
                    const message = err instanceof Error ? err.message : String(err);
                    const stack = err instanceof Error ? err.stack : undefined;
                    this.emitError({
                        extensionPath: ext.path,
                        event: eventType,
                        error: message,
                        stack,
                    });
                }
            }
        }
    }
    async emit(event) {
        let result;
        const isSessionBefore = this.isSessionBeforeEvent(event);
        await this.invokeHandlers(event.type, () => event, (handlerResult) => {
            if (isSessionBefore && handlerResult) {
                result = handlerResult;
                if (result.cancel)
                    return { done: true };
            }
            return { done: false };
        });
        return result;
    }
    async emitToolResult(event) {
        const currentEvent = { ...event };
        let modified = false;
        await this.invokeHandlers("tool_result", () => currentEvent, (handlerResult) => {
            const r = handlerResult;
            if (!r)
                return { done: false };
            if (r.content !== undefined) {
                currentEvent.content = r.content;
                modified = true;
            }
            if (r.details !== undefined) {
                currentEvent.details = r.details;
                modified = true;
            }
            if (r.isError !== undefined) {
                currentEvent.isError = r.isError;
                modified = true;
            }
            return { done: false };
        });
        if (!modified)
            return undefined;
        return { content: currentEvent.content, details: currentEvent.details, isError: currentEvent.isError };
    }
    async emitToolCall(event) {
        let result;
        await this.invokeHandlers("tool_call", () => event, (handlerResult) => {
            if (handlerResult) {
                result = handlerResult;
                if (result.block)
                    return { done: true };
            }
            return { done: false };
        });
        return result;
    }
    async emitBashTransform(command, cwd) {
        if (!this.hasHandlers("bash_transform"))
            return command;
        let current = command;
        await this.invokeHandlers("bash_transform", () => ({ type: "bash_transform", command: current, cwd }), (handlerResult) => {
            const result = handlerResult;
            if (result?.command && result.command.trim()) {
                current = result.command;
            }
            return { done: false }; // chain all handlers
        });
        return current;
    }
    async emitUserBash(event) {
        let result;
        await this.invokeHandlers("user_bash", () => event, (handlerResult) => {
            if (handlerResult) {
                result = handlerResult;
                return { done: true };
            }
            return { done: false };
        });
        return result;
    }
    async emitContext(messages) {
        let currentMessages = structuredClone(messages);
        await this.invokeHandlers("context", () => ({ type: "context", messages: currentMessages }), (handlerResult) => {
            if (handlerResult && handlerResult.messages) {
                currentMessages = handlerResult.messages;
            }
            return { done: false };
        });
        return currentMessages;
    }
    async emitBeforeProviderRequest(payload, model) {
        let currentPayload = payload;
        await this.invokeHandlers("before_provider_request", () => ({
            type: "before_provider_request",
            payload: currentPayload,
            model,
        }), (handlerResult) => {
            if (handlerResult !== undefined)
                currentPayload = handlerResult;
            return { done: false };
        });
        return currentPayload;
    }
    async emitBeforeAgentStart(prompt, images, systemPrompt) {
        const messages = [];
        let currentSystemPrompt = systemPrompt;
        let systemPromptModified = false;
        await this.invokeHandlers("before_agent_start", () => ({
            type: "before_agent_start",
            prompt,
            images,
            systemPrompt: currentSystemPrompt,
        }), (handlerResult) => {
            if (handlerResult) {
                const r = handlerResult;
                if (r.message)
                    messages.push(r.message);
                if (r.systemPrompt !== undefined) {
                    currentSystemPrompt = r.systemPrompt;
                    systemPromptModified = true;
                }
            }
            return { done: false };
        });
        if (messages.length > 0 || systemPromptModified) {
            return {
                messages: messages.length > 0 ? messages : undefined,
                systemPrompt: systemPromptModified ? currentSystemPrompt : undefined,
            };
        }
        return undefined;
    }
    async emitResourcesDiscover(cwd, reason) {
        const skillPaths = [];
        const promptPaths = [];
        const themePaths = [];
        await this.invokeHandlers("resources_discover", () => ({
            type: "resources_discover",
            cwd,
            reason,
        }), (handlerResult, extensionPath) => {
            const r = handlerResult;
            if (r?.skillPaths?.length)
                skillPaths.push(...r.skillPaths.map((path) => ({ path, extensionPath })));
            if (r?.promptPaths?.length)
                promptPaths.push(...r.promptPaths.map((path) => ({ path, extensionPath })));
            if (r?.themePaths?.length)
                themePaths.push(...r.themePaths.map((path) => ({ path, extensionPath })));
            return { done: false };
        });
        return { skillPaths, promptPaths, themePaths };
    }
    /** Emit input event. Transforms chain, "handled" short-circuits. */
    async emitInput(text, images, source) {
        let currentText = text;
        let currentImages = images;
        let handled;
        await this.invokeHandlers("input", () => ({
            type: "input",
            text: currentText,
            images: currentImages,
            source,
        }), (handlerResult) => {
            const r = handlerResult;
            if (r?.action === "handled") {
                handled = r;
                return { done: true };
            }
            if (r?.action === "transform") {
                currentText = r.text;
                currentImages = r.images ?? currentImages;
            }
            return { done: false };
        });
        if (handled)
            return handled;
        return currentText !== text || currentImages !== images
            ? { action: "transform", text: currentText, images: currentImages }
            : { action: "continue" };
    }
}
//# sourceMappingURL=runner.js.map