import { spawn, } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { Readable, Writable } from "node:stream";
import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream, } from "@agentclientprotocol/sdk";
import { applyProviderEnv, resolveProviderCommandPrefix, } from "../provider-launch-config.js";
import { findExecutable } from "../../../utils/executable.js";
const DEFAULT_ACP_CAPABILITIES = {
    supportsStreaming: true,
    supportsSessionPersistence: true,
    supportsDynamicModes: true,
    supportsMcpServers: true,
    supportsReasoningStream: true,
    supportsToolInvocations: true,
};
const ACP_CLIENT_CAPABILITIES = {
    fs: {
        readTextFile: true,
        writeTextFile: true,
    },
    terminal: true,
};
export function mapACPUsage(usage) {
    if (!usage) {
        return undefined;
    }
    return {
        inputTokens: usage.inputTokens ?? undefined,
        outputTokens: usage.outputTokens ?? undefined,
        cachedInputTokens: usage.cachedReadTokens ?? undefined,
    };
}
export function deriveModesFromACP(fallbackModes, modeState, configOptions) {
    if (modeState?.availableModes?.length) {
        return {
            modes: modeState.availableModes.map((mode) => ({
                id: mode.id,
                label: mode.name,
                description: mode.description ?? undefined,
            })),
            currentModeId: modeState.currentModeId ?? null,
        };
    }
    const modeOption = configOptions?.find((option) => option.type === "select" && option.category === "mode");
    if (modeOption?.type === "select") {
        const flatOptions = flattenSelectOptions(modeOption.options);
        return {
            modes: flatOptions.map((option) => ({
                id: option.value,
                label: option.name,
                description: option.description ?? undefined,
            })),
            currentModeId: modeOption.currentValue,
        };
    }
    return {
        modes: fallbackModes,
        currentModeId: null,
    };
}
export function deriveModelDefinitionsFromACP(provider, models, configOptions) {
    const thinkingOptions = deriveSelectorOptions(configOptions, "thought_level");
    const defaultThinkingOptionId = thinkingOptions.find((option) => option.isDefault)?.id ?? null;
    if (models?.availableModels?.length) {
        return models.availableModels.map((model) => ({
            provider,
            id: model.modelId,
            label: model.name,
            description: model.description ?? undefined,
            isDefault: model.modelId === models.currentModelId,
            thinkingOptions: thinkingOptions.length > 0 ? thinkingOptions : undefined,
            defaultThinkingOptionId: defaultThinkingOptionId ?? undefined,
        }));
    }
    const modelOptions = deriveSelectorOptions(configOptions, "model");
    return modelOptions.map((option) => ({
        provider,
        id: option.id,
        label: option.label,
        description: option.description,
        isDefault: option.isDefault,
        thinkingOptions: thinkingOptions.length > 0 ? thinkingOptions : undefined,
        defaultThinkingOptionId: defaultThinkingOptionId ?? undefined,
        metadata: option.metadata,
    }));
}
export class ACPAgentClient {
    constructor(options) {
        this.provider = options.provider;
        this.capabilities = options.capabilities ?? DEFAULT_ACP_CAPABILITIES;
        this.logger = options.logger.child({ module: "agent", provider: options.provider });
        this.runtimeSettings = options.runtimeSettings;
        this.defaultCommand = options.defaultCommand;
        this.defaultModes = options.defaultModes ?? [];
        this.modelTransformer = options.modelTransformer;
        this.sessionResponseTransformer = options.sessionResponseTransformer;
        this.toolSnapshotTransformer = options.toolSnapshotTransformer;
        this.thinkingOptionWriter = options.thinkingOptionWriter;
    }
    async createSession(config, launchContext) {
        this.assertProvider(config);
        const session = new ACPAgentSession({ ...config, provider: this.provider }, {
            provider: this.provider,
            logger: this.logger,
            runtimeSettings: this.runtimeSettings,
            defaultCommand: this.defaultCommand,
            defaultModes: this.defaultModes,
            modelTransformer: this.modelTransformer,
            sessionResponseTransformer: this.sessionResponseTransformer,
            toolSnapshotTransformer: this.toolSnapshotTransformer,
            thinkingOptionWriter: this.thinkingOptionWriter,
            capabilities: this.capabilities,
            launchEnv: launchContext?.env,
        });
        await session.initializeNewSession();
        return session;
    }
    async resumeSession(handle, overrides, launchContext) {
        if (handle.provider !== this.provider) {
            throw new Error(`Cannot resume ${handle.provider} handle with ${this.provider} provider`);
        }
        const storedConfig = coerceSessionConfigMetadata(handle.metadata);
        const cwd = overrides?.cwd ?? storedConfig.cwd;
        if (!cwd) {
            throw new Error(`${this.provider} resume requires the original working directory`);
        }
        const mergedConfig = {
            ...storedConfig,
            ...overrides,
            provider: this.provider,
            cwd,
        };
        const session = new ACPAgentSession(mergedConfig, {
            provider: this.provider,
            logger: this.logger,
            runtimeSettings: this.runtimeSettings,
            defaultCommand: this.defaultCommand,
            defaultModes: this.defaultModes,
            modelTransformer: this.modelTransformer,
            sessionResponseTransformer: this.sessionResponseTransformer,
            toolSnapshotTransformer: this.toolSnapshotTransformer,
            thinkingOptionWriter: this.thinkingOptionWriter,
            capabilities: this.capabilities,
            handle,
            launchEnv: launchContext?.env,
        });
        await session.initializeResumedSession();
        return session;
    }
    async listModels(options) {
        const cwd = options?.cwd ?? process.cwd();
        const probe = await this.spawnProcess(undefined);
        try {
            const response = await probe.connection.newSession({
                cwd,
                mcpServers: [],
            });
            const transformed = this.transformSessionResponse(response);
            const models = deriveModelDefinitionsFromACP(this.provider, transformed.models, transformed.configOptions);
            return this.modelTransformer ? this.modelTransformer(models) : models;
        }
        finally {
            await this.closeProbe(probe);
        }
    }
    async listModes(options) {
        const cwd = options?.cwd ?? process.cwd();
        const probe = await this.spawnProcess(undefined);
        try {
            const response = await probe.connection.newSession({
                cwd,
                mcpServers: [],
            });
            const transformed = this.transformSessionResponse(response);
            const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, transformed.configOptions);
            return modeInfo.modes;
        }
        finally {
            await this.closeProbe(probe);
        }
    }
    async listPersistedAgents(options) {
        const probe = await this.spawnProcess(undefined);
        try {
            if (!probe.initialize.agentCapabilities?.sessionCapabilities?.list) {
                return [];
            }
            const sessions = [];
            let cursor;
            do {
                const page = await probe.connection.listSessions({
                    ...(cursor ? { cursor } : {}),
                });
                for (const session of page.sessions) {
                    sessions.push({
                        provider: this.provider,
                        sessionId: session.sessionId,
                        cwd: session.cwd,
                        title: session.title ?? null,
                        lastActivityAt: session.updatedAt ? new Date(session.updatedAt) : new Date(0),
                        persistence: {
                            provider: this.provider,
                            sessionId: session.sessionId,
                            nativeHandle: session.sessionId,
                            metadata: {
                                provider: this.provider,
                                cwd: session.cwd,
                                title: session.title ?? null,
                            },
                        },
                        timeline: [],
                    });
                }
                cursor = page.nextCursor ?? null;
            } while (cursor && (!options?.limit || sessions.length < options.limit));
            return typeof options?.limit === "number" ? sessions.slice(0, options.limit) : sessions;
        }
        finally {
            await this.closeProbe(probe);
        }
    }
    async isAvailable() {
        try {
            this.resolveLaunchCommand();
            return true;
        }
        catch {
            return false;
        }
    }
    async spawnProcess(launchEnv) {
        const { command, args } = this.resolveLaunchCommand();
        const child = spawn(command, args, {
            cwd: process.cwd(),
            env: {
                ...applyProviderEnv(process.env, this.runtimeSettings),
                ...(launchEnv ?? {}),
            },
            shell: process.platform === "win32",
            stdio: ["pipe", "pipe", "pipe"],
        });
        const stderrChunks = [];
        child.stderr.on("data", (chunk) => {
            stderrChunks.push(chunk.toString());
        });
        const spawnErrorPromise = new Promise((_, reject) => {
            child.once("error", (error) => {
                const stderr = stderrChunks.join("").trim();
                reject(new Error(stderr ? `${String(error)}\n${stderr}` : String(error)));
            });
        });
        if (!child.stdin || !child.stdout) {
            throw new Error(`${this.provider} ACP process did not expose stdio pipes`);
        }
        const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
        const connection = new ClientSideConnection(() => this.buildProbeClient(), stream);
        const initialize = (await Promise.race([
            connection.initialize({
                protocolVersion: PROTOCOL_VERSION,
                clientCapabilities: ACP_CLIENT_CAPABILITIES,
                clientInfo: { name: "Paseo", version: "dev" },
            }),
            spawnErrorPromise,
        ]));
        return { child, connection, initialize };
    }
    buildProbeClient() {
        return {
            async requestPermission() {
                return { outcome: { outcome: "cancelled" } };
            },
            async sessionUpdate() { },
            async readTextFile(params) {
                const content = await fs.readFile(params.path, "utf8");
                return { content };
            },
            async writeTextFile(params) {
                await fs.mkdir(path.dirname(params.path), { recursive: true });
                await fs.writeFile(params.path, params.content, "utf8");
                return {};
            },
            async createTerminal() {
                throw new Error("ACP model probe does not support terminal execution");
            },
        };
    }
    async closeProbe(probe) {
        try {
            if (probe.initialize.agentCapabilities?.sessionCapabilities?.close) {
                // No active session to close here; ignore capability.
            }
        }
        finally {
            probe.child.kill("SIGTERM");
            await waitForChildExit(probe.child, 2000);
        }
    }
    resolveLaunchCommand() {
        const prefix = resolveProviderCommandPrefix(this.runtimeSettings?.command, () => {
            const resolved = findExecutable(this.defaultCommand[0]);
            if (!resolved) {
                throw new Error(`${this.provider} command '${this.defaultCommand[0]}' not found`);
            }
            return resolved;
        });
        return {
            command: prefix.command,
            args: [...prefix.args, ...this.defaultCommand.slice(1)],
        };
    }
    assertProvider(config) {
        if (config.provider !== this.provider) {
            throw new Error(`Expected ${this.provider} config, received ${config.provider}`);
        }
    }
    transformSessionResponse(response) {
        return this.sessionResponseTransformer ? this.sessionResponseTransformer(response) : response;
    }
}
export class ACPAgentSession {
    constructor(config, options) {
        this.subscribers = new Set();
        this.pendingPermissions = new Map();
        this.messageAssemblies = new Map();
        this.toolCalls = new Map();
        this.terminalEntries = new Map();
        this.persistedHistory = [];
        this.child = null;
        this.connection = null;
        this.agentCapabilities = null;
        this.sessionId = null;
        this.currentMode = null;
        this.availableModels = [];
        this.currentModel = null;
        this.thinkingOptionId = null;
        this.currentTitle = null;
        this.lastActivityAt = null;
        this.configOptions = [];
        this.cachedCommands = [];
        this.activeForegroundTurnId = null;
        this.closed = false;
        this.historyPending = false;
        this.replayingHistory = false;
        this.suppressUserEchoMessageId = null;
        this.suppressUserEchoText = null;
        this.bootstrapThreadEventPending = false;
        this.provider = options.provider;
        this.capabilities = options.capabilities;
        this.logger = options.logger.child({ module: "agent", provider: options.provider });
        this.runtimeSettings = options.runtimeSettings;
        this.defaultCommand = options.defaultCommand;
        this.defaultModes = options.defaultModes;
        this.modelTransformer = options.modelTransformer;
        this.sessionResponseTransformer = options.sessionResponseTransformer;
        this.toolSnapshotTransformer = options.toolSnapshotTransformer;
        this.thinkingOptionWriter = options.thinkingOptionWriter;
        this.availableModes = options.defaultModes;
        this.launchEnv = options.launchEnv;
        this.initialHandle = options.handle;
        this.config = { ...config, provider: options.provider };
        this.currentMode = config.modeId ?? null;
        this.currentModel = config.model ?? null;
        this.thinkingOptionId = config.thinkingOptionId ?? null;
        this.currentTitle = config.title ?? null;
    }
    get id() {
        return this.sessionId;
    }
    async initializeNewSession() {
        const spawned = await this.spawnProcess();
        this.child = spawned.child;
        this.connection = spawned.connection;
        this.agentCapabilities = spawned.initialize.agentCapabilities ?? null;
        const response = await this.connection.newSession({
            cwd: this.config.cwd,
            mcpServers: normalizeMcpServers(this.config.mcpServers),
        });
        this.sessionId = response.sessionId;
        this.bootstrapThreadEventPending = true;
        this.applySessionState(response);
        await this.applyConfiguredOverrides();
    }
    async initializeResumedSession() {
        const handle = this.initialHandle;
        if (!handle) {
            throw new Error("Resume requested without persistence handle");
        }
        const spawned = await this.spawnProcess();
        this.child = spawned.child;
        this.connection = spawned.connection;
        this.agentCapabilities = spawned.initialize.agentCapabilities ?? null;
        this.sessionId = handle.sessionId;
        this.bootstrapThreadEventPending = true;
        const sessionCapabilities = this.agentCapabilities?.sessionCapabilities;
        if (this.agentCapabilities?.loadSession) {
            this.replayingHistory = true;
            const response = await this.connection.loadSession({
                sessionId: handle.sessionId,
                cwd: this.config.cwd,
                mcpServers: normalizeMcpServers(this.config.mcpServers),
            });
            this.replayingHistory = false;
            this.historyPending = this.persistedHistory.length > 0;
            this.applySessionState(response);
        }
        else if (sessionCapabilities?.resume) {
            const response = await this.connection.unstable_resumeSession({
                sessionId: handle.sessionId,
                cwd: this.config.cwd,
                mcpServers: normalizeMcpServers(this.config.mcpServers),
            });
            this.applySessionState(response);
        }
        else {
            throw new Error(`${this.provider} does not support ACP session resume`);
        }
        await this.applyConfiguredOverrides();
    }
    async run(prompt, options) {
        const timeline = [];
        let finalText = "";
        let usage;
        let turnId = null;
        let settled = false;
        let resolveCompletion;
        let rejectCompletion;
        const buffered = [];
        const completion = new Promise((resolve, reject) => {
            resolveCompletion = resolve;
            rejectCompletion = reject;
        });
        const processEvent = (event) => {
            if (settled) {
                return;
            }
            if (turnId && "turnId" in event && event.turnId && event.turnId !== turnId) {
                return;
            }
            if (event.type === "timeline") {
                timeline.push(event.item);
                if (event.item.type === "assistant_message") {
                    finalText = event.item.text.startsWith(finalText)
                        ? event.item.text
                        : `${finalText}${event.item.text}`;
                }
                return;
            }
            if (event.type === "turn_completed") {
                usage = event.usage;
                settled = true;
                resolveCompletion();
                return;
            }
            if (event.type === "turn_failed") {
                settled = true;
                rejectCompletion(new Error(event.error));
                return;
            }
            if (event.type === "turn_canceled") {
                settled = true;
                resolveCompletion();
            }
        };
        const unsubscribe = this.subscribe((event) => {
            if (!turnId) {
                buffered.push(event);
                return;
            }
            processEvent(event);
        });
        try {
            const started = await this.startTurn(prompt, options);
            turnId = started.turnId;
            for (const event of buffered) {
                processEvent(event);
            }
            if (!settled) {
                await completion;
            }
        }
        finally {
            unsubscribe();
        }
        if (!this.sessionId) {
            throw new Error("ACP session did not expose a session id");
        }
        return {
            sessionId: this.sessionId,
            finalText,
            usage,
            timeline,
        };
    }
    async startTurn(prompt, _options) {
        if (this.closed) {
            throw new Error(`${this.provider} session is closed`);
        }
        if (!this.connection || !this.sessionId) {
            throw new Error(`${this.provider} session is not initialized`);
        }
        if (this.activeForegroundTurnId) {
            throw new Error("A foreground turn is already active");
        }
        const turnId = randomUUID();
        const messageId = randomUUID();
        this.activeForegroundTurnId = turnId;
        this.suppressUserEchoMessageId = messageId;
        this.suppressUserEchoText = extractPromptText(prompt);
        this.emitBootstrapThreadEvent();
        this.pushEvent({ type: "turn_started", provider: this.provider, turnId });
        void this.connection
            .prompt({
            sessionId: this.sessionId,
            messageId,
            prompt: toACPContentBlocks(prompt),
        })
            .then((response) => {
            this.handlePromptResponse(response, turnId);
        })
            .catch((error) => {
            const message = error instanceof Error ? error.message : String(error);
            this.finishTurn({
                type: "turn_failed",
                provider: this.provider,
                error: message,
                diagnostic: this.collectDiagnostic(message),
                turnId,
            });
        });
        return { turnId };
    }
    subscribe(callback) {
        this.subscribers.add(callback);
        if (this.sessionId) {
            callback({
                type: "thread_started",
                provider: this.provider,
                sessionId: this.sessionId,
            });
        }
        return () => {
            this.subscribers.delete(callback);
        };
    }
    async *streamHistory() {
        if (!this.historyPending || this.persistedHistory.length === 0) {
            return;
        }
        const history = [...this.persistedHistory];
        this.persistedHistory.length = 0;
        this.historyPending = false;
        for (const item of history) {
            yield { type: "timeline", provider: this.provider, item };
        }
    }
    async getRuntimeInfo() {
        return {
            provider: this.provider,
            sessionId: this.sessionId,
            model: this.currentModel,
            thinkingOptionId: this.thinkingOptionId,
            modeId: this.currentMode,
            extra: {
                title: this.currentTitle,
                updatedAt: this.lastActivityAt,
            },
        };
    }
    async getAvailableModes() {
        return [...this.availableModes];
    }
    async getCurrentMode() {
        return this.currentMode;
    }
    async listCommands() {
        return this.cachedCommands;
    }
    async setMode(modeId) {
        if (!this.connection || !this.sessionId) {
            throw new Error("ACP session not initialized");
        }
        const modeExists = this.availableModes.some((mode) => mode.id === modeId);
        if (!modeExists && this.availableModes.length > 0) {
            throw new Error(`Unknown ${this.provider} mode '${modeId}'`);
        }
        if (this.availableModes.length > 0) {
            await this.connection.setSessionMode({ sessionId: this.sessionId, modeId });
            this.currentMode = modeId;
            return;
        }
        const modeOption = this.getSelectConfigOption("mode");
        if (!modeOption) {
            throw new Error(`${this.provider} does not expose ACP mode switching`);
        }
        await this.connection.setSessionConfigOption({
            sessionId: this.sessionId,
            configId: modeOption.id,
            value: modeId,
        });
        this.currentMode = modeId;
    }
    async setModel(modelId) {
        if (!this.connection || !this.sessionId) {
            throw new Error("ACP session not initialized");
        }
        if (!modelId) {
            this.currentModel = null;
            return;
        }
        const modelExists = this.availableModels.some((model) => model.id === modelId);
        if (!modelExists && this.availableModels.length > 0) {
            throw new Error(`Unknown ${this.provider} model '${modelId}'`);
        }
        if ("unstable_setSessionModel" in this.connection) {
            try {
                await this.connection.unstable_setSessionModel({
                    sessionId: this.sessionId,
                    modelId,
                });
                this.currentModel = modelId;
                return;
            }
            catch {
                // Fall through to config option path.
            }
        }
        const modelOption = this.getSelectConfigOption("model");
        if (!modelOption) {
            throw new Error(`${this.provider} does not expose ACP model selection`);
        }
        await this.connection.setSessionConfigOption({
            sessionId: this.sessionId,
            configId: modelOption.id,
            value: modelId,
        });
        this.currentModel = modelId;
    }
    async setThinkingOption(thinkingOptionId) {
        if (!this.connection || !this.sessionId) {
            throw new Error("ACP session not initialized");
        }
        if (!thinkingOptionId) {
            this.thinkingOptionId = null;
            return;
        }
        if (this.thinkingOptionWriter) {
            await this.thinkingOptionWriter(this.connection, this.sessionId, thinkingOptionId);
            this.thinkingOptionId = thinkingOptionId;
            return;
        }
        const option = this.getSelectConfigOption("thought_level");
        if (!option) {
            throw new Error(`${this.provider} does not expose ACP thought-level selection`);
        }
        await this.connection.setSessionConfigOption({
            sessionId: this.sessionId,
            configId: option.id,
            value: thinkingOptionId,
        });
        this.thinkingOptionId = thinkingOptionId;
    }
    getPendingPermissions() {
        return Array.from(this.pendingPermissions.values(), (entry) => entry.request);
    }
    async respondToPermission(requestId, response) {
        const pending = this.pendingPermissions.get(requestId);
        if (!pending) {
            throw new Error(`No pending permission request with id '${requestId}'`);
        }
        this.pendingPermissions.delete(requestId);
        const selectedOption = selectPermissionOption(pending.options, response);
        pending.resolve(selectedOption
            ? {
                outcome: {
                    outcome: "selected",
                    optionId: selectedOption.optionId,
                },
            }
            : { outcome: { outcome: "cancelled" } });
        this.pushEvent({
            type: "permission_resolved",
            provider: this.provider,
            requestId,
            resolution: response,
            turnId: pending.turnId ?? undefined,
        });
        if (response.behavior === "deny" && response.interrupt && this.connection && this.sessionId) {
            await this.connection.cancel({ sessionId: this.sessionId });
        }
    }
    describePersistence() {
        if (!this.sessionId) {
            return null;
        }
        return {
            provider: this.provider,
            sessionId: this.sessionId,
            nativeHandle: this.sessionId,
            metadata: {
                ...this.config,
                title: this.currentTitle,
            },
        };
    }
    async interrupt() {
        if (!this.connection || !this.sessionId) {
            return;
        }
        for (const pending of this.pendingPermissions.values()) {
            pending.resolve({ outcome: { outcome: "cancelled" } });
        }
        this.pendingPermissions.clear();
        if (this.activeForegroundTurnId) {
            await this.connection.cancel({ sessionId: this.sessionId });
        }
    }
    async close() {
        if (this.closed) {
            return;
        }
        this.closed = true;
        for (const pending of this.pendingPermissions.values()) {
            pending.resolve({ outcome: { outcome: "cancelled" } });
        }
        this.pendingPermissions.clear();
        if (this.connection && this.sessionId) {
            try {
                if (this.activeForegroundTurnId) {
                    await this.connection.cancel({ sessionId: this.sessionId });
                }
            }
            catch { }
            try {
                if (this.agentCapabilities?.sessionCapabilities?.close) {
                    await this.connection.unstable_closeSession({ sessionId: this.sessionId });
                }
            }
            catch (error) {
                this.logger.debug({ err: error }, "ACP closeSession failed during shutdown");
            }
        }
        for (const terminal of this.terminalEntries.values()) {
            terminal.child.kill("SIGTERM");
        }
        this.terminalEntries.clear();
        if (this.child) {
            this.child.kill("SIGTERM");
            await waitForChildExit(this.child, 2000);
        }
        this.subscribers.clear();
        this.connection = null;
        this.child = null;
        this.activeForegroundTurnId = null;
    }
    async requestPermission(params) {
        const requestId = randomUUID();
        let toolSnapshot = this.toolCalls.get(params.toolCall.toolCallId) ??
            mergeToolSnapshot(params.toolCall.toolCallId, params.toolCall);
        if (this.toolSnapshotTransformer) {
            toolSnapshot = this.toolSnapshotTransformer(toolSnapshot);
        }
        const request = mapPermissionRequest(this.provider, requestId, params, toolSnapshot);
        const promise = new Promise((resolve, reject) => {
            this.pendingPermissions.set(requestId, {
                request,
                options: params.options,
                resolve,
                reject,
                turnId: this.activeForegroundTurnId,
            });
        });
        this.pushEvent({
            type: "permission_requested",
            provider: this.provider,
            request,
            turnId: this.activeForegroundTurnId ?? undefined,
        });
        return promise;
    }
    async sessionUpdate(params) {
        if (params.sessionId !== this.sessionId) {
            return;
        }
        const events = this.translateSessionUpdate(params.update);
        if (this.replayingHistory) {
            for (const event of events) {
                if (event.type === "timeline") {
                    this.persistedHistory.push(event.item);
                }
            }
            return;
        }
        for (const event of events) {
            this.pushEvent(event);
        }
    }
    async readTextFile(params) {
        const raw = await fs.readFile(params.path, "utf8");
        if (!params.line && !params.limit) {
            return { content: raw };
        }
        const lines = raw.split(/\r?\n/);
        const start = Math.max((params.line ?? 1) - 1, 0);
        const end = params.limit ? start + params.limit : undefined;
        return { content: lines.slice(start, end).join("\n") };
    }
    async writeTextFile(params) {
        await fs.mkdir(path.dirname(params.path), { recursive: true });
        await fs.writeFile(params.path, params.content, "utf8");
        return {};
    }
    async createTerminal(params) {
        const terminalId = randomUUID();
        const env = Object.fromEntries((params.env ?? []).map((entry) => [entry.name, entry.value]));
        const child = spawn(params.command, params.args ?? [], {
            cwd: params.cwd ?? this.config.cwd,
            env: {
                ...applyProviderEnv(process.env, this.runtimeSettings),
                ...env,
            },
            shell: process.platform === "win32",
            stdio: ["ignore", "pipe", "pipe"],
        });
        let resolveExit;
        let rejectExit;
        const waitForExit = new Promise((resolve, reject) => {
            resolveExit = resolve;
            rejectExit = reject;
        });
        const entry = {
            id: terminalId,
            child,
            output: "",
            truncated: false,
            outputByteLimit: params.outputByteLimit ?? null,
            exit: null,
            waitForExit,
            resolveExit,
            rejectExit,
        };
        child.stdout.on("data", (chunk) => appendTerminalOutput(entry, chunk.toString()));
        child.stderr.on("data", (chunk) => appendTerminalOutput(entry, chunk.toString()));
        child.once("error", (error) => rejectExit(error instanceof Error ? error : new Error(String(error))));
        child.once("exit", (code, signal) => {
            const exit = { exitCode: code, signal };
            entry.exit = exit;
            resolveExit(exit);
        });
        this.terminalEntries.set(terminalId, entry);
        return { terminalId };
    }
    async terminalOutput(params) {
        const entry = this.getTerminalEntry(params.terminalId);
        return {
            output: entry.output,
            truncated: entry.truncated,
            exitStatus: entry.exit ?? undefined,
        };
    }
    async waitForTerminalExit(params) {
        const entry = this.getTerminalEntry(params.terminalId);
        return entry.waitForExit;
    }
    async releaseTerminal(params) {
        const entry = this.getTerminalEntry(params.terminalId);
        if (!entry.exit) {
            entry.child.kill("SIGTERM");
        }
        this.terminalEntries.delete(params.terminalId);
    }
    async killTerminal(params) {
        const entry = this.getTerminalEntry(params.terminalId);
        if (!entry.exit) {
            entry.child.kill("SIGTERM");
        }
        return {};
    }
    async spawnProcess() {
        const prefix = resolveProviderCommandPrefix(this.runtimeSettings?.command, () => {
            const resolved = findExecutable(this.defaultCommand[0]);
            if (!resolved) {
                throw new Error(`${this.provider} command '${this.defaultCommand[0]}' not found`);
            }
            return resolved;
        });
        const command = prefix.command;
        const args = [...prefix.args, ...this.defaultCommand.slice(1)];
        const child = spawn(command, args, {
            cwd: this.config.cwd,
            env: {
                ...applyProviderEnv(process.env, this.runtimeSettings),
                ...(this.launchEnv ?? {}),
            },
            shell: process.platform === "win32",
            stdio: ["pipe", "pipe", "pipe"],
        });
        const stderrChunks = [];
        child.stderr.on("data", (chunk) => {
            stderrChunks.push(chunk.toString());
        });
        child.once("exit", (code, signal) => {
            if (this.closed) {
                return;
            }
            if (this.activeForegroundTurnId) {
                this.synthesizeCanceledToolCalls();
                this.finishTurn({
                    type: "turn_failed",
                    provider: this.provider,
                    error: `ACP agent exited unexpectedly (${code ?? "null"}${signal ? `, ${signal}` : ""})`,
                    diagnostic: stderrChunks.join("").trim() || undefined,
                    turnId: this.activeForegroundTurnId,
                });
            }
        });
        if (!child.stdin || !child.stdout) {
            throw new Error(`${this.provider} ACP process did not expose stdio pipes`);
        }
        const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
        const connection = new ClientSideConnection(() => this, stream);
        const initialize = await connection.initialize({
            protocolVersion: PROTOCOL_VERSION,
            clientCapabilities: ACP_CLIENT_CAPABILITIES,
            clientInfo: { name: "Paseo", version: "dev" },
        });
        return { child, connection, initialize };
    }
    applySessionState(response) {
        const transformed = this.sessionResponseTransformer
            ? this.sessionResponseTransformer(response)
            : response;
        this.configOptions = transformed.configOptions ?? [];
        const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, this.configOptions);
        this.availableModes = modeInfo.modes;
        this.availableModels = this.deriveAvailableModels(transformed.models);
        this.currentMode = modeInfo.currentModeId ?? this.currentMode;
        this.currentModel =
            transformed.models?.currentModelId ?? deriveCurrentConfigValue(this.configOptions, "model");
        this.thinkingOptionId =
            deriveCurrentConfigValue(this.configOptions, "thought_level") ?? this.thinkingOptionId;
    }
    async applyConfiguredOverrides() {
        if (this.config.modeId && this.config.modeId !== this.currentMode) {
            await this.setMode(this.config.modeId);
        }
        if (this.config.model && this.config.model !== this.currentModel) {
            await this.setModel(this.config.model);
        }
        if (this.config.thinkingOptionId && this.config.thinkingOptionId !== this.thinkingOptionId) {
            await this.setThinkingOption(this.config.thinkingOptionId);
        }
    }
    translateSessionUpdate(update) {
        switch (update.sessionUpdate) {
            case "user_message_chunk": {
                const item = this.createMessageTimelineItem("user_message", update);
                if (!item) {
                    return [];
                }
                const shouldSuppress = this.suppressUserEchoMessageId &&
                    update.messageId === this.suppressUserEchoMessageId &&
                    this.suppressUserEchoText &&
                    item.text === this.suppressUserEchoText;
                if (shouldSuppress) {
                    return [];
                }
                return [this.wrapTimeline(item)];
            }
            case "agent_message_chunk": {
                const item = this.createMessageTimelineItem("assistant_message", update);
                return item ? [this.wrapTimeline(item)] : [];
            }
            case "agent_thought_chunk": {
                const item = this.createMessageTimelineItem("reasoning", update);
                return item ? [this.wrapTimeline(item)] : [];
            }
            case "tool_call": {
                let snapshot = mergeToolSnapshot(update.toolCallId, update);
                if (this.toolSnapshotTransformer) {
                    snapshot = this.toolSnapshotTransformer(snapshot);
                }
                this.toolCalls.set(update.toolCallId, snapshot);
                return [this.wrapTimeline(mapToolSnapshotToTimeline(snapshot, this.terminalEntries))];
            }
            case "tool_call_update": {
                const previous = this.toolCalls.get(update.toolCallId);
                let snapshot = mergeToolSnapshot(update.toolCallId, update, previous);
                if (this.toolSnapshotTransformer) {
                    snapshot = this.toolSnapshotTransformer(snapshot);
                }
                this.toolCalls.set(update.toolCallId, snapshot);
                return [this.wrapTimeline(mapToolSnapshotToTimeline(snapshot, this.terminalEntries))];
            }
            case "plan":
                return [this.wrapTimeline(mapPlanToTimeline(update))];
            case "current_mode_update":
                this.handleCurrentModeUpdate(update);
                return [];
            case "config_option_update":
                this.handleConfigOptionUpdate(update);
                return [];
            case "session_info_update":
                this.handleSessionInfoUpdate(update);
                return [];
            case "usage_update":
                this.handleUsageUpdate(update);
                return [];
            case "available_commands_update":
                this.cachedCommands = update.availableCommands.map((command) => ({
                    name: command.name,
                    description: command.description,
                    argumentHint: "",
                }));
                return [];
            default:
                return [];
        }
    }
    createMessageTimelineItem(type, update) {
        const chunkText = contentBlockToText(update.content);
        if (!chunkText) {
            return null;
        }
        const key = `${type}:${update.messageId ?? "default"}`;
        const state = this.messageAssemblies.get(key) ?? { text: "" };
        state.text += chunkText;
        this.messageAssemblies.set(key, state);
        if (type === "user_message") {
            return { type: "user_message", text: state.text, messageId: update.messageId ?? undefined };
        }
        if (type === "assistant_message") {
            return { type: "assistant_message", text: chunkText };
        }
        return { type: "reasoning", text: chunkText };
    }
    handleCurrentModeUpdate(update) {
        this.currentMode = update.currentModeId;
    }
    handleConfigOptionUpdate(update) {
        this.configOptions = update.configOptions;
        const modeInfo = deriveModesFromACP(this.defaultModes, null, this.configOptions);
        this.availableModes = modeInfo.modes;
        this.availableModels = this.deriveAvailableModels(null);
        this.currentMode = modeInfo.currentModeId ?? this.currentMode;
        this.currentModel = deriveCurrentConfigValue(this.configOptions, "model") ?? this.currentModel;
        this.thinkingOptionId =
            deriveCurrentConfigValue(this.configOptions, "thought_level") ?? this.thinkingOptionId;
    }
    deriveAvailableModels(models) {
        const availableModels = deriveModelDefinitionsFromACP(this.provider, models, this.configOptions);
        return this.modelTransformer ? this.modelTransformer(availableModels) : availableModels;
    }
    handleSessionInfoUpdate(update) {
        if ("title" in update) {
            this.currentTitle = update.title ?? null;
        }
        if ("updatedAt" in update) {
            this.lastActivityAt = update.updatedAt ?? null;
        }
    }
    handleUsageUpdate(update) {
        void update;
    }
    handlePromptResponse(response, turnId) {
        this.currentTurnUsage = mapACPUsage(response.usage) ?? this.currentTurnUsage;
        switch (response.stopReason) {
            case "cancelled":
                this.synthesizeCanceledToolCalls();
                this.finishTurn({
                    type: "turn_canceled",
                    provider: this.provider,
                    reason: "Interrupted",
                    turnId,
                });
                break;
            case "end_turn":
            case "max_tokens":
            case "max_turn_requests":
            case "refusal":
            default:
                this.finishTurn({
                    type: "turn_completed",
                    provider: this.provider,
                    usage: this.currentTurnUsage,
                    turnId,
                });
                break;
        }
    }
    wrapTimeline(item) {
        return {
            type: "timeline",
            provider: this.provider,
            item,
            turnId: this.activeForegroundTurnId ?? undefined,
        };
    }
    pushEvent(event) {
        for (const subscriber of this.subscribers) {
            subscriber(event);
        }
    }
    finishTurn(event) {
        this.activeForegroundTurnId = null;
        this.suppressUserEchoMessageId = null;
        this.suppressUserEchoText = null;
        this.pushEvent(event);
    }
    emitBootstrapThreadEvent() {
        if (!this.bootstrapThreadEventPending || !this.sessionId) {
            return;
        }
        this.bootstrapThreadEventPending = false;
        this.pushEvent({
            type: "thread_started",
            provider: this.provider,
            sessionId: this.sessionId,
        });
    }
    synthesizeCanceledToolCalls() {
        for (const snapshot of this.toolCalls.values()) {
            const mapped = mapToolSnapshotToTimeline(snapshot, this.terminalEntries);
            if (mapped.status === "running") {
                this.pushEvent(this.wrapTimeline({
                    ...mapped,
                    status: "canceled",
                    error: null,
                }));
            }
        }
    }
    collectDiagnostic(message) {
        const parts = [message];
        if (this.child?.exitCode != null) {
            parts.push(`exitCode=${this.child.exitCode}`);
        }
        if (this.child?.signalCode) {
            parts.push(`signal=${this.child.signalCode}`);
        }
        return parts.length > 0 ? parts.join(" | ") : undefined;
    }
    getSelectConfigOption(category) {
        const option = this.configOptions.find((entry) => entry.type === "select" && entry.category === category);
        return option ?? null;
    }
    getTerminalEntry(terminalId) {
        const entry = this.terminalEntries.get(terminalId);
        if (!entry) {
            throw new Error(`Unknown terminal '${terminalId}'`);
        }
        return entry;
    }
}
function flattenSelectOptions(options) {
    const flattened = [];
    for (const option of options) {
        if ("value" in option) {
            flattened.push(option);
            continue;
        }
        for (const groupOption of option.options) {
            flattened.push({ ...groupOption, group: option.group });
        }
    }
    return flattened;
}
function deriveSelectorOptions(configOptions, category) {
    const option = configOptions?.find((entry) => entry.type === "select" && entry.category === category);
    if (!option) {
        return [];
    }
    return flattenSelectOptions(option.options).map((value) => ({
        id: value.value,
        label: value.name,
        description: value.description ?? undefined,
        isDefault: value.value === option.currentValue,
        metadata: value.group ? { group: value.group } : undefined,
    }));
}
function deriveCurrentConfigValue(configOptions, category) {
    const option = configOptions?.find((entry) => entry.type === "select" && entry.category === category);
    return option?.currentValue ?? null;
}
function normalizeMcpServers(servers) {
    if (!servers) {
        return [];
    }
    return Object.entries(servers).map(([name, config]) => {
        if (config.type === "stdio") {
            return {
                name,
                command: config.command,
                args: config.args ?? [],
                env: Object.entries(config.env ?? {}).map(([envName, value]) => ({
                    name: envName,
                    value,
                })),
            };
        }
        if (config.type === "http") {
            return {
                type: "http",
                name,
                url: config.url,
                headers: Object.entries(config.headers ?? {}).map(([headerName, value]) => ({
                    name: headerName,
                    value,
                })),
            };
        }
        return {
            type: "sse",
            name,
            url: config.url,
            headers: Object.entries(config.headers ?? {}).map(([headerName, value]) => ({
                name: headerName,
                value,
            })),
        };
    });
}
function toACPContentBlocks(prompt) {
    if (typeof prompt === "string") {
        return [{ type: "text", text: prompt }];
    }
    return prompt.map((block) => {
        if (block.type === "text") {
            return { type: "text", text: block.text };
        }
        return {
            type: "image",
            data: block.data,
            mimeType: block.mimeType,
        };
    });
}
function extractPromptText(prompt) {
    if (typeof prompt === "string") {
        return prompt;
    }
    return prompt
        .filter((block) => block.type === "text")
        .map((block) => block.text)
        .join("");
}
function contentBlockToText(content) {
    switch (content.type) {
        case "text":
            return content.text;
        case "resource_link":
            return content.title ?? content.uri;
        case "resource":
            return "text" in content.resource ? content.resource.text : `[resource:${content.resource.mimeType ?? "binary"}]`;
        case "image":
            return "[image]";
        case "audio":
            return "[audio]";
        default:
            return "";
    }
}
function mergeToolSnapshot(toolCallId, update, previous) {
    const isFull = "title" in update && typeof update.title === "string";
    return {
        toolCallId,
        title: (update.title ?? previous?.title ?? toolCallId),
        kind: update.kind ?? previous?.kind ?? null,
        status: update.status ?? previous?.status ?? null,
        content: update.content !== undefined ? update.content : previous?.content ?? null,
        locations: update.locations !== undefined ? update.locations : previous?.locations ?? null,
        rawInput: update.rawInput !== undefined ? update.rawInput : previous?.rawInput,
        rawOutput: update.rawOutput !== undefined ? update.rawOutput : previous?.rawOutput,
        ...(isFull ? {} : {}),
    };
}
function mapPlanToTimeline(plan) {
    return {
        type: "todo",
        items: plan.entries.map((entry) => ({
            text: entry.content,
            completed: entry.status === "completed",
        })),
    };
}
function mapToolSnapshotToTimeline(snapshot, terminals) {
    const status = mapToolStatus(snapshot.status);
    const detail = mapToolDetail(snapshot, terminals);
    const base = {
        type: "tool_call",
        callId: snapshot.toolCallId,
        name: snapshot.kind ?? snapshot.title,
        detail,
        metadata: {
            kind: snapshot.kind ?? undefined,
            title: snapshot.title,
        },
    };
    if (status === "failed") {
        return {
            ...base,
            status: "failed",
            error: { message: readErrorMessage(snapshot.rawOutput) },
        };
    }
    if (status === "completed") {
        return {
            ...base,
            status: "completed",
            error: null,
        };
    }
    return {
        ...base,
        status: "running",
        error: null,
    };
}
function mapToolStatus(status) {
    switch (status) {
        case "completed":
            return "completed";
        case "failed":
            return "failed";
        case "pending":
        case "in_progress":
        default:
            return "running";
    }
}
function mapToolDetail(snapshot, terminals) {
    const firstLocation = snapshot.locations?.[0]?.path;
    const textContent = extractToolText(snapshot.content);
    const diffContent = extractDiffContent(snapshot.content);
    const terminalContent = extractTerminalContent(snapshot.content, terminals);
    const rawInput = readRecord(snapshot.rawInput);
    const rawOutput = readRecord(snapshot.rawOutput);
    switch (snapshot.kind) {
        case "read":
            return {
                type: "read",
                filePath: firstLocation ?? readString(rawInput, ["path", "filePath", "file"]) ?? snapshot.title,
                content: textContent ?? readString(rawOutput, ["content", "text"]),
                offset: readNumber(rawInput, ["offset", "line"]),
                limit: readNumber(rawInput, ["limit"]),
            };
        case "edit":
        case "delete":
            return {
                type: "edit",
                filePath: firstLocation ?? readString(rawInput, ["path", "filePath", "file"]) ?? snapshot.title,
                oldString: diffContent?.oldText ?? readString(rawInput, ["oldText", "oldString"]),
                newString: snapshot.kind === "delete"
                    ? ""
                    : diffContent?.newText ?? readString(rawInput, ["newText", "newString"]),
                unifiedDiff: textContent ?? undefined,
            };
        case "search":
            return {
                type: "search",
                query: readString(rawInput, ["query", "pattern"]) ?? snapshot.title,
                toolName: "search",
                content: textContent ?? readString(rawOutput, ["content", "text"]),
                filePaths: snapshot.locations?.map((location) => location.path),
            };
        case "execute":
            return {
                type: "shell",
                command: terminalContent?.command ??
                    buildShellCommand(rawInput) ??
                    readString(rawInput, ["command"]) ??
                    snapshot.title,
                cwd: terminalContent?.cwd ?? readString(rawInput, ["cwd"]),
                output: terminalContent?.output ?? textContent ?? readString(rawOutput, ["output", "text"]),
                exitCode: terminalContent?.exitCode ?? readNumber(rawOutput, ["exitCode"]),
            };
        case "fetch":
            return {
                type: "fetch",
                url: readString(rawInput, ["url"]) ?? snapshot.title,
                prompt: readString(rawInput, ["prompt"]),
                result: textContent ?? readString(rawOutput, ["result", "text", "content"]),
                code: readNumber(rawOutput, ["status", "code"]),
            };
        case "think":
            return {
                type: "plain_text",
                label: snapshot.title,
                icon: "brain",
                text: textContent ?? stringifyUnknown(snapshot.rawOutput),
            };
        case "switch_mode":
            return {
                type: "plain_text",
                label: snapshot.title,
                icon: "sparkles",
                text: textContent ?? stringifyUnknown(snapshot.rawInput),
            };
        default:
            if (terminalContent) {
                return {
                    type: "shell",
                    command: terminalContent.command ?? snapshot.title,
                    cwd: terminalContent.cwd,
                    output: terminalContent.output,
                    exitCode: terminalContent.exitCode,
                };
            }
            if (textContent) {
                return {
                    type: "plain_text",
                    label: snapshot.title,
                    text: textContent,
                    icon: "wrench",
                };
            }
            return {
                type: "unknown",
                input: snapshot.rawInput ?? null,
                output: snapshot.rawOutput ?? null,
            };
    }
}
function extractToolText(content) {
    if (!content) {
        return undefined;
    }
    const parts = [];
    for (const item of content) {
        if (item.type === "content") {
            const text = contentBlockToText(item.content);
            if (text) {
                parts.push(text);
            }
        }
    }
    return parts.length > 0 ? parts.join("\n") : undefined;
}
function extractDiffContent(content) {
    const diff = content?.find((item) => item.type === "diff");
    return diff ? { oldText: diff.oldText ?? undefined, newText: diff.newText } : null;
}
function extractTerminalContent(content, terminals) {
    const terminal = content?.find((item) => item.type === "terminal");
    if (!terminal) {
        return undefined;
    }
    const entry = terminals.get(terminal.terminalId);
    if (!entry) {
        return undefined;
    }
    return {
        output: entry.output,
        exitCode: entry.exit?.exitCode ?? null,
    };
}
function mapPermissionRequest(provider, requestId, params, snapshot) {
    const kind = snapshot.kind === "switch_mode" ? "mode" : "tool";
    return {
        id: requestId,
        provider,
        name: snapshot.kind ?? snapshot.title,
        kind,
        title: params.toolCall.title ?? snapshot.title,
        detail: mapToolDetail(snapshot, new Map()),
        metadata: {
            toolCallId: params.toolCall.toolCallId,
            rawRequest: params,
            options: params.options,
        },
    };
}
function selectPermissionOption(options, response) {
    const order = response.behavior === "allow"
        ? ["allow_once", "allow_always"]
        : ["reject_once", "reject_always"];
    for (const kind of order) {
        const match = options.find((option) => option.kind === kind);
        if (match) {
            return match;
        }
    }
    return null;
}
function appendTerminalOutput(entry, chunk) {
    entry.output += chunk;
    const limit = entry.outputByteLimit;
    if (!limit) {
        return;
    }
    while (Buffer.byteLength(entry.output, "utf8") > limit && entry.output.length > 0) {
        entry.output = entry.output.slice(1);
        entry.truncated = true;
    }
}
function readRecord(value) {
    return value && typeof value === "object" && !Array.isArray(value)
        ? value
        : null;
}
function readString(record, keys) {
    if (!record) {
        return undefined;
    }
    for (const key of keys) {
        const value = record[key];
        if (typeof value === "string" && value.trim().length > 0) {
            return value;
        }
    }
    return undefined;
}
function readNumber(record, keys) {
    if (!record) {
        return undefined;
    }
    for (const key of keys) {
        const value = record[key];
        if (typeof value === "number" && Number.isFinite(value)) {
            return value;
        }
    }
    return undefined;
}
function buildShellCommand(record) {
    if (!record) {
        return undefined;
    }
    const command = readString(record, ["command"]);
    const args = Array.isArray(record["args"])
        ? record["args"].filter((value) => typeof value === "string")
        : [];
    if (!command) {
        return undefined;
    }
    return args.length > 0 ? `${command} ${args.join(" ")}` : command;
}
function readErrorMessage(value) {
    if (typeof value === "string") {
        return value;
    }
    const record = readRecord(value);
    return readString(record, ["message", "error"]) ?? "Tool call failed";
}
function stringifyUnknown(value) {
    if (value == null) {
        return undefined;
    }
    if (typeof value === "string") {
        return value;
    }
    try {
        return JSON.stringify(value);
    }
    catch {
        return String(value);
    }
}
function coerceSessionConfigMetadata(metadata) {
    if (!metadata || typeof metadata !== "object") {
        return {};
    }
    return metadata;
}
async function waitForChildExit(child, timeoutMs) {
    if (child.exitCode !== null || child.signalCode !== null) {
        return;
    }
    await Promise.race([
        new Promise((resolve) => child.once("exit", () => resolve())),
        new Promise((resolve) => setTimeout(resolve, timeoutMs)),
    ]);
    if (child.exitCode === null && child.signalCode === null) {
        child.kill("SIGKILL");
    }
}
//# sourceMappingURL=acp-agent.js.map