import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
import net from "node:net";
import { z } from "zod";
import { applyProviderEnv, resolveProviderCommandPrefix, } from "../provider-launch-config.js";
import { findExecutable, quoteWindowsArgument, quoteWindowsCommand, } from "../../../utils/executable.js";
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
const OPENCODE_CAPABILITIES = {
    supportsStreaming: true,
    supportsSessionPersistence: true,
    supportsDynamicModes: true,
    supportsMcpServers: true,
    supportsReasoningStream: true,
    supportsToolInvocations: true,
};
const DEFAULT_MODES = [
    {
        id: "build",
        label: "Build",
        description: "Allows edits and tool execution for implementation work",
    },
    {
        id: "plan",
        label: "Plan",
        description: "Read-only planning mode that avoids file edits",
    },
];
const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"];
const OpencodeToolStateSchema = z
    .object({
    status: z.string().optional(),
    input: z.unknown().optional(),
    output: z.unknown().optional(),
    error: z.unknown().optional(),
})
    .passthrough();
const OpencodeToolPartBaseSchema = z
    .object({
    tool: z.string().trim().min(1),
    state: OpencodeToolStateSchema.optional(),
})
    .passthrough();
const OpencodeToolPartWithCallIdSchema = OpencodeToolPartBaseSchema.extend({
    callID: z.string().trim().min(1),
    id: z.string().optional(),
}).transform((part) => ({
    toolName: part.tool,
    callId: part.callID,
    status: part.state?.status,
    input: part.state?.input,
    output: part.state?.output,
    error: part.state?.error,
}));
const OpencodeToolPartWithIdSchema = OpencodeToolPartBaseSchema.extend({
    id: z.string().trim().min(1),
    callID: z.string().optional(),
}).transform((part) => ({
    toolName: part.tool,
    callId: part.id,
    status: part.state?.status,
    input: part.state?.input,
    output: part.state?.output,
    error: part.state?.error,
}));
const OpencodeToolPartWithoutIdSchema = OpencodeToolPartBaseSchema.extend({
    id: z.string().optional(),
    callID: z.string().optional(),
}).transform((part) => ({
    toolName: part.tool,
    callId: undefined,
    status: part.state?.status,
    input: part.state?.input,
    output: part.state?.output,
    error: part.state?.error,
}));
const OpencodeToolPartSchema = z.union([
    OpencodeToolPartWithCallIdSchema,
    OpencodeToolPartWithIdSchema,
    OpencodeToolPartWithoutIdSchema,
]);
const OpencodeToolPartTimelineEnvelopeSchema = OpencodeToolPartSchema.transform((part) => ({
    toolName: part.toolName,
    callId: part.callId,
    status: part.status,
    input: part.input,
    output: part.output,
    error: part.error,
}));
const OpencodeToolPartToTimelineItemSchema = OpencodeToolPartTimelineEnvelopeSchema.transform((part) => mapOpencodeToolCall({
    toolName: part.toolName,
    callId: part.callId,
    status: part.status,
    input: part.input,
    output: part.output,
    error: part.error,
}));
function resolveOpenCodeBinary() {
    const found = findExecutable("opencode");
    if (found) {
        return found;
    }
    throw new Error("OpenCode binary not found. Install OpenCode (https://github.com/opencode-ai/opencode) and ensure it is available in your shell PATH.");
}
function toOpenCodeMcpConfig(config) {
    if (config.type === "stdio") {
        return {
            type: "local",
            command: [config.command, ...(config.args ?? [])],
            ...(config.env ? { environment: config.env } : {}),
            enabled: true,
        };
    }
    return {
        type: "remote",
        url: config.url,
        ...(config.headers ? { headers: config.headers } : {}),
        enabled: true,
    };
}
function stringifyUnknownError(error) {
    if (typeof error === "string") {
        return error;
    }
    try {
        return JSON.stringify(error);
    }
    catch {
        return String(error);
    }
}
function normalizeTurnFailureError(error) {
    const normalized = stringifyUnknownError(error).trim();
    return normalized.length > 0 ? normalized : "Unknown error";
}
function isAlreadyPresentMcpError(error) {
    const normalized = stringifyUnknownError(error).toLowerCase();
    return MCP_ALREADY_PRESENT_ERROR_TOKENS.some((token) => normalized.includes(token));
}
async function findAvailablePort() {
    return new Promise((resolve, reject) => {
        const server = net.createServer();
        server.listen(0, () => {
            const address = server.address();
            if (address && typeof address === "object") {
                const port = address.port;
                server.close(() => resolve(port));
            }
            else {
                server.close(() => reject(new Error("Failed to get port")));
            }
        });
        server.on("error", reject);
    });
}
function resolvePartDedupeKey(part, partType) {
    const partId = part.id;
    if (typeof partId === "string" && partId.trim().length > 0) {
        return `${partType}:${partId}`;
    }
    const messageId = part.messageID;
    if (typeof messageId === "string" && messageId.trim().length > 0) {
        return `${partType}:message:${messageId}`;
    }
    return null;
}
function normalizeOpenCodeModeId(modeId) {
    const trimmed = typeof modeId === "string" ? modeId.trim() : "";
    if (!trimmed || trimmed === "default") {
        return "build";
    }
    return trimmed;
}
function sortOpenCodeModes(modes) {
    const order = new Map(DEFAULT_MODES.map((mode, index) => [mode.id, index]));
    return [...modes].sort((left, right) => {
        const leftOrder = order.get(left.id) ?? Number.MAX_SAFE_INTEGER;
        const rightOrder = order.get(right.id) ?? Number.MAX_SAFE_INTEGER;
        if (leftOrder !== rightOrder) {
            return leftOrder - rightOrder;
        }
        return left.label.localeCompare(right.label);
    });
}
export class OpenCodeServerManager {
    constructor(logger, runtimeSettings) {
        this.server = null;
        this.port = null;
        this.startPromise = null;
        this.logger = logger;
        this.runtimeSettings = runtimeSettings;
        this.runtimeSettingsKey = JSON.stringify(runtimeSettings ?? {});
    }
    static getInstance(logger, runtimeSettings) {
        const nextSettingsKey = JSON.stringify(runtimeSettings ?? {});
        if (!OpenCodeServerManager.instance) {
            OpenCodeServerManager.instance = new OpenCodeServerManager(logger, runtimeSettings);
            OpenCodeServerManager.registerExitHandler();
        }
        else if (OpenCodeServerManager.instance.runtimeSettingsKey !== nextSettingsKey) {
            logger.warn({
                existingRuntimeSettings: OpenCodeServerManager.instance.runtimeSettingsKey,
                requestedRuntimeSettings: nextSettingsKey,
            }, "OpenCode server manager already initialized with different runtime settings");
        }
        return OpenCodeServerManager.instance;
    }
    static registerExitHandler() {
        if (OpenCodeServerManager.exitHandlerRegistered) {
            return;
        }
        OpenCodeServerManager.exitHandlerRegistered = true;
        const cleanup = () => {
            const instance = OpenCodeServerManager.instance;
            if (instance?.server && !instance.server.killed) {
                instance.server.kill("SIGTERM");
            }
        };
        process.on("exit", cleanup);
        process.on("SIGTERM", cleanup);
        process.on("SIGINT", cleanup);
    }
    async ensureRunning() {
        if (this.startPromise) {
            return this.startPromise;
        }
        if (this.server && this.port && !this.server.killed) {
            return { port: this.port, url: `http://127.0.0.1:${this.port}` };
        }
        this.startPromise = this.startServer();
        try {
            const result = await this.startPromise;
            return result;
        }
        finally {
            this.startPromise = null;
        }
    }
    async startServer() {
        this.port = await findAvailablePort();
        const url = `http://127.0.0.1:${this.port}`;
        const launchPrefix = resolveProviderCommandPrefix(this.runtimeSettings?.command, resolveOpenCodeBinary);
        return new Promise((resolve, reject) => {
            this.server = spawn(quoteWindowsCommand(launchPrefix.command), [...launchPrefix.args, "serve", "--port", String(this.port)].map((argument) => quoteWindowsArgument(argument)), {
                shell: process.platform === "win32",
                stdio: ["ignore", "pipe", "pipe"],
                env: applyProviderEnv(process.env, this.runtimeSettings),
            });
            let started = false;
            const timeout = setTimeout(() => {
                if (!started) {
                    reject(new Error("OpenCode server startup timeout"));
                }
            }, 30000);
            this.server.stdout?.on("data", (data) => {
                const output = data.toString();
                if (output.includes("listening on") && !started) {
                    started = true;
                    clearTimeout(timeout);
                    resolve({ port: this.port, url });
                }
            });
            this.server.stderr?.on("data", (data) => {
                this.logger.error({ stderr: data.toString().trim() }, "OpenCode server stderr");
            });
            this.server.on("error", (error) => {
                clearTimeout(timeout);
                reject(error);
            });
            this.server.on("exit", (code) => {
                if (!started) {
                    clearTimeout(timeout);
                    reject(new Error(`OpenCode server exited with code ${code}`));
                }
                this.server = null;
                this.port = null;
            });
        });
    }
    async shutdown() {
        if (this.server && !this.server.killed) {
            this.server.kill("SIGTERM");
            await new Promise((resolve) => {
                const timeout = setTimeout(() => {
                    this.server?.kill("SIGKILL");
                    resolve();
                }, 5000);
                this.server?.on("exit", () => {
                    clearTimeout(timeout);
                    resolve();
                });
            });
        }
        this.server = null;
        this.port = null;
    }
}
OpenCodeServerManager.instance = null;
OpenCodeServerManager.exitHandlerRegistered = false;
export class OpenCodeAgentClient {
    constructor(logger, runtimeSettings) {
        this.provider = "opencode";
        this.capabilities = OPENCODE_CAPABILITIES;
        this.logger = logger.child({ module: "agent", provider: "opencode" });
        this.runtimeSettings = runtimeSettings;
        this.serverManager = OpenCodeServerManager.getInstance(this.logger, runtimeSettings);
    }
    async createSession(config, _launchContext) {
        const openCodeConfig = this.assertConfig(config);
        const { url } = await this.serverManager.ensureRunning();
        const client = createOpencodeClient({
            baseUrl: url,
            directory: openCodeConfig.cwd,
        });
        // Set a timeout for session creation to fail fast
        const timeoutPromise = new Promise((_, reject) => {
            setTimeout(() => reject(new Error("OpenCode session.create timed out after 10s")), 10000);
        });
        const response = await Promise.race([
            client.session.create({ directory: openCodeConfig.cwd }),
            timeoutPromise,
        ]);
        if (response.error) {
            throw new Error(`Failed to create OpenCode session: ${JSON.stringify(response.error)}`);
        }
        const session = response.data;
        if (!session) {
            throw new Error("OpenCode session creation returned no data");
        }
        return new OpenCodeAgentSession(openCodeConfig, client, session.id, this.logger);
    }
    async resumeSession(handle, overrides, _launchContext) {
        const cwd = overrides?.cwd ?? handle.metadata?.cwd;
        if (!cwd) {
            throw new Error("OpenCode resume requires the original working directory");
        }
        const config = {
            provider: "opencode",
            cwd,
            ...overrides,
        };
        const openCodeConfig = this.assertConfig(config);
        const { url } = await this.serverManager.ensureRunning();
        const client = createOpencodeClient({
            baseUrl: url,
            directory: openCodeConfig.cwd,
        });
        return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId, this.logger);
    }
    async listModels(options) {
        const { url } = await this.serverManager.ensureRunning();
        const client = createOpencodeClient({
            baseUrl: url,
            directory: options?.cwd ?? process.cwd(),
        });
        // Set a timeout for the API call to fail fast if OpenCode isn't responding
        const timeoutPromise = new Promise((_, reject) => {
            setTimeout(() => reject(new Error("OpenCode provider.list timed out after 10s - server may not be authenticated or connected to any providers")), 10000);
        });
        const response = await Promise.race([
            client.provider.list({ directory: options?.cwd ?? process.cwd() }),
            timeoutPromise,
        ]);
        if (response.error) {
            throw new Error(`Failed to fetch OpenCode providers: ${JSON.stringify(response.error)}`);
        }
        const providers = response.data;
        if (!providers) {
            return [];
        }
        // Only include models from connected providers (ones that are actually available)
        const connectedProviderIds = new Set(providers.connected);
        // Fail fast if no providers are connected
        if (connectedProviderIds.size === 0) {
            throw new Error("OpenCode has no connected providers. Please authenticate with at least one provider (e.g., openai, anthropic) or set appropriate environment variables (e.g., OPENAI_API_KEY).");
        }
        const models = [];
        for (const provider of providers.all) {
            // Skip providers that aren't connected/configured
            if (!connectedProviderIds.has(provider.id)) {
                continue;
            }
            for (const [modelId, model] of Object.entries(provider.models)) {
                const rawVariants = model.variants ? Object.keys(model.variants) : [];
                const thinkingOptions = rawVariants.map((id, index) => ({
                    id,
                    label: id,
                    isDefault: index === 0,
                }));
                models.push({
                    provider: "opencode",
                    id: `${provider.id}/${modelId}`,
                    label: model.name,
                    description: `${provider.name} - ${model.family ?? ""}`.trim(),
                    thinkingOptions: thinkingOptions.length > 0 ? thinkingOptions : undefined,
                    defaultThinkingOptionId: thinkingOptions[0]?.id,
                    metadata: {
                        providerId: provider.id,
                        providerName: provider.name,
                        modelId,
                        family: model.family,
                        releaseDate: model.release_date,
                        supportsAttachments: model.attachment,
                        supportsReasoning: model.reasoning,
                        supportsToolCall: model.tool_call,
                        cost: model.cost,
                    },
                });
            }
        }
        return models;
    }
    async listModes(options) {
        const { url } = await this.serverManager.ensureRunning();
        const directory = options?.cwd ?? process.cwd();
        const client = createOpencodeClient({ baseUrl: url, directory });
        const timeoutPromise = new Promise((_, reject) => {
            setTimeout(() => reject(new Error("OpenCode app.agents timed out after 10s")), 10000);
        });
        const response = await Promise.race([
            client.app.agents({ directory }),
            timeoutPromise,
        ]);
        if (response.error || !response.data) {
            return DEFAULT_MODES;
        }
        const discovered = response.data
            .filter((agent) => agent.mode === "primary" && agent.hidden !== true)
            .map((agent) => ({
            id: agent.name,
            label: agent.name.charAt(0).toUpperCase() + agent.name.slice(1),
            description: typeof agent.description === "string" && agent.description.trim().length > 0
                ? agent.description.trim()
                : DEFAULT_MODES.find((mode) => mode.id === agent.name)?.description,
        }));
        return discovered.length > 0 ? sortOpenCodeModes(discovered) : DEFAULT_MODES;
    }
    async listPersistedAgents(_options) {
        // TODO: Implement by listing sessions from OpenCode
        return [];
    }
    async isAvailable() {
        const command = this.runtimeSettings?.command;
        if (command?.mode === "replace") {
            return existsSync(command.argv[0]);
        }
        return true;
    }
    assertConfig(config) {
        if (config.provider !== "opencode") {
            throw new Error(`OpenCodeAgentClient received config for provider '${config.provider}'`);
        }
        return { ...config, provider: "opencode" };
    }
}
function stringifyStructuredAssistantMessage(value) {
    if (value === undefined) {
        return null;
    }
    if (typeof value === "string") {
        const trimmed = value.trim();
        return trimmed.length > 0 ? trimmed : null;
    }
    try {
        return JSON.stringify(value);
    }
    catch {
        return null;
    }
}
function readOpenCodeRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value)
        ? value
        : null;
}
function readNonEmptyString(value) {
    return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function normalizeQuestionOptions(value) {
    if (!Array.isArray(value)) {
        return null;
    }
    const options = [];
    for (const item of value) {
        if (typeof item === "string" && item.trim().length > 0) {
            options.push({ label: item.trim() });
            continue;
        }
        const record = readOpenCodeRecord(item);
        const label = readNonEmptyString(record?.label);
        if (!label) {
            continue;
        }
        const description = readNonEmptyString(record?.description);
        options.push(description ? { label, description } : { label });
    }
    return options;
}
export function translateOpenCodeEvent(event, state) {
    const events = [];
    if (!event || typeof event !== "object") {
        return events;
    }
    const e = event;
    const type = e.type;
    const props = e.properties ?? {};
    switch (type) {
        case "session.created":
        case "session.updated": {
            const sessionId = props.id;
            if (sessionId === state.sessionId) {
                events.push({
                    type: "thread_started",
                    sessionId: state.sessionId,
                    provider: "opencode",
                });
            }
            break;
        }
        case "message.updated": {
            const info = props.info;
            if (!info) {
                break;
            }
            const messageId = info.id;
            const messageSessionId = info.sessionID;
            const role = info.role;
            if (messageId && messageSessionId === state.sessionId && role) {
                state.messageRoles.set(messageId, role);
                if (role === "assistant" &&
                    !state.emittedStructuredMessageIds.has(messageId) &&
                    typeof info.time === "object" &&
                    info.time !== null &&
                    "completed" in info.time) {
                    const text = stringifyStructuredAssistantMessage(info.structured);
                    if (text) {
                        state.emittedStructuredMessageIds.add(messageId);
                        events.push({
                            type: "timeline",
                            provider: "opencode",
                            item: { type: "assistant_message", text },
                        });
                    }
                }
            }
            break;
        }
        case "message.part.updated": {
            const part = props.part;
            const delta = props.delta;
            if (!part) {
                break;
            }
            const partSessionId = part.sessionID;
            if (partSessionId !== state.sessionId) {
                break;
            }
            const partId = part.id;
            const messageId = part.messageID;
            const messageRole = messageId ? state.messageRoles.get(messageId) : undefined;
            const partType = part.type;
            const partTime = part.time;
            if (partId && partType) {
                state.partTypes.set(partId, partType);
            }
            if (partType === "text") {
                const partKey = resolvePartDedupeKey(part, "text");
                if (messageRole === "user") {
                    break;
                }
                if (!messageRole && !delta) {
                    break;
                }
                if (delta) {
                    if (partKey) {
                        state.streamedPartKeys.add(partKey);
                    }
                    events.push({
                        type: "timeline",
                        provider: "opencode",
                        item: { type: "assistant_message", text: delta },
                    });
                }
                else if (partTime?.end) {
                    if (partKey && state.streamedPartKeys.delete(partKey)) {
                        break;
                    }
                    const text = part.text;
                    if (text) {
                        events.push({
                            type: "timeline",
                            provider: "opencode",
                            item: { type: "assistant_message", text },
                        });
                    }
                }
            }
            else if (partType === "reasoning") {
                const partKey = resolvePartDedupeKey(part, "reasoning");
                if (delta) {
                    if (partKey) {
                        state.streamedPartKeys.add(partKey);
                    }
                    events.push({
                        type: "timeline",
                        provider: "opencode",
                        item: { type: "reasoning", text: delta },
                    });
                }
                else if (partTime?.end) {
                    if (partKey && state.streamedPartKeys.delete(partKey)) {
                        break;
                    }
                    const text = part.text;
                    if (text) {
                        events.push({
                            type: "timeline",
                            provider: "opencode",
                            item: { type: "reasoning", text },
                        });
                    }
                }
            }
            else if (partType === "tool") {
                const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
                if (parsedToolPart.success && parsedToolPart.data) {
                    events.push({
                        type: "timeline",
                        provider: "opencode",
                        item: parsedToolPart.data,
                    });
                }
            }
            else if (partType === "step-finish") {
                const tokens = part.tokens;
                const cost = part.cost;
                if (tokens) {
                    state.accumulatedUsage.inputTokens =
                        (state.accumulatedUsage.inputTokens ?? 0) + (tokens.input ?? 0);
                    state.accumulatedUsage.outputTokens =
                        (state.accumulatedUsage.outputTokens ?? 0) + (tokens.output ?? 0);
                }
                if (cost !== undefined) {
                    state.accumulatedUsage.totalCostUsd = (state.accumulatedUsage.totalCostUsd ?? 0) + cost;
                }
            }
            break;
        }
        case "message.part.delta": {
            const deltaSessionId = props.sessionID;
            if (deltaSessionId !== state.sessionId) {
                break;
            }
            const deltaMessageId = props.messageID;
            const deltaMessageRole = deltaMessageId
                ? state.messageRoles.get(deltaMessageId)
                : undefined;
            const deltaField = props.field;
            const deltaText = props.delta;
            if (!deltaText || !deltaField) {
                break;
            }
            const partId = props.partID;
            const knownPartType = partId ? state.partTypes.get(partId) : undefined;
            const isReasoning = knownPartType === "reasoning" || deltaField === "reasoning";
            if (isReasoning) {
                const partKey = partId ? `reasoning:${partId}` : null;
                if (partKey) {
                    state.streamedPartKeys.add(partKey);
                }
                events.push({
                    type: "timeline",
                    provider: "opencode",
                    item: { type: "reasoning", text: deltaText },
                });
            }
            else if (deltaField === "text") {
                if (deltaMessageRole === "user") {
                    break;
                }
                const partKey = partId ? `text:${partId}` : null;
                if (partKey) {
                    state.streamedPartKeys.add(partKey);
                }
                events.push({
                    type: "timeline",
                    provider: "opencode",
                    item: { type: "assistant_message", text: deltaText },
                });
            }
            break;
        }
        case "permission.asked": {
            const sessionId = props.sessionID;
            if (sessionId !== state.sessionId) {
                break;
            }
            const requestId = props.id;
            const permission = props.permission;
            const metadata = props.metadata;
            const patterns = props.patterns;
            const permRequest = {
                id: requestId,
                provider: "opencode",
                name: permission,
                kind: "tool",
                title: permission,
                description: patterns?.join(", "),
                input: metadata,
            };
            events.push({
                type: "permission_requested",
                provider: "opencode",
                request: permRequest,
            });
            break;
        }
        case "question.asked": {
            const sessionId = props.sessionID;
            if (sessionId !== state.sessionId) {
                break;
            }
            const requestId = props.id;
            const rawQuestions = Array.isArray(props.questions) ? props.questions : [];
            if (!requestId || rawQuestions.length === 0) {
                break;
            }
            const questions = rawQuestions.flatMap((item) => {
                const questionRecord = readOpenCodeRecord(item);
                const question = readNonEmptyString(questionRecord?.question);
                const header = readNonEmptyString(questionRecord?.header);
                if (!question || !header) {
                    return [];
                }
                const options = normalizeQuestionOptions(questionRecord?.options) ?? [];
                return [
                    {
                        question,
                        header,
                        options,
                        ...(questionRecord?.multiple === true ? { multiSelect: true } : {}),
                    },
                ];
            });
            if (questions.length === 0) {
                break;
            }
            events.push({
                type: "permission_requested",
                provider: "opencode",
                request: {
                    id: requestId,
                    provider: "opencode",
                    name: "question",
                    kind: "question",
                    title: "Question",
                    input: { questions },
                    metadata: {
                        source: "opencode_question",
                        ...(readOpenCodeRecord(props.tool) ?? {}),
                    },
                },
            });
            break;
        }
        case "session.idle": {
            const sessionId = props.sessionID;
            if (sessionId === state.sessionId) {
                state.streamedPartKeys.clear();
                state.partTypes.clear();
                events.push({
                    type: "turn_completed",
                    provider: "opencode",
                    usage: undefined,
                });
            }
            break;
        }
        case "session.error": {
            const sessionId = props.sessionID;
            if (sessionId === state.sessionId) {
                state.streamedPartKeys.clear();
                state.partTypes.clear();
                events.push({
                    type: "turn_failed",
                    provider: "opencode",
                    error: normalizeTurnFailureError(props.error),
                });
            }
            break;
        }
    }
    return events;
}
class OpenCodeAgentSession {
    constructor(config, client, sessionId, logger) {
        this.provider = "opencode";
        this.capabilities = OPENCODE_CAPABILITIES;
        this.currentMode = "default";
        this.pendingPermissions = new Map();
        this.abortController = null;
        this.accumulatedUsage = {};
        this.mcpConfigured = false;
        this.mcpSetupPromise = null;
        /** Tracks the role of each message by ID to distinguish user from assistant messages */
        this.messageRoles = new Map();
        /** Tracks streamed textual part IDs to suppress final full-text echoes from OpenCode. */
        this.streamedPartKeys = new Set();
        /** Tracks assistant messages already emitted from structured payloads. */
        this.emittedStructuredMessageIds = new Set();
        /** Tracks the type of each part by ID, learned from message.part.updated events. */
        this.partTypes = new Map();
        this.availableModesCache = null;
        this.subscribers = new Set();
        this.nextTurnOrdinal = 0;
        this.activeForegroundTurnId = null;
        this.runningToolCalls = new Map();
        this.config = config;
        this.client = client;
        this.sessionId = sessionId;
        this.logger = logger;
        this.currentMode = normalizeOpenCodeModeId(config.modeId);
    }
    get id() {
        return this.sessionId;
    }
    async getRuntimeInfo() {
        return {
            provider: "opencode",
            sessionId: this.sessionId,
            model: this.config.model ?? null,
            modeId: this.currentMode,
        };
    }
    async setModel(modelId) {
        const normalizedModelId = typeof modelId === "string" && modelId.trim().length > 0 ? modelId : null;
        this.config.model = normalizedModelId ?? undefined;
    }
    async setThinkingOption(thinkingOptionId) {
        const normalizedThinkingOptionId = typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
            ? thinkingOptionId
            : null;
        this.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined;
    }
    async run(prompt, options) {
        const timeline = [];
        let finalText = "";
        let usage;
        let turnId = null;
        const bufferedEvents = [];
        let settled = false;
        let resolveCompletion;
        let rejectCompletion;
        const processEvent = (event) => {
            if (settled) {
                return;
            }
            const eventTurnId = event.turnId;
            if (turnId && eventTurnId && eventTurnId !== turnId) {
                return;
            }
            if (event.type === "timeline") {
                timeline.push(event.item);
                if (event.item.type === "assistant_message") {
                    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 completion = new Promise((resolve, reject) => {
            resolveCompletion = resolve;
            rejectCompletion = reject;
        });
        const unsubscribe = this.subscribe((event) => {
            if (!turnId) {
                bufferedEvents.push(event);
                return;
            }
            processEvent(event);
        });
        try {
            const result = await this.startTurn(prompt, options);
            turnId = result.turnId;
            for (const event of bufferedEvents) {
                processEvent(event);
            }
            if (!settled) {
                await completion;
            }
        }
        finally {
            unsubscribe();
        }
        return {
            sessionId: this.sessionId,
            finalText,
            usage,
            timeline,
        };
    }
    async interrupt() {
        const turnId = this.activeForegroundTurnId;
        const turnAbortController = this.abortController;
        turnAbortController?.abort();
        await this.client.session.abort({
            sessionID: this.sessionId,
            directory: this.config.cwd,
        });
        if (turnId) {
            this.finishForegroundTurn({ type: "turn_canceled", provider: "opencode", reason: "interrupted" }, turnId);
        }
    }
    async startTurn(prompt, options) {
        if (this.activeForegroundTurnId) {
            throw new Error("A foreground turn is already active");
        }
        this.runningToolCalls.clear();
        const turnAbortController = new AbortController();
        this.abortController = turnAbortController;
        await this.ensureMcpServersConfigured();
        const parts = this.buildPromptParts(prompt);
        const model = this.parseModel(this.config.model);
        const thinkingOptionId = this.config.thinkingOptionId;
        const effectiveVariant = thinkingOptionId ?? undefined;
        const effectiveMode = normalizeOpenCodeModeId(this.currentMode);
        const turnId = this.createTurnId();
        this.activeForegroundTurnId = turnId;
        void this.consumeEventStream(turnId, turnAbortController);
        const slashCommand = await this.resolveSlashCommandInvocation(prompt);
        if (slashCommand) {
            // command() blocks until completion, but events stream via SSE in the
            // background — fire without awaiting so the event stream isn't starved.
            void this.client.session.command({
                sessionID: this.sessionId,
                directory: this.config.cwd,
                command: slashCommand.commandName,
                arguments: slashCommand.args ?? "",
                ...(this.config.model ? { model: this.config.model } : {}),
                ...(effectiveMode ? { agent: effectiveMode } : {}),
                ...(effectiveVariant ? { variant: effectiveVariant } : {}),
            }).then((response) => {
                if (response.error) {
                    const errorMsg = normalizeTurnFailureError(response.error);
                    this.finishForegroundTurn({ type: "turn_failed", provider: "opencode", error: errorMsg }, turnId);
                }
            });
        }
        else {
            const promptResponse = await this.client.session.promptAsync({
                sessionID: this.sessionId,
                directory: this.config.cwd,
                parts,
                ...(options?.outputSchema
                    ? {
                        format: {
                            type: "json_schema",
                            schema: options.outputSchema,
                        },
                    }
                    : {}),
                ...(this.config.systemPrompt ? { system: this.config.systemPrompt } : {}),
                ...(model ? { model } : {}),
                ...(effectiveMode ? { agent: effectiveMode } : {}),
                ...(effectiveVariant ? { variant: effectiveVariant } : {}),
            });
            if (promptResponse.error) {
                const errorMsg = normalizeTurnFailureError(promptResponse.error);
                this.notifySubscribers({
                    type: "turn_failed",
                    provider: "opencode",
                    error: errorMsg,
                });
                throw new Error(errorMsg);
            }
        }
        return { turnId };
    }
    subscribe(callback) {
        this.subscribers.add(callback);
        return () => {
            this.subscribers.delete(callback);
        };
    }
    async consumeEventStream(turnId, turnAbortController) {
        const eventsResult = await this.client.event.subscribe({
            directory: this.config.cwd,
        });
        try {
            for await (const event of eventsResult.stream) {
                if (turnAbortController.signal.aborted || this.activeForegroundTurnId !== turnId) {
                    break;
                }
                const translated = this.translateEvent(event);
                for (const e of translated) {
                    if (this.activeForegroundTurnId !== turnId) {
                        return;
                    }
                    if (e.type === "timeline" && e.item.type === "tool_call") {
                        this.trackToolCall(e.item);
                    }
                    if (e.type === "turn_completed" || e.type === "turn_failed" || e.type === "turn_canceled") {
                        if (e.type === "turn_failed") {
                            this.finishForegroundTurn({
                                type: "turn_failed",
                                provider: "opencode",
                                error: normalizeTurnFailureError(e.error),
                            }, turnId);
                        }
                        else {
                            this.finishForegroundTurn(e, turnId);
                        }
                        return;
                    }
                    this.notifySubscribers(e, turnId);
                }
            }
        }
        catch (error) {
            if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
                this.finishForegroundTurn({
                    type: "turn_failed",
                    provider: "opencode",
                    error: normalizeTurnFailureError(error),
                }, turnId);
            }
        }
        finally {
            if (turnAbortController.signal.aborted) {
                this.finishForegroundTurn({
                    type: "turn_canceled",
                    provider: "opencode",
                    reason: "interrupted",
                }, turnId);
            }
            if (this.abortController === turnAbortController && this.activeForegroundTurnId !== turnId) {
                this.abortController = null;
            }
        }
    }
    finishForegroundTurn(event, turnId) {
        if (this.activeForegroundTurnId !== turnId) {
            return;
        }
        if (event.type === "turn_canceled" || event.type === "turn_failed") {
            this.synthesizeInterruptedToolCalls(turnId);
        }
        else {
            this.runningToolCalls.clear();
        }
        this.activeForegroundTurnId = null;
        this.notifySubscribers(event, turnId);
    }
    trackToolCall(item) {
        if (item.status === "running") {
            this.runningToolCalls.set(item.callId, item);
            return;
        }
        this.runningToolCalls.delete(item.callId);
    }
    synthesizeInterruptedToolCalls(turnId) {
        for (const item of this.runningToolCalls.values()) {
            this.notifySubscribers({
                type: "timeline",
                provider: "opencode",
                item: {
                    ...item,
                    status: "failed",
                    error: { message: "Tool execution aborted" },
                },
            }, turnId);
        }
        this.runningToolCalls.clear();
    }
    notifySubscribers(event, turnIdOverride) {
        const turnId = turnIdOverride ?? this.activeForegroundTurnId;
        const tagged = turnId ? { ...event, turnId } : event;
        for (const callback of this.subscribers) {
            try {
                callback(tagged);
            }
            catch {
                // Subscriber callback error isolation
            }
        }
    }
    createTurnId() {
        return `opencode-turn-${this.nextTurnOrdinal++}`;
    }
    async *streamHistory() {
        const response = await this.client.session.messages({
            sessionID: this.sessionId,
            directory: this.config.cwd,
        });
        if (response.error || !response.data) {
            return;
        }
        const messages = response.data;
        for (const message of messages) {
            const { info, parts } = message;
            const role = info.role;
            if (role === "user") {
                // Extract user message text from parts
                const textParts = parts.filter((p) => p.type === "text");
                const text = textParts.map((p) => p.text ?? "").join("");
                if (text) {
                    yield {
                        type: "timeline",
                        provider: "opencode",
                        item: { type: "user_message", text },
                    };
                }
            }
            else if (role === "assistant") {
                let emittedAssistantText = false;
                // Process each part
                for (const part of parts) {
                    const partType = part.type;
                    if (partType === "text") {
                        const text = part.text;
                        if (text) {
                            emittedAssistantText = true;
                            yield {
                                type: "timeline",
                                provider: "opencode",
                                item: { type: "assistant_message", text },
                            };
                        }
                    }
                    else if (partType === "reasoning") {
                        const text = part.text;
                        if (text) {
                            yield {
                                type: "timeline",
                                provider: "opencode",
                                item: { type: "reasoning", text },
                            };
                        }
                    }
                    else if (partType === "tool") {
                        const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
                        if (parsedToolPart.success) {
                            if (parsedToolPart.data) {
                                yield {
                                    type: "timeline",
                                    provider: "opencode",
                                    item: parsedToolPart.data,
                                };
                            }
                        }
                    }
                }
                if (!emittedAssistantText) {
                    const text = stringifyStructuredAssistantMessage(info.structured);
                    if (text) {
                        yield {
                            type: "timeline",
                            provider: "opencode",
                            item: { type: "assistant_message", text },
                        };
                    }
                }
            }
        }
    }
    async getAvailableModes() {
        if (this.availableModesCache) {
            return this.availableModesCache;
        }
        const response = await this.client.app.agents({
            directory: this.config.cwd,
        });
        const discoveredModes = response.error || !response.data
            ? []
            : response.data
                .filter((agent) => agent.mode === "primary" && agent.hidden !== true)
                .map((agent) => ({
                id: agent.name,
                label: agent.name.charAt(0).toUpperCase() + agent.name.slice(1),
                description: typeof agent.description === "string" && agent.description.trim().length > 0
                    ? agent.description.trim()
                    : DEFAULT_MODES.find((mode) => mode.id === agent.name)?.description,
            }));
        this.availableModesCache =
            discoveredModes.length > 0 ? sortOpenCodeModes(discoveredModes) : DEFAULT_MODES;
        return this.availableModesCache;
    }
    async getCurrentMode() {
        return this.currentMode;
    }
    async listCommands() {
        const result = await this.client.command.list({
            directory: this.config.cwd,
        });
        if (result.error || !result.data) {
            return [];
        }
        return result.data.map((cmd) => ({
            name: cmd.name,
            description: cmd.description ?? "",
            argumentHint: cmd.hints?.length ? cmd.hints.join(" ") : "",
        }));
    }
    async setMode(modeId) {
        this.currentMode = normalizeOpenCodeModeId(modeId);
    }
    getPendingPermissions() {
        return Array.from(this.pendingPermissions.values());
    }
    async respondToPermission(requestId, response) {
        const pending = this.pendingPermissions.get(requestId);
        if (!pending) {
            throw new Error(`No pending permission request with id '${requestId}'`);
        }
        if (pending.kind === "question") {
            if (response.behavior === "deny") {
                await this.client.question.reject({
                    requestID: requestId,
                    directory: this.config.cwd,
                });
            }
            else {
                const answersRecord = readOpenCodeRecord(response.updatedInput?.answers);
                const questions = Array.isArray(pending.input?.questions) ? pending.input.questions : [];
                const answers = questions.map((item) => {
                    const header = readNonEmptyString(readOpenCodeRecord(item)?.header);
                    const rawAnswer = header ? readNonEmptyString(answersRecord?.[header]) : null;
                    if (!rawAnswer) {
                        return [];
                    }
                    return rawAnswer
                        .split(",")
                        .map((entry) => entry.trim())
                        .filter((entry) => entry.length > 0);
                });
                await this.client.question.reply({
                    requestID: requestId,
                    directory: this.config.cwd,
                    answers,
                });
            }
            this.pendingPermissions.delete(requestId);
            return;
        }
        const reply = response.behavior === "allow" ? "once" : "reject";
        await this.client.permission.reply({
            requestID: requestId,
            directory: this.config.cwd,
            reply,
            message: response.behavior === "deny" ? response.message : undefined,
        });
        this.pendingPermissions.delete(requestId);
    }
    describePersistence() {
        return {
            provider: "opencode",
            sessionId: this.sessionId,
            nativeHandle: this.sessionId,
            metadata: {
                cwd: this.config.cwd,
            },
        };
    }
    async close() {
        this.abortController?.abort();
        this.subscribers.clear();
        this.activeForegroundTurnId = null;
    }
    buildPromptParts(prompt) {
        if (typeof prompt === "string") {
            return [{ type: "text", text: prompt }];
        }
        return prompt
            .filter((p) => p.type === "text")
            .map((p) => ({ type: "text", text: p.text }));
    }
    parseSlashCommandInput(text) {
        const trimmed = text.trim();
        if (!trimmed.startsWith("/") || trimmed.length <= 1) {
            return null;
        }
        const withoutPrefix = trimmed.slice(1);
        const firstWhitespaceIdx = withoutPrefix.search(/\s/);
        const commandName = firstWhitespaceIdx === -1 ? withoutPrefix : withoutPrefix.slice(0, firstWhitespaceIdx);
        if (!commandName || commandName.includes("/")) {
            return null;
        }
        const rawArgs = firstWhitespaceIdx === -1 ? "" : withoutPrefix.slice(firstWhitespaceIdx + 1).trim();
        return rawArgs.length > 0 ? { commandName, args: rawArgs } : { commandName };
    }
    async resolveSlashCommandInvocation(prompt) {
        if (typeof prompt !== "string") {
            return null;
        }
        const parsed = this.parseSlashCommandInput(prompt);
        if (!parsed) {
            return null;
        }
        try {
            const commands = await this.listCommands();
            return commands.some((command) => command.name === parsed.commandName) ? parsed : null;
        }
        catch (error) {
            this.logger.warn({ err: error, commandName: parsed.commandName }, "Failed to resolve slash command; falling back to plain prompt input");
            return null;
        }
    }
    parseModel(model) {
        if (!model) {
            return undefined;
        }
        const parts = model.split("/");
        if (parts.length >= 2) {
            return { providerID: parts[0], modelID: parts.slice(1).join("/") };
        }
        return { providerID: "opencode", modelID: model };
    }
    async ensureMcpServersConfigured() {
        if (this.mcpConfigured) {
            return;
        }
        const mcpServers = this.config.mcpServers;
        if (!mcpServers || Object.keys(mcpServers).length === 0) {
            this.mcpConfigured = true;
            return;
        }
        if (!this.mcpSetupPromise) {
            this.mcpSetupPromise = this.configureMcpServers(mcpServers);
        }
        try {
            await this.mcpSetupPromise;
            this.mcpConfigured = true;
        }
        catch (error) {
            this.mcpSetupPromise = null;
            throw error;
        }
    }
    async configureMcpServers(mcpServers) {
        for (const [name, serverConfig] of Object.entries(mcpServers)) {
            const mappedConfig = toOpenCodeMcpConfig(serverConfig);
            await this.registerMcpServer(name, mappedConfig);
        }
    }
    async registerMcpServer(name, config) {
        await this.runMcpOperation("add", name, () => this.client.mcp.add({
            directory: this.config.cwd,
            name,
            config,
        }));
        await this.runMcpOperation("connect", name, () => this.client.mcp.connect({
            directory: this.config.cwd,
            name,
        }));
    }
    async runMcpOperation(operation, name, run) {
        const response = await run();
        const error = response.error;
        if (!error) {
            return;
        }
        if (isAlreadyPresentMcpError(error)) {
            return;
        }
        throw new Error(`Failed to ${operation} OpenCode MCP server '${name}': ${stringifyUnknownError(error)}`);
    }
    translateEvent(event) {
        const translated = translateOpenCodeEvent(event, {
            sessionId: this.sessionId,
            messageRoles: this.messageRoles,
            accumulatedUsage: this.accumulatedUsage,
            streamedPartKeys: this.streamedPartKeys,
            emittedStructuredMessageIds: this.emittedStructuredMessageIds,
            partTypes: this.partTypes,
        });
        for (const translatedEvent of translated) {
            if (translatedEvent.type === "permission_requested") {
                this.pendingPermissions.set(translatedEvent.request.id, translatedEvent.request);
            }
            if (translatedEvent.type === "turn_completed") {
                translatedEvent.usage = this.extractAndResetUsage();
            }
        }
        return translated;
    }
    extractAndResetUsage() {
        const usage = this.accumulatedUsage;
        this.accumulatedUsage = {};
        if (!usage.inputTokens && !usage.outputTokens && !usage.totalCostUsd) {
            return undefined;
        }
        return usage;
    }
}
//# sourceMappingURL=opencode-agent.js.map