/**
 * Extension runner - executes extensions and manages their lifecycle.
 */
import { getActiveUserMessageId } from '../../core/user-message-id.js';
import { createDefaultPermissionPolicy } from '../permissions/policy.js';
import { createShortcutLlmProxyClient } from '../providers/shortcut-llm-proxy-client.js';
import { runPostTurnHandlers as runPostTurnQueue } from './post-turn-queue.js';
const noOpUIContext = {
    select: async () => undefined,
    confirm: async () => false,
    input: async () => undefined,
    notify: () => { }
};
export class ExtensionRunner {
    extensions;
    runtime;
    uiContext;
    cwd;
    sessionManager;
    modelRegistry;
    errorListeners = new Set();
    getModel = () => undefined;
    isIdleFn = () => true;
    waitForIdleFn = async () => { };
    abortFn = () => { };
    hasPendingMessagesFn = () => false;
    getContextUsageFn = () => undefined;
    compactFn = () => { };
    getSystemPromptFn = () => '';
    deductChildUsageFn = () => { };
    getMessagesFn = () => [];
    getToolsFn = () => [];
    getPermissionPolicyFn = () => createDefaultPermissionPolicy();
    newSessionHandler = async () => ({ cancelled: false });
    forkHandler = async () => ({ cancelled: false });
    navigateTreeHandler = async () => ({ cancelled: false });
    switchSessionHandler = async () => ({ cancelled: false });
    reloadHandler = async () => { };
    shutdownHandler = () => { };
    commandDiagnostics = [];
    sessionState;
    constructor(extensions, runtime, cwd, sessionManager, modelRegistry, sessionState) {
        this.extensions = extensions;
        this.runtime = runtime;
        this.uiContext = noOpUIContext;
        this.cwd = cwd;
        this.sessionManager = sessionManager;
        this.modelRegistry = modelRegistry;
        this.sessionState = sessionState;
    }
    bindCore(actions, contextActions) {
        // Copy actions into the shared runtime (extension API delegates to these)
        this.runtime.appendEntry = actions.appendEntry;
        this.runtime.refreshTools = actions.refreshTools;
        // 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;
        this.deductChildUsageFn = contextActions.deductChildUsage;
        this.getMessagesFn = contextActions.getMessages;
        this.getToolsFn = contextActions.getTools;
        this.getPermissionPolicyFn = contextActions.getPermissionPolicy;
    }
    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.filter((e) => e.visibility === 'visible').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;
    }
    /** Get all stop hooks from all extensions, sorted by priority (lower first). */
    getStopHooks() {
        const hooks = [];
        for (const ext of this.extensions) {
            hooks.push(...ext.stopHooks);
        }
        return hooks.sort((a, b) => a.priority - b.priority);
    }
    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;
    }
    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;
    }
    /** Collect post-turn handlers from all extensions, sorted by priority. */
    getPostTurnHandlers() {
        const handlers = [];
        for (const ext of this.extensions) {
            handlers.push(...ext.postTurnHandlers);
        }
        return handlers;
    }
    /**
     * Run all registered post-turn handlers after a turn_end event.
     *
     * Creates a PostTurnContext from the turn data, then delegates to the
     * post-turn queue which handles priority, sequential execution, and cancellation.
     *
     * @returns The number of handlers that executed.
     */
    async runPostTurnHandlers(turnIndex, message, toolResults) {
        const handlers = [];
        const owners = new Map();
        for (const ext of this.extensions) {
            for (const handler of ext.postTurnHandlers) {
                handlers.push(handler);
                owners.set(handler, ext.path);
            }
        }
        if (handlers.length === 0)
            return 0;
        const baseCtx = this.createContext();
        const ctx = {
            ...baseCtx,
            turnIndex,
            message,
            toolResults
        };
        const controller = new AbortController();
        return runPostTurnQueue(handlers, ctx, controller, {
            hasPendingMessages: () => this.hasPendingMessagesFn(),
            onError: (error) => this.emitError(error),
            resolveExtensionPath: (handler) => owners.get(handler)
        });
    }
    /**
     * 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;
        const shortcutLlmProxy = createShortcutLlmProxyClient({
            modelRegistry: this.modelRegistry,
            getAttribution: () => ({
                sessionId: this.sessionManager.getSessionId(),
                userMessageId: getActiveUserMessageId(this.getMessagesFn())
            })
        });
        return {
            ui: this.uiContext,
            hasUI: this.hasUI(),
            cwd: this.cwd,
            sessionManager: this.sessionManager,
            modelRegistry: this.modelRegistry,
            shortcutLlmProxy,
            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(),
            deductChildUsage: (usage) => this.deductChildUsageFn(usage),
            getMessages: () => this.getMessagesFn(),
            getTools: () => this.getToolsFn(),
            permissionPolicy: this.getPermissionPolicyFn(),
            sessionState: this.sessionState
        };
    }
    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_compact';
    }
    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) {
                try {
                    const handlerResult = await handler(event, ctx);
                    if (handlerResult) {
                        result = handlerResult;
                        if (result.block) {
                            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: 'tool_call', error: message, stack });
                }
            }
        }
        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);
                    const nextMessages = handlerResult?.messages;
                    if (nextMessages) {
                        currentMessages = nextMessages;
                    }
                }
                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 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 = [];
        const agentPaths = [];
        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 })));
                    }
                    if (result?.agentPaths?.length) {
                        agentPaths.push(...result.agentPaths.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, agentPaths };
    }
    /** 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