/**
 * Extension runner - executes extensions and manages their lifecycle.
 */
import { theme } from "../../modes/interactive/theme/theme.js";
// Extension shortcuts compete with canonical keybinding ids from keybindings.json.
// Only editor-global shortcuts are reserved here. Picker-specific bindings are not.
const RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS = [
    "app.interrupt",
    "app.clear",
    "app.exit",
    "app.suspend",
    "app.thinking.cycle",
    "app.model.cycleForward",
    "app.model.cycleBackward",
    "app.model.select",
    "app.tools.expand",
    "app.thinking.toggle",
    "app.editor.external",
    "app.message.followUp",
    "tui.input.submit",
    "tui.select.confirm",
    "tui.select.cancel",
    "tui.input.copy",
    "tui.editor.deleteToLineEnd",
];
const buildBuiltinKeybindings = (resolvedKeybindings) => {
    const builtinKeybindings = {};
    for (const [keybinding, keys] of Object.entries(resolvedKeybindings)) {
        if (keys === undefined)
            continue;
        const keyList = Array.isArray(keys) ? keys : [keys];
        const restrictOverride = RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS.includes(keybinding);
        for (const key of keyList) {
            const normalizedKey = key.toLowerCase();
            builtinKeybindings[normalizedKey] = {
                keybinding,
                restrictOverride,
            };
        }
    }
    return builtinKeybindings;
};
/**
 * Helper function to emit session_shutdown event to extensions.
 * Returns true if the event was emitted, false if there were no handlers.
 */
export async function emitSessionShutdownEvent(extensionRunner) {
    if (extensionRunner?.hasHandlers("session_shutdown")) {
        await extensionRunner.emit({
            type: "session_shutdown",
        });
        return true;
    }
    return false;
}
const noOpUIContext = {
    select: async () => undefined,
    confirm: async () => false,
    input: async () => undefined,
    notify: () => { },
    onTerminalInput: () => () => { },
    setStatus: () => { },
    setWorkingMessage: () => { },
    setHiddenThinkingLabel: () => { },
    setWidget: () => { },
    setFooter: () => { },
    setHeader: () => { },
    setTitle: () => { },
    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 {
    extensions;
    runtime;
    uiContext;
    cwd;
    sessionManager;
    modelRegistry;
    errorListeners = new Set();
    getModel = () => undefined;
    isIdleFn = () => true;
    getSignalFn = () => undefined;
    waitForIdleFn = async () => { };
    abortFn = () => { };
    hasPendingMessagesFn = () => false;
    getContextUsageFn = () => undefined;
    compactFn = () => { };
    getSystemPromptFn = () => "";
    newSessionHandler = async () => ({ cancelled: false });
    forkHandler = async () => ({ cancelled: false });
    navigateTreeHandler = async () => ({ cancelled: false });
    switchSessionHandler = async () => ({ cancelled: false });
    reloadHandler = async () => { };
    shutdownHandler = () => { };
    shortcutDiagnostics = [];
    commandDiagnostics = [];
    constructor(extensions, runtime, cwd, sessionManager, modelRegistry) {
        this.extensions = extensions;
        this.runtime = runtime;
        this.uiContext = noOpUIContext;
        this.cwd = cwd;
        this.sessionManager = sessionManager;
        this.modelRegistry = modelRegistry;
    }
    bindCore(actions, contextActions, providerActions) {
        // Copy actions into the shared runtime (all extension APIs reference this)
        this.runtime.sendMessage = actions.sendMessage;
        this.runtime.sendUserMessage = actions.sendUserMessage;
        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.getSignalFn = contextActions.getSignal;
        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, extensionPath } of this.runtime.pendingProviderRegistrations) {
            try {
                if (providerActions?.registerProvider) {
                    providerActions.registerProvider(name, config);
                }
                else {
                    this.modelRegistry.registerProvider(name, config);
                }
            }
            catch (err) {
                this.emitError({
                    extensionPath,
                    event: "register_provider",
                    error: err instanceof Error ? err.message : String(err),
                    stack: err instanceof Error ? err.stack : undefined,
                });
            }
        }
        this.runtime.pendingProviderRegistrations = [];
        // From this point on, provider registration/unregistration takes effect immediately
        // without requiring a /reload.
        this.runtime.registerProvider = (name, config) => {
            if (providerActions?.registerProvider) {
                providerActions.registerProvider(name, config);
                return;
            }
            this.modelRegistry.registerProvider(name, config);
        };
        this.runtime.unregisterProvider = (name) => {
            if (providerActions?.unregisterProvider) {
                providerActions.unregisterProvider(name);
                return;
            }
            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(resolvedKeybindings) {
        this.shortcutDiagnostics = [];
        const builtinKeybindings = buildBuiltinKeybindings(resolvedKeybindings);
        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.keybinding} 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;
    }
    resolveRegisteredCommands() {
        const commands = [];
        const counts = new Map();
        for (const ext of this.extensions) {
            for (const command of ext.commands.values()) {
                commands.push(command);
                counts.set(command.name, (counts.get(command.name) ?? 0) + 1);
            }
        }
        const seen = new Map();
        const takenInvocationNames = new Set();
        return commands.map((command) => {
            const occurrence = (seen.get(command.name) ?? 0) + 1;
            seen.set(command.name, occurrence);
            let invocationName = (counts.get(command.name) ?? 0) > 1 ? `${command.name}:${occurrence}` : command.name;
            if (takenInvocationNames.has(invocationName)) {
                let suffix = occurrence;
                do {
                    suffix++;
                    invocationName = `${command.name}:${suffix}`;
                } while (takenInvocationNames.has(invocationName));
            }
            takenInvocationNames.add(invocationName);
            return {
                ...command,
                invocationName,
            };
        });
    }
    getRegisteredCommands() {
        this.commandDiagnostics = [];
        return this.resolveRegisteredCommands();
    }
    getCommandDiagnostics() {
        return this.commandDiagnostics;
    }
    getCommand(name) {
        return this.resolveRegisteredCommands().find((command) => command.invocationName === name);
    }
    /**
     * 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(),
            signal: this.getSignalFn(),
            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");
    }
    async emit(event) {
        const ctx = this.createContext();
        let result;
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get(event.type);
            if (!handlers || handlers.length === 0)
                continue;
            for (const handler of handlers) {
                try {
                    const handlerResult = await handler(event, ctx);
                    if (this.isSessionBeforeEvent(event) && handlerResult) {
                        result = handlerResult;
                        if (result.cancel) {
                            return result;
                        }
                    }
                }
                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: event.type,
                        error: message,
                        stack,
                    });
                }
            }
        }
        return result;
    }
    async emitToolResult(event) {
        const ctx = this.createContext();
        const currentEvent = { ...event };
        let modified = false;
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get("tool_result");
            if (!handlers || handlers.length === 0)
                continue;
            for (const handler of handlers) {
                try {
                    const handlerResult = (await handler(currentEvent, ctx));
                    if (!handlerResult)
                        continue;
                    if (handlerResult.content !== undefined) {
                        currentEvent.content = handlerResult.content;
                        modified = true;
                    }
                    if (handlerResult.details !== undefined) {
                        currentEvent.details = handlerResult.details;
                        modified = true;
                    }
                    if (handlerResult.isError !== undefined) {
                        currentEvent.isError = handlerResult.isError;
                        modified = true;
                    }
                }
                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: "tool_result",
                        error: message,
                        stack,
                    });
                }
            }
        }
        if (!modified) {
            return undefined;
        }
        return {
            content: currentEvent.content,
            details: currentEvent.details,
            isError: currentEvent.isError,
        };
    }
    async emitToolCall(event) {
        const ctx = this.createContext();
        let result;
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get("tool_call");
            if (!handlers || handlers.length === 0)
                continue;
            for (const handler of handlers) {
                const handlerResult = await handler(event, ctx);
                if (handlerResult) {
                    result = handlerResult;
                    if (result.block) {
                        return result;
                    }
                }
            }
        }
        return result;
    }
    async emitUserBash(event) {
        const ctx = this.createContext();
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get("user_bash");
            if (!handlers || handlers.length === 0)
                continue;
            for (const handler of handlers) {
                try {
                    const handlerResult = await handler(event, ctx);
                    if (handlerResult) {
                        return handlerResult;
                    }
                }
                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: "user_bash",
                        error: message,
                        stack,
                    });
                }
            }
        }
        return undefined;
    }
    async emitContext(messages) {
        const ctx = this.createContext();
        let currentMessages = structuredClone(messages);
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get("context");
            if (!handlers || handlers.length === 0)
                continue;
            for (const handler of handlers) {
                try {
                    const event = { type: "context", messages: currentMessages };
                    const handlerResult = await handler(event, ctx);
                    if (handlerResult && handlerResult.messages) {
                        currentMessages = handlerResult.messages;
                    }
                }
                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: "context",
                        error: message,
                        stack,
                    });
                }
            }
        }
        return currentMessages;
    }
    async emitBeforeProviderRequest(payload) {
        const ctx = this.createContext();
        let currentPayload = payload;
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get("before_provider_request");
            if (!handlers || handlers.length === 0)
                continue;
            for (const handler of handlers) {
                try {
                    const event = {
                        type: "before_provider_request",
                        payload: currentPayload,
                    };
                    const handlerResult = await handler(event, ctx);
                    if (handlerResult !== undefined) {
                        currentPayload = handlerResult;
                    }
                }
                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: "before_provider_request",
                        error: message,
                        stack,
                    });
                }
            }
        }
        return currentPayload;
    }
    async emitBeforeAgentStart(prompt, images, systemPrompt) {
        const ctx = this.createContext();
        const messages = [];
        let currentSystemPrompt = systemPrompt;
        let systemPromptModified = false;
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get("before_agent_start");
            if (!handlers || handlers.length === 0)
                continue;
            for (const handler of handlers) {
                try {
                    const event = {
                        type: "before_agent_start",
                        prompt,
                        images,
                        systemPrompt: currentSystemPrompt,
                    };
                    const handlerResult = await handler(event, ctx);
                    if (handlerResult) {
                        const result = handlerResult;
                        if (result.message) {
                            messages.push(result.message);
                        }
                        if (result.systemPrompt !== undefined) {
                            currentSystemPrompt = result.systemPrompt;
                            systemPromptModified = true;
                        }
                    }
                }
                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: "before_agent_start",
                        error: message,
                        stack,
                    });
                }
            }
        }
        if (messages.length > 0 || systemPromptModified) {
            return {
                messages: messages.length > 0 ? messages : undefined,
                systemPrompt: systemPromptModified ? currentSystemPrompt : undefined,
            };
        }
        return undefined;
    }
    async emitResourcesDiscover(cwd, reason) {
        const ctx = this.createContext();
        const skillPaths = [];
        const promptPaths = [];
        const themePaths = [];
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get("resources_discover");
            if (!handlers || handlers.length === 0)
                continue;
            for (const handler of handlers) {
                try {
                    const event = { type: "resources_discover", cwd, reason };
                    const handlerResult = await handler(event, ctx);
                    const result = handlerResult;
                    if (result?.skillPaths?.length) {
                        skillPaths.push(...result.skillPaths.map((path) => ({ path, extensionPath: ext.path })));
                    }
                    if (result?.promptPaths?.length) {
                        promptPaths.push(...result.promptPaths.map((path) => ({ path, extensionPath: ext.path })));
                    }
                    if (result?.themePaths?.length) {
                        themePaths.push(...result.themePaths.map((path) => ({ path, extensionPath: ext.path })));
                    }
                }
                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: "resources_discover",
                        error: message,
                        stack,
                    });
                }
            }
        }
        return { skillPaths, promptPaths, themePaths };
    }
    /** Emit input event. Transforms chain, "handled" short-circuits. */
    async emitInput(text, images, source) {
        const ctx = this.createContext();
        let currentText = text;
        let currentImages = images;
        for (const ext of this.extensions) {
            for (const handler of ext.handlers.get("input") ?? []) {
                try {
                    const event = { type: "input", text: currentText, images: currentImages, source };
                    const result = (await handler(event, ctx));
                    if (result?.action === "handled")
                        return result;
                    if (result?.action === "transform") {
                        currentText = result.text;
                        currentImages = result.images ?? currentImages;
                    }
                }
                catch (err) {
                    this.emitError({
                        extensionPath: ext.path,
                        event: "input",
                        error: err instanceof Error ? err.message : String(err),
                        stack: err instanceof Error ? err.stack : undefined,
                    });
                }
            }
        }
        return currentText !== text || currentImages !== images
            ? { action: "transform", text: currentText, images: currentImages }
            : { action: "continue" };
    }
}
//# sourceMappingURL=runner.js.map