import { readdir, readFile } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
import { z } from "zod";
import { getAgentStreamEventTurnId, } from "../agent-sdk-types.js";
import { createProviderEnvSpec } from "../provider-launch-config.js";
import { withTimeout } from "../../../utils/promise-timeout.js";
import { execCommand } from "../../../utils/spawn.js";
import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
import { OpenCodeServerManager } from "./opencode/server-manager.js";
import { formatDiagnosticStatus, formatProviderDiagnostic, formatProviderDiagnosticError, resolveBinaryVersion, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
import { runProviderTurn } from "./provider-runner.js";
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
import { createSdkOpenCodeClient, } from "./opencode/runtime.js";
import { normalizeProviderReplayTimestamp } from "../provider-history-timestamps.js";
const OPENCODE_CAPABILITIES = {
    supportsStreaming: true,
    supportsSessionPersistence: true,
    supportsDynamicModes: true,
    supportsMcpServers: true,
    supportsReasoningStream: true,
    supportsToolInvocations: true,
};
const OPENCODE_BUILD_MODE_ID = "build";
const OPENCODE_FULL_ACCESS_MODE_ID = "full-access";
const OPENCODE_STORAGE_SESSION_LIMIT = 200;
const OPENCODE_PENDING_ABORT_START_TIMEOUT_MS = 10000;
const DEFAULT_MODES = [
    {
        id: OPENCODE_BUILD_MODE_ID,
        label: "Build",
        description: "Allows edits and tool execution for implementation work",
    },
    {
        id: "plan",
        label: "Plan",
        description: "Read-only planning mode that avoids file edits",
    },
    {
        id: OPENCODE_FULL_ACCESS_MODE_ID,
        label: "Full Access",
        description: "Automatically approves all tool permission prompts for the session",
    },
];
const OpenCodeStoredSessionSchema = z
    .object({
    id: z.string().min(1),
    directory: z.string().min(1),
    title: z.string().nullable().optional(),
    time: z
        .object({
        created: z.number().optional(),
        updated: z.number().optional(),
    })
        .optional(),
})
    .passthrough();
const OpenCodeStoredMessageSchema = z
    .object({
    id: z.string().min(1),
    sessionID: z.string().min(1),
    role: z.string().optional(),
    time: z
        .object({
        created: z.number().optional(),
        completed: z.number().optional(),
    })
        .optional(),
})
    .passthrough();
const OpenCodeStoredPartSchema = z
    .object({
    type: z.string().optional(),
    text: z.string().optional(),
    time: z
        .object({
        start: z.number().optional(),
        end: z.number().optional(),
    })
        .optional(),
})
    .passthrough();
const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"];
const OPENCODE_PROVIDER_LIST_TIMEOUT_MS = 30000;
const OPENCODE_HANDLED_BUILTIN_SLASH_COMMANDS = [
    { name: "compact", description: "Compact the current session", argumentHint: "" },
    { name: "summarize", description: "Compact the current session", argumentHint: "" },
];
const OPENCODE_HEADERS_TIMEOUT_TOKENS = [
    "headers timeout",
    "headers timeout error",
    "headers_timeout",
    "und_err_headers_timeout",
];
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 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 toTerminalTurnEvent(event) {
    if (event.type === "turn_failed") {
        return {
            type: "turn_failed",
            provider: "opencode",
            error: toDiagnosticErrorMessage(event.error),
        };
    }
    if (event.type === "turn_completed" || event.type === "turn_canceled") {
        return event;
    }
    return null;
}
function isOpenCodeNotFoundError(error) {
    return (typeof error === "object" &&
        error !== null &&
        "name" in error &&
        error.name === "NotFoundError");
}
async function reconcileOpenCodeSessionClose(params) {
    const { client, sessionId, directory, logger } = params;
    try {
        const response = await client.session.abort({
            sessionID: sessionId,
            directory,
        });
        if (response.error && !isOpenCodeNotFoundError(response.error)) {
            logger.warn({
                sessionId,
                error: toDiagnosticErrorMessage(response.error),
            }, "Failed to abort OpenCode session during close");
        }
    }
    catch (error) {
        logger.warn({
            sessionId,
            error: toDiagnosticErrorMessage(error),
        }, "Failed to abort OpenCode session during close");
    }
    try {
        const response = await client.session.update({
            sessionID: sessionId,
            directory,
            time: { archived: Date.now() },
        });
        if (response.error && !isOpenCodeNotFoundError(response.error)) {
            logger.warn({
                sessionId,
                error: toDiagnosticErrorMessage(response.error),
            }, "Failed to archive OpenCode session during close");
        }
    }
    catch (error) {
        logger.warn({
            sessionId,
            error: toDiagnosticErrorMessage(error),
        }, "Failed to archive OpenCode session during close");
    }
}
function isOpenCodeHeadersTimeoutFailure(error) {
    const diagnostics = new Set();
    const queue = [error];
    while (queue.length > 0) {
        const current = queue.shift();
        if (!current) {
            continue;
        }
        const normalized = toDiagnosticErrorMessage(current).trim().toLowerCase();
        if (normalized) {
            diagnostics.add(normalized);
        }
        if (typeof current === "object") {
            const record = current;
            for (const value of [record.message, record.code, record.name]) {
                if (typeof value !== "string") {
                    continue;
                }
                const diagnostic = value.trim().toLowerCase();
                if (diagnostic) {
                    diagnostics.add(diagnostic);
                }
            }
            if (record.cause) {
                queue.push(record.cause);
            }
        }
    }
    return [...diagnostics].some((diagnostic) => OPENCODE_HEADERS_TIMEOUT_TOKENS.some((token) => diagnostic.includes(token)));
}
function isAlreadyPresentMcpError(error) {
    const normalized = toDiagnosticErrorMessage(error).toLowerCase();
    return MCP_ALREADY_PRESENT_ERROR_TOKENS.some((token) => normalized.includes(token));
}
function resolvePartDedupeKey(part, partType) {
    if (part.id.trim().length > 0) {
        return `${partType}:${part.id}`;
    }
    if (part.messageID.trim().length > 0) {
        return `${partType}:message:${part.messageID}`;
    }
    return null;
}
function normalizeOpenCodeModeId(modeId) {
    const trimmed = typeof modeId === "string" ? modeId.trim() : "";
    if (!trimmed || trimmed === "default") {
        return OPENCODE_BUILD_MODE_ID;
    }
    return trimmed;
}
function resolveOpenCodeRuntimeAgentId(modeId) {
    const normalizedModeId = normalizeOpenCodeModeId(modeId);
    return normalizedModeId === OPENCODE_FULL_ACCESS_MODE_ID
        ? OPENCODE_BUILD_MODE_ID
        : normalizedModeId;
}
function isSelectableOpenCodeAgent(agent) {
    return (agent.mode === "primary" || agent.mode === "all") && agent.hidden !== true;
}
function mergeOpenCodeModes(discoveredModes) {
    const modesById = new Map(DEFAULT_MODES.map((mode) => [mode.id, mode]));
    for (const mode of discoveredModes) {
        modesById.set(mode.id, mode);
    }
    return sortOpenCodeModes(Array.from(modesById.values()));
}
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);
    });
}
function readPositiveFiniteNumber(value) {
    return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
}
function buildOpenCodeModelLookupKey(providerId, modelId) {
    return `${providerId}/${modelId}`;
}
function parseOpenCodeModelLookupKey(modelId) {
    if (typeof modelId !== "string" || modelId.trim().length === 0) {
        return undefined;
    }
    const slashIndex = modelId.indexOf("/");
    if (slashIndex <= 0 || slashIndex === modelId.length - 1) {
        return undefined;
    }
    const providerId = modelId.slice(0, slashIndex).trim();
    const providerModelId = modelId.slice(slashIndex + 1).trim();
    if (!providerId || !providerModelId) {
        return undefined;
    }
    return buildOpenCodeModelLookupKey(providerId, providerModelId);
}
function extractOpenCodeModelContextWindow(model) {
    if (!model || typeof model !== "object") {
        return undefined;
    }
    const limit = model.limit;
    return readPositiveFiniteNumber(limit?.context);
}
function buildOpenCodeModelDefinition(provider, modelId, model) {
    const rawVariants = model.variants ? Object.keys(model.variants) : [];
    const thinkingOptions = rawVariants.map((id, index) => ({
        id,
        label: id,
        isDefault: index === 0,
    }));
    return {
        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,
            contextWindowMaxTokens: extractOpenCodeModelContextWindow(model),
            ...(model.limit ? { limit: model.limit } : {}),
        },
    };
}
function resolveOpenCodeSelectedModelContextWindow(providers, modelId) {
    if (!providers) {
        return undefined;
    }
    const modelLookupKey = parseOpenCodeModelLookupKey(modelId);
    if (!modelLookupKey) {
        return undefined;
    }
    const lookup = buildOpenCodeModelContextWindowLookup(providers);
    return lookup.get(modelLookupKey);
}
function buildOpenCodeModelContextWindowLookup(providers) {
    const lookup = new Map();
    if (!providers) {
        return lookup;
    }
    const connectedProviderIds = new Set(providers.connected ?? []);
    for (const provider of providers.all ?? []) {
        // Providers with source "api" are managed by the OpenCode console/subscription and are
        // usable even though they don't appear in `connected` (which only lists env/config providers).
        if (!connectedProviderIds.has(provider.id) && provider.source !== "api") {
            continue;
        }
        for (const [modelId, modelDefinition] of Object.entries(provider.models ?? {})) {
            const contextWindow = extractOpenCodeModelContextWindow(modelDefinition);
            if (contextWindow === undefined) {
                continue;
            }
            lookup.set(buildOpenCodeModelLookupKey(provider.id, modelId), contextWindow);
        }
    }
    return lookup;
}
function resolveOpenCodeModelLookupKeyFromAssistantMessage(info) {
    const providerId = info.providerID;
    const modelId = info.modelID;
    if (!providerId || !modelId) {
        return undefined;
    }
    return buildOpenCodeModelLookupKey(providerId, modelId);
}
function mergeOpenCodeStepFinishUsage(usage, part) {
    const inputTokens = readPositiveFiniteNumber(part.tokens?.input);
    const outputTokens = readPositiveFiniteNumber(part.tokens?.output);
    const reasoningTokens = readPositiveFiniteNumber(part.tokens?.reasoning);
    const cacheReadTokens = readPositiveFiniteNumber(part.tokens?.cache?.read);
    const cacheWriteTokens = readPositiveFiniteNumber(part.tokens?.cache?.write);
    const totalTokens = (inputTokens ?? 0) +
        (outputTokens ?? 0) +
        (reasoningTokens ?? 0) +
        (cacheReadTokens ?? 0) +
        (cacheWriteTokens ?? 0);
    const cost = readPositiveFiniteNumber(part.cost);
    if (inputTokens !== undefined) {
        usage.inputTokens = inputTokens;
    }
    if (cacheReadTokens !== undefined) {
        usage.cachedInputTokens = cacheReadTokens;
    }
    if (outputTokens !== undefined) {
        usage.outputTokens = outputTokens;
    }
    if (totalTokens > 0) {
        usage.contextWindowUsedTokens = totalTokens;
    }
    if (cost !== undefined) {
        usage.totalCostUsd = (usage.totalCostUsd ?? 0) + cost;
    }
}
function hasNormalizedOpenCodeUsage(usage) {
    return [
        usage.inputTokens,
        usage.cachedInputTokens,
        usage.outputTokens,
        usage.totalCostUsd,
        usage.contextWindowMaxTokens,
        usage.contextWindowUsedTokens,
    ].some((value) => typeof value === "number" && Number.isFinite(value));
}
function getOpenCodeAttachmentExtension(mimeType) {
    switch (mimeType) {
        case "image/png":
            return "png";
        case "image/jpeg":
            return "jpg";
        case "image/webp":
            return "webp";
        case "image/gif":
            return "gif";
        case "image/svg+xml":
            return "svg";
        default:
            return "bin";
    }
}
function toOpenCodeDataUrl(mimeType, data) {
    const match = data.match(/^data:([^;,]+);base64,(.+)$/);
    if (match) {
        return {
            mimeType: match[1] ?? mimeType,
            url: data,
        };
    }
    return {
        mimeType,
        url: `data:${mimeType};base64,${data}`,
    };
}
function buildOpenCodePromptParts(prompt) {
    if (typeof prompt === "string") {
        return [{ type: "text", text: prompt }];
    }
    let attachmentOrdinal = 0;
    const output = [];
    for (const part of prompt) {
        if (part.type === "text") {
            output.push({ type: "text", text: part.text });
            continue;
        }
        if (part.type === "image") {
            attachmentOrdinal += 1;
            const normalized = toOpenCodeDataUrl(part.mimeType, part.data);
            output.push({
                type: "file",
                mime: normalized.mimeType,
                filename: `attachment-${attachmentOrdinal}.${getOpenCodeAttachmentExtension(normalized.mimeType)}`,
                url: normalized.url,
            });
            continue;
        }
        output.push({ type: "text", text: renderPromptAttachmentAsText(part) });
    }
    return output;
}
function resolveOpenCodeStorageRoot() {
    const xdgDataHome = process.env.XDG_DATA_HOME;
    const dataHome = typeof xdgDataHome === "string" && xdgDataHome.trim().length > 0
        ? xdgDataHome
        : path.join(homedir(), ".local", "share");
    return path.join(dataHome, "opencode", "storage");
}
async function collectOpenCodePersistedAgentsFromStorage(storageRoot, options) {
    const sessions = await readOpenCodeStoredSessions(path.join(storageRoot, "session"));
    const limit = options?.limit ?? OPENCODE_STORAGE_SESSION_LIMIT;
    const candidates = sessions
        .filter((session) => !options?.cwd || session.directory === options.cwd)
        .sort((left, right) => getOpenCodeSessionTimestamp(right) - getOpenCodeSessionTimestamp(left))
        .slice(0, limit);
    return await Promise.all(candidates.map((session) => buildOpenCodePersistedAgentDescriptor(storageRoot, session)));
}
async function readOpenCodeStoredSessions(sessionRoot) {
    const files = await findJsonFiles(sessionRoot);
    const sessions = [];
    for (const file of files) {
        const parsed = await readJsonFile(file, OpenCodeStoredSessionSchema);
        if (parsed) {
            sessions.push(parsed);
        }
    }
    return sessions;
}
async function buildOpenCodePersistedAgentDescriptor(storageRoot, session) {
    const timeline = await readOpenCodeSessionTimeline(storageRoot, session.id);
    return {
        provider: "opencode",
        sessionId: session.id,
        cwd: session.directory,
        title: normalizeOpenCodeSessionTitle(session.title),
        lastActivityAt: new Date(getOpenCodeSessionTimestamp(session)),
        persistence: {
            provider: "opencode",
            sessionId: session.id,
            nativeHandle: session.id,
            metadata: {
                provider: "opencode",
                cwd: session.directory,
                title: normalizeOpenCodeSessionTitle(session.title),
            },
        },
        timeline,
    };
}
async function readOpenCodeSessionTimeline(storageRoot, sessionId) {
    const messageRoot = path.join(storageRoot, "message", sessionId);
    const messageFiles = await findJsonFiles(messageRoot);
    const messages = [];
    for (const file of messageFiles) {
        const parsed = await readJsonFile(file, OpenCodeStoredMessageSchema);
        if (parsed?.sessionID === sessionId) {
            messages.push(parsed);
        }
    }
    const timeline = [];
    for (const message of messages.sort((left, right) => getOpenCodeMessageTimestamp(left) - getOpenCodeMessageTimestamp(right))) {
        const text = await readOpenCodeMessageText(storageRoot, message.id);
        if (!text) {
            continue;
        }
        if (message.role === "user") {
            timeline.push({ type: "user_message", text, messageId: message.id });
        }
        else if (message.role === "assistant") {
            timeline.push({ type: "assistant_message", text });
        }
    }
    return timeline;
}
async function readOpenCodeMessageText(storageRoot, messageId) {
    const parts = await readOpenCodeStoredParts(storageRoot, messageId);
    return readOpenCodeTextFromParts(parts);
}
async function readOpenCodeStoredParts(storageRoot, messageId) {
    const partRoot = path.join(storageRoot, "part", messageId);
    const partFiles = await findJsonFiles(partRoot);
    const parts = [];
    for (const file of partFiles) {
        const parsed = await readJsonFile(file, OpenCodeStoredPartSchema);
        if (parsed) {
            parts.push(parsed);
        }
    }
    return parts.sort((left, right) => getOpenCodePartTimestamp(left) - getOpenCodePartTimestamp(right));
}
function readOpenCodeTextFromParts(parts) {
    return parts
        .filter((part) => part.type === "text" && typeof part.text === "string")
        .map((part) => part.text?.trim() ?? "")
        .filter(Boolean)
        .join("\n\n");
}
async function findJsonFiles(root) {
    let entries;
    try {
        entries = await readdir(root, { withFileTypes: true });
    }
    catch {
        return [];
    }
    const files = await Promise.all(entries.map(async (entry) => {
        const entryPath = path.join(root, entry.name);
        if (entry.isDirectory()) {
            return findJsonFiles(entryPath);
        }
        return entry.isFile() && entry.name.endsWith(".json") ? [entryPath] : [];
    }));
    return files.flat();
}
async function readJsonFile(file, schema) {
    try {
        return schema.parse(JSON.parse(await readFile(file, "utf8")));
    }
    catch {
        return null;
    }
}
function normalizeOpenCodeSessionTitle(title) {
    const normalized = title?.trim();
    return normalized ? normalized : null;
}
function getOpenCodeSessionTimestamp(session) {
    return session.time?.updated ?? session.time?.created ?? 0;
}
function getOpenCodeMessageTimestamp(message) {
    return message.time?.created ?? message.time?.completed ?? 0;
}
function getOpenCodePartTimestamp(part) {
    return part.time?.start ?? part.time?.end ?? 0;
}
function resolveOpenCodeReplayTimestamp(params) {
    const timedPart = params.part;
    const partTimestamp = timedPart?.time?.start ??
        timedPart?.time?.end ??
        params.message.time?.created ??
        params.message.time?.completed;
    return normalizeProviderReplayTimestamp(partTimestamp);
}
function buildOpenCodeReplayTimelineEvent(params) {
    const timestamp = resolveOpenCodeReplayTimestamp({
        message: params.message,
        part: params.part,
    });
    return {
        type: "timeline",
        provider: "opencode",
        item: params.item,
        ...(timestamp ? { timestamp } : {}),
    };
}
function buildOpenCodeReplayPartTimelineEvent(params) {
    const { part, message } = params;
    if (part.type === "text" && part.text) {
        return buildOpenCodeReplayTimelineEvent({
            item: { type: "assistant_message", text: part.text },
            message,
            part,
        });
    }
    if (part.type === "reasoning" && part.text) {
        return buildOpenCodeReplayTimelineEvent({
            item: { type: "reasoning", text: part.text },
            message,
            part,
        });
    }
    if (part.type !== "tool") {
        return null;
    }
    const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
    if (!parsedToolPart.success || !parsedToolPart.data) {
        return null;
    }
    return buildOpenCodeReplayTimelineEvent({
        item: parsedToolPart.data,
        message,
        part,
    });
}
export const __openCodeInternals = {
    buildOpenCodePromptParts,
    buildOpenCodeModelContextWindowLookup,
    buildOpenCodeModelDefinition,
    buildOpenCodeModelLookupKey,
    extractOpenCodeModelContextWindow,
    hasNormalizedOpenCodeUsage,
    mergeOpenCodeStepFinishUsage,
    parseOpenCodeModelLookupKey,
    reconcileOpenCodeSessionClose,
    resolveOpenCodeModelLookupKeyFromAssistantMessage,
    resolveOpenCodeSelectedModelContextWindow,
    isSelectableOpenCodeAgent,
    get OpenCodeAgentSession() {
        return OpenCodeAgentSession;
    },
};
class ProductionOpenCodeRuntime {
    constructor(serverManager) {
        this.serverManager = serverManager;
    }
    async acquireServer(options) {
        return this.serverManager.acquire(options);
    }
    async ensureServerRunning() {
        return this.serverManager.ensureRunning();
    }
    createClient(options) {
        return createSdkOpenCodeClient(options);
    }
    async shutdown() {
        await this.serverManager.shutdown();
    }
}
export class OpenCodeAgentClient {
    constructor(logger, runtimeSettings, storageRoot, deps = {}) {
        this.provider = "opencode";
        this.capabilities = OPENCODE_CAPABILITIES;
        this.modelContextWindows = new Map();
        this.logger = logger.child({ module: "agent", provider: "opencode" });
        this.runtimeSettings = runtimeSettings;
        this.storageRoot = storageRoot ?? resolveOpenCodeStorageRoot();
        this.runtime =
            deps.runtime ??
                new ProductionOpenCodeRuntime(OpenCodeServerManager.getInstance(this.logger, runtimeSettings));
    }
    async createSession(config, launchContext, options) {
        const openCodeConfig = this.assertConfig(config);
        const acquisition = await this.runtime.acquireServer({ force: false });
        const { url } = acquisition.server;
        const client = this.runtime.createClient({
            baseUrl: url,
            directory: openCodeConfig.cwd,
        });
        try {
            const response = await withTimeout(client.session.create({ directory: openCodeConfig.cwd }), 10000, "OpenCode session.create timed out after 10s");
            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");
            }
            await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
            return new OpenCodeAgentSession(openCodeConfig, client, session.id, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, options?.persistSession, launchContext?.agentId);
        }
        catch (error) {
            acquisition.release();
            throw error;
        }
    }
    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 acquisition = await this.runtime.acquireServer({ force: false });
        const { url } = acquisition.server;
        const client = this.runtime.createClient({
            baseUrl: url,
            directory: openCodeConfig.cwd,
        });
        try {
            await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
            return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, undefined, launchContext?.agentId);
        }
        catch (error) {
            acquisition.release();
            throw error;
        }
    }
    async listModels(options) {
        const acquisition = await this.runtime.acquireServer({ force: options.force });
        const { url } = acquisition.server;
        const client = this.runtime.createClient({
            baseUrl: url,
            directory: options.cwd,
        });
        try {
            // Background model discovery can be legitimately slow while OpenCode refreshes
            // provider state, so allow longer than turn execution paths.
            const response = await withTimeout(client.provider.list({ directory: options.cwd }), OPENCODE_PROVIDER_LIST_TIMEOUT_MS, `OpenCode provider.list timed out after ${OPENCODE_PROVIDER_LIST_TIMEOUT_MS / 1000}s - server may not be authenticated or connected to any providers`);
            if (response.error) {
                throw new Error(`Failed to fetch OpenCode providers: ${JSON.stringify(response.error)}`);
            }
            const providers = response.data;
            if (!providers) {
                return [];
            }
            const connectedProviderIds = new Set(providers.connected);
            // Providers with source "api" are managed by the OpenCode console/subscription (e.g. Pi
            // coding agent). They do not appear in `connected` (which only lists env/config providers)
            // but are fully usable — OpenCode authenticates them internally via the console session.
            const isAccessible = (provider) => connectedProviderIds.has(provider.id) || provider.source === "api";
            // Fail fast if no providers are accessible at all
            if (!providers.all.some(isAccessible)) {
                throw new Error("OpenCode has no connected providers. Please authenticate with at least one provider " +
                    "(e.g., openai, anthropic), set appropriate environment variables (e.g., OPENAI_API_KEY), " +
                    "or log in to OpenCode Go via the console.");
            }
            const models = [];
            this.modelContextWindows.clear();
            for (const provider of providers.all) {
                if (!isAccessible(provider)) {
                    continue;
                }
                for (const [modelId, model] of Object.entries(provider.models)) {
                    const definition = buildOpenCodeModelDefinition(provider, modelId, model);
                    const contextWindowMaxTokens = extractOpenCodeModelContextWindow(model);
                    if (contextWindowMaxTokens !== undefined) {
                        this.modelContextWindows.set(buildOpenCodeModelLookupKey(provider.id, modelId), contextWindowMaxTokens);
                    }
                    models.push(definition);
                }
            }
            return models;
        }
        finally {
            acquisition.release();
        }
    }
    async listModes(options) {
        const acquisition = await this.runtime.acquireServer({ force: options.force });
        const { url } = acquisition.server;
        const directory = options.cwd;
        const client = this.runtime.createClient({ baseUrl: url, directory });
        try {
            const response = await withTimeout(client.app.agents({ directory }), 10000, "OpenCode app.agents timed out after 10s");
            if (response.error || !response.data) {
                return DEFAULT_MODES;
            }
            const discovered = response.data.filter(isSelectableOpenCodeAgent).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 mergeOpenCodeModes(discovered);
        }
        finally {
            acquisition.release();
        }
    }
    async listPersistedAgents(options) {
        return collectOpenCodePersistedAgentsFromStorage(this.storageRoot, options);
    }
    async isAvailable() {
        const command = this.runtimeSettings?.command;
        if (command?.mode === "replace") {
            return await isCommandAvailable(command.argv[0]);
        }
        return await isCommandAvailable("opencode");
    }
    async getDiagnostic() {
        try {
            const available = await this.isAvailable();
            const resolvedBinary = await findExecutable("opencode");
            let serverStatus = "Not running";
            let modelsValue = "Not checked";
            let status = formatDiagnosticStatus(available);
            try {
                const { url } = await this.runtime.ensureServerRunning();
                serverStatus = `Running (${url})`;
            }
            catch (error) {
                serverStatus = `Unavailable (${toDiagnosticErrorMessage(error)})`;
            }
            let authValue = "Not checked";
            if (resolvedBinary) {
                try {
                    const { stdout, stderr } = await execCommand(resolvedBinary, ["auth", "list"], {
                        ...createProviderEnvSpec(),
                        timeout: 5000,
                    });
                    const text = (stdout.trim() || stderr.trim()).trim();
                    authValue = text ? `\n    ${text.replace(/\n/g, "\n    ")}` : "(empty)";
                }
                catch (error) {
                    authValue = `Error - ${toDiagnosticErrorMessage(error)}`;
                }
            }
            if (available) {
                try {
                    const models = await this.listModels({ cwd: homedir(), force: false });
                    modelsValue = String(models.length);
                }
                catch (error) {
                    modelsValue = `Error - ${toDiagnosticErrorMessage(error)}`;
                    status = formatDiagnosticStatus(available, {
                        source: "model fetch",
                        cause: error,
                    });
                }
                if (!modelsValue.startsWith("Error -")) {
                    try {
                        await this.listModes({ cwd: homedir(), force: false });
                    }
                    catch (error) {
                        status = formatDiagnosticStatus(available, {
                            source: "mode fetch",
                            cause: error,
                        });
                    }
                }
            }
            return {
                diagnostic: formatProviderDiagnostic("OpenCode", [
                    {
                        label: "Binary",
                        value: resolvedBinary ?? "not found",
                    },
                    {
                        label: "Version",
                        value: resolvedBinary ? await resolveBinaryVersion(resolvedBinary) : "unknown",
                    },
                    { label: "Server", value: serverStatus },
                    { label: "Auth", value: authValue },
                    { label: "Models", value: modelsValue },
                    { label: "Status", value: status },
                ]),
            };
        }
        catch (error) {
            return {
                diagnostic: formatProviderDiagnosticError("OpenCode", error),
            };
        }
    }
    assertConfig(config) {
        if (config.provider !== "opencode") {
            throw new Error(`OpenCodeAgentClient received config for provider '${config.provider}'`);
        }
        return { ...config, provider: "opencode" };
    }
    async populateModelContextWindowCache(client, cwd) {
        const response = await client.provider.list({ directory: cwd });
        if (response.error || !response.data) {
            return;
        }
        const lookup = buildOpenCodeModelContextWindowLookup(response.data);
        this.modelContextWindows.clear();
        for (const [modelLookupKey, contextWindowMaxTokens] of lookup.entries()) {
            this.modelContextWindows.set(modelLookupKey, contextWindowMaxTokens);
        }
    }
}
const MAX_OPENCODE_SUB_AGENT_ACTIONS = 200;
const MAX_OPENCODE_PENDING_CHILD_TOOL_PARTS = 200;
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 mapOpenCodeTodosToTimelineItems(todos) {
    return {
        type: "todo",
        items: todos.flatMap((todo) => {
            const text = readNonEmptyString(todo.content);
            if (!text) {
                return [];
            }
            return [
                {
                    text,
                    completed: todo.status === "completed",
                },
            ];
        }),
    };
}
function createCompactionTimelineItem(status, trigger) {
    return {
        type: "compaction",
        status,
        ...(trigger ? { trigger } : {}),
    };
}
const PERMISSION_COMMAND_KEYS = ["command", "cmd", "shellCommand"];
const PERMISSION_CWD_KEYS = ["cwd", "directory", "path", "workdir"];
const PERMISSION_REASON_KEYS = ["reason", "purpose", "description", "message"];
const PERMISSION_TITLE_BY_NAME = {
    external_directory: "Access external directory",
    bash: "Run shell command",
    read: "Read files",
    read_file: "Read files",
    write: "Write files",
    write_file: "Write files",
    create_file: "Write files",
    edit: "Edit files",
    apply_patch: "Edit files",
    apply_diff: "Edit files",
};
function toHumanReadablePermissionTitle(permission) {
    const mapped = PERMISSION_TITLE_BY_NAME[permission];
    if (mapped) {
        return mapped;
    }
    const normalized = permission
        .split(/[\s_-]+/)
        .map((part) => part.trim())
        .filter((part) => part.length > 0)
        .map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
        .join(" ");
    return normalized.length > 0 ? normalized : "Permission request";
}
function readFirstStringFromRecord(record, keys) {
    if (!record) {
        return null;
    }
    for (const key of keys) {
        const value = readNonEmptyString(record[key]);
        if (value) {
            return value;
        }
    }
    return null;
}
function readPermissionField(metadata, keys) {
    const direct = readFirstStringFromRecord(metadata, keys);
    if (direct) {
        return direct;
    }
    const nestedInput = readOpenCodeRecord(metadata?.input);
    return readFirstStringFromRecord(nestedInput, keys);
}
function buildOpenCodePermissionInput(params) {
    return {
        ...(params.patterns.length > 0 ? { patterns: params.patterns } : {}),
        ...(params.metadata ? { metadata: params.metadata } : {}),
        ...(params.tool ? { tool: params.tool } : {}),
        ...(params.command ? { command: params.command } : {}),
    };
}
function buildOpenCodePermissionDetail(params) {
    if (params.command) {
        return {
            type: "shell",
            command: params.command,
            ...(params.cwd ? { cwd: params.cwd } : {}),
        };
    }
    return {
        type: "unknown",
        input: {
            permission: params.permission,
            ...params.input,
        },
        output: null,
    };
}
function buildOpenCodePermissionDescription(params) {
    const parts = [];
    if (params.reason) {
        parts.push(params.reason);
    }
    if (params.patterns.length > 0) {
        parts.push(`Scope: ${params.patterns.join(", ")}`);
    }
    return parts.length > 0 ? parts.join(" - ") : undefined;
}
export function translateOpenCodeEvent(event, state) {
    const events = [];
    switch (event.type) {
        case "session.created":
        case "session.updated":
            appendOpenCodeSessionCreatedOrUpdated(event, state, events);
            break;
        case "message.updated":
            appendOpenCodeMessageUpdated(event, state, events);
            break;
        case "message.part.updated":
            appendOpenCodeMessagePartUpdated(event, state, events);
            break;
        case "message.part.delta":
            appendOpenCodeMessagePartDelta(event, state, events);
            break;
        case "permission.asked":
            appendOpenCodePermissionAsked(event, state, events);
            break;
        case "question.asked":
            appendOpenCodeQuestionAsked(event, state, events);
            break;
        case "todo.updated":
            if (event.properties.sessionID === state.sessionId) {
                events.push({
                    type: "timeline",
                    provider: "opencode",
                    item: mapOpenCodeTodosToTimelineItems(event.properties.todos),
                });
            }
            break;
        case "session.compacted":
            if (event.properties.sessionID === state.sessionId) {
                events.push({
                    type: "timeline",
                    provider: "opencode",
                    item: createCompactionTimelineItem("completed"),
                });
            }
            break;
        case "session.idle":
            if (event.properties.sessionID === state.sessionId) {
                resetOpenCodeTurnTrackingState(state);
                events.push({ type: "turn_completed", provider: "opencode", usage: undefined });
            }
            break;
        case "session.error":
            if (event.properties.sessionID === state.sessionId) {
                resetOpenCodeTurnTrackingState(state);
                events.push({
                    type: "turn_failed",
                    provider: "opencode",
                    error: toDiagnosticErrorMessage(event.properties.error),
                });
            }
            break;
        case "session.status":
            appendOpenCodeSessionStatus(event, state, events);
            break;
    }
    return events;
}
function resetOpenCodeTurnTrackingState(state) {
    state.streamedPartKeys.clear();
    state.partTypes.clear();
}
function getOpenCodeSubAgentMaps(state) {
    state.subAgentsByCallId ?? (state.subAgentsByCallId = new Map());
    state.subAgentCallIdByChildSessionId ?? (state.subAgentCallIdByChildSessionId = new Map());
    state.pendingChildToolPartsBySessionId ?? (state.pendingChildToolPartsBySessionId = new Map());
    return {
        byCallId: state.subAgentsByCallId,
        callIdByChildSessionId: state.subAgentCallIdByChildSessionId,
        pendingChildToolPartsBySessionId: state.pendingChildToolPartsBySessionId,
    };
}
function getOpenCodeSubAgentState(callId, state, toolCall) {
    const maps = getOpenCodeSubAgentMaps(state);
    const existing = maps.byCallId.get(callId);
    if (existing) {
        existing.toolCall = toolCall;
        return existing;
    }
    const created = {
        toolCall,
        actions: [],
        actionIndexByKey: new Map(),
        nextActionIndex: 1,
    };
    maps.byCallId.set(callId, created);
    return created;
}
function linkOpenCodeSubAgentChildSession(activity, childSessionId, state) {
    activity.childSessionId = childSessionId;
    const maps = getOpenCodeSubAgentMaps(state);
    maps.callIdByChildSessionId.set(childSessionId, activity.toolCall.callId);
}
function buildOpenCodeSubAgentLog(detail, activity) {
    const actionLog = activity.actions
        .map((action) => action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`)
        .join("\n");
    const parts = [actionLog, detail.log].filter((part) => part.trim().length > 0);
    return parts.join("\n\n");
}
function buildOpenCodeSubAgentTimelineItem(activity) {
    const toolCall = activity.toolCall;
    if (toolCall.detail.type !== "sub_agent") {
        return toolCall;
    }
    const childSessionId = activity.childSessionId ?? toolCall.detail.childSessionId;
    return {
        ...toolCall,
        detail: {
            ...toolCall.detail,
            ...(childSessionId ? { childSessionId } : {}),
            log: buildOpenCodeSubAgentLog(toolCall.detail, activity),
        },
    };
}
function registerOpenCodeSubAgentToolCall(item, state) {
    if (item.detail.type !== "sub_agent") {
        return item;
    }
    const activity = getOpenCodeSubAgentState(item.callId, state, item);
    if (item.detail.childSessionId) {
        linkOpenCodeSubAgentChildSession(activity, item.detail.childSessionId, state);
    }
    return buildOpenCodeSubAgentTimelineItem(activity);
}
function bufferOpenCodeSubAgentChildToolPart(part, state) {
    const maps = getOpenCodeSubAgentMaps(state);
    if (maps.byCallId.size === 0) {
        return;
    }
    const totalPending = [...maps.pendingChildToolPartsBySessionId.values()].reduce((total, parts) => total + parts.length, 0);
    if (totalPending >= MAX_OPENCODE_PENDING_CHILD_TOOL_PARTS) {
        return;
    }
    const pending = maps.pendingChildToolPartsBySessionId.get(part.sessionID) ?? [];
    pending.push(part);
    maps.pendingChildToolPartsBySessionId.set(part.sessionID, pending);
}
function flushOpenCodeSubAgentChildToolParts(childSessionId, state, events) {
    const maps = getOpenCodeSubAgentMaps(state);
    const pending = maps.pendingChildToolPartsBySessionId.get(childSessionId);
    if (!pending || pending.length === 0) {
        return;
    }
    maps.pendingChildToolPartsBySessionId.delete(childSessionId);
    for (const part of pending) {
        appendOpenCodeSubAgentChildToolPart(part, state, events);
    }
}
function findOnlyOpenCodeSubAgentWaitingForChild(state) {
    const maps = getOpenCodeSubAgentMaps(state);
    const candidates = [...maps.byCallId.values()].filter((activity) => activity.toolCall.status === "running" &&
        activity.toolCall.detail.type === "sub_agent" &&
        !activity.childSessionId);
    return candidates.length === 1 ? (candidates[0] ?? null) : null;
}
function summarizeOpenCodeSubAgentAction(item, cwd) {
    const display = buildToolCallDisplayModel({
        name: item.name,
        status: item.status,
        error: item.error,
        metadata: item.metadata,
        detail: item.detail,
        cwd,
    });
    return display.summary ?? display.errorText;
}
function appendOpenCodeSubAgentAction(activity, item, cwd) {
    const key = item.callId || `${item.name}:${activity.actions.length}`;
    const existingIndex = activity.actionIndexByKey.get(key);
    const summary = summarizeOpenCodeSubAgentAction(item, cwd);
    if (existingIndex !== undefined) {
        const action = activity.actions[existingIndex];
        if (!action) {
            return false;
        }
        const changed = action.toolName !== item.name || action.summary !== summary;
        action.toolName = item.name;
        if (summary) {
            action.summary = summary;
        }
        else {
            delete action.summary;
        }
        return changed;
    }
    if (activity.actions.length >= MAX_OPENCODE_SUB_AGENT_ACTIONS) {
        return false;
    }
    activity.actionIndexByKey.set(key, activity.actions.length);
    activity.actions.push({
        index: activity.nextActionIndex,
        key,
        toolName: item.name,
        ...(summary ? { summary } : {}),
    });
    activity.nextActionIndex += 1;
    return true;
}
function appendOpenCodeToolCallTimelineItem(item, state, events) {
    const timelineItem = registerOpenCodeSubAgentToolCall(item, state);
    events.push({
        type: "timeline",
        provider: "opencode",
        item: timelineItem,
    });
    if (timelineItem.detail.type === "sub_agent" && timelineItem.detail.childSessionId) {
        flushOpenCodeSubAgentChildToolParts(timelineItem.detail.childSessionId, state, events);
    }
}
function appendOpenCodeSubAgentChildSessionLinked(childSessionId, state, events) {
    const activity = findOnlyOpenCodeSubAgentWaitingForChild(state);
    if (!activity) {
        return;
    }
    linkOpenCodeSubAgentChildSession(activity, childSessionId, state);
    events.push({
        type: "timeline",
        provider: "opencode",
        item: buildOpenCodeSubAgentTimelineItem(activity),
    });
    flushOpenCodeSubAgentChildToolParts(childSessionId, state, events);
}
function appendOpenCodeSubAgentChildToolPart(part, state, events) {
    const maps = getOpenCodeSubAgentMaps(state);
    const parentCallId = maps.callIdByChildSessionId.get(part.sessionID);
    if (!parentCallId) {
        bufferOpenCodeSubAgentChildToolPart(part, state);
        return;
    }
    const activity = maps.byCallId.get(parentCallId);
    if (!activity) {
        return;
    }
    const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
    if (!parsedToolPart.success || !parsedToolPart.data) {
        return;
    }
    if (!appendOpenCodeSubAgentAction(activity, parsedToolPart.data, state.cwd)) {
        return;
    }
    events.push({
        type: "timeline",
        provider: "opencode",
        item: buildOpenCodeSubAgentTimelineItem(activity),
    });
}
function appendOpenCodeSessionCreatedOrUpdated(event, state, events) {
    if (event.properties.info.id === state.sessionId) {
        events.push({
            type: "thread_started",
            sessionId: state.sessionId,
            provider: "opencode",
        });
        return;
    }
    const info = readOpenCodeRecord(event.properties.info);
    const parentSessionId = readNonEmptyString(info?.parentID) ?? readNonEmptyString(info?.parentId);
    if (parentSessionId === state.sessionId) {
        appendOpenCodeSubAgentChildSessionLinked(event.properties.info.id, state, events);
    }
}
function appendOpenCodeMessageUpdated(event, state, events) {
    const info = event.properties.info;
    if (info.sessionID !== state.sessionId) {
        return;
    }
    state.messageRoles.set(info.id, info.role);
    if (info.role !== "assistant") {
        return;
    }
    const modelLookupKey = resolveOpenCodeModelLookupKeyFromAssistantMessage(info);
    if (modelLookupKey) {
        const contextWindowMaxTokens = state.modelContextWindowsByModelKey?.get(modelLookupKey);
        if (contextWindowMaxTokens !== undefined) {
            state.onAssistantModelContextWindowResolved?.(contextWindowMaxTokens);
        }
    }
    if (state.emittedStructuredMessageIds.has(info.id) || info.time?.completed === undefined) {
        return;
    }
    const text = stringifyStructuredAssistantMessage(info.structured);
    if (!text) {
        return;
    }
    state.emittedStructuredMessageIds.add(info.id);
    events.push({
        type: "timeline",
        provider: "opencode",
        item: { type: "assistant_message", text },
    });
}
function appendOpenCodeMessagePartUpdated(event, state, events) {
    const part = event.properties.part;
    if (part.sessionID !== state.sessionId) {
        if (part.type === "tool") {
            appendOpenCodeSubAgentChildToolPart(part, state, events);
        }
        return;
    }
    const messageRole = state.messageRoles.get(part.messageID);
    state.partTypes.set(part.id, part.type);
    if (part.type === "text") {
        appendOpenCodeTextPart(part, messageRole, state, events);
        return;
    }
    if (part.type === "reasoning") {
        appendOpenCodeReasoningPart(part, state, events);
        return;
    }
    if (part.type === "tool") {
        const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
        if (parsedToolPart.success && parsedToolPart.data) {
            appendOpenCodeToolCallTimelineItem(parsedToolPart.data, state, events);
        }
        return;
    }
    if (part.type === "compaction") {
        events.push({
            type: "timeline",
            provider: "opencode",
            item: createCompactionTimelineItem("loading", part.auto ? "auto" : "manual"),
        });
        return;
    }
    if (part.type === "step-finish") {
        mergeOpenCodeStepFinishUsage(state.accumulatedUsage, part);
        if (hasNormalizedOpenCodeUsage(state.accumulatedUsage)) {
            events.push({
                type: "usage_updated",
                provider: "opencode",
                usage: { ...state.accumulatedUsage },
            });
        }
    }
}
function appendOpenCodeTextPart(part, messageRole, state, events) {
    if (messageRole === "user") {
        return;
    }
    if (!part.time?.end) {
        return;
    }
    const partKey = resolvePartDedupeKey(part, "text");
    if (partKey && state.streamedPartKeys.delete(partKey)) {
        return;
    }
    if (part.text) {
        events.push({
            type: "timeline",
            provider: "opencode",
            item: { type: "assistant_message", text: part.text },
        });
    }
}
function appendOpenCodeReasoningPart(part, state, events) {
    if (!part.time.end) {
        return;
    }
    const partKey = resolvePartDedupeKey(part, "reasoning");
    if (partKey && state.streamedPartKeys.delete(partKey)) {
        return;
    }
    if (part.text) {
        events.push({
            type: "timeline",
            provider: "opencode",
            item: { type: "reasoning", text: part.text },
        });
    }
}
function appendOpenCodeMessagePartDelta(event, state, events) {
    const { sessionID, messageID, partID, field, delta } = event.properties;
    if (sessionID !== state.sessionId) {
        return;
    }
    if (!delta || !field) {
        return;
    }
    const messageRole = messageID ? state.messageRoles.get(messageID) : undefined;
    const knownPartType = partID ? state.partTypes.get(partID) : undefined;
    const isReasoning = knownPartType === "reasoning" || field === "reasoning";
    if (isReasoning) {
        if (partID) {
            state.streamedPartKeys.add(`reasoning:${partID}`);
        }
        events.push({
            type: "timeline",
            provider: "opencode",
            item: { type: "reasoning", text: delta },
        });
        return;
    }
    if (field !== "text") {
        return;
    }
    if (messageRole === "user") {
        return;
    }
    if (partID) {
        state.streamedPartKeys.add(`text:${partID}`);
    }
    events.push({
        type: "timeline",
        provider: "opencode",
        item: { type: "assistant_message", text: delta },
    });
}
function appendOpenCodePermissionAsked(event, state, events) {
    if (event.properties.sessionID !== state.sessionId) {
        return;
    }
    const metadata = readOpenCodeRecord(event.properties.metadata);
    const tool = readOpenCodeRecord(event.properties.tool);
    const patterns = Array.isArray(event.properties.patterns)
        ? event.properties.patterns.filter((value) => typeof value === "string")
        : [];
    const command = readPermissionField(metadata, PERMISSION_COMMAND_KEYS);
    const cwd = readPermissionField(metadata, PERMISSION_CWD_KEYS);
    const reason = readPermissionField(metadata, PERMISSION_REASON_KEYS);
    const input = buildOpenCodePermissionInput({ patterns, metadata, tool, command });
    const detail = buildOpenCodePermissionDetail({
        permission: event.properties.permission,
        input,
        command,
        cwd,
    });
    const description = buildOpenCodePermissionDescription({ reason, patterns });
    events.push({
        type: "permission_requested",
        provider: "opencode",
        request: {
            id: event.properties.id,
            provider: "opencode",
            name: event.properties.permission,
            kind: "tool",
            title: toHumanReadablePermissionTitle(event.properties.permission),
            ...(description ? { description } : {}),
            input,
            detail,
        },
    });
}
function appendOpenCodeQuestionAsked(event, state, events) {
    if (event.properties.sessionID !== state.sessionId) {
        return;
    }
    const questions = event.properties.questions.flatMap((q) => {
        if (!q.question || !q.header) {
            return [];
        }
        const options = q.options?.map((o) => ({
            label: o.label,
            ...(o.description ? { description: o.description } : {}),
        })) ?? [];
        return [
            {
                question: q.question,
                header: q.header,
                options,
                ...(q.multiple === true ? { multiSelect: true } : {}),
            },
        ];
    });
    if (questions.length === 0) {
        return;
    }
    events.push({
        type: "permission_requested",
        provider: "opencode",
        request: {
            id: event.properties.id,
            provider: "opencode",
            name: "question",
            kind: "question",
            title: "Question",
            input: { questions },
            metadata: {
                source: "opencode_question",
                ...event.properties.tool,
            },
        },
    });
}
function appendOpenCodeSessionStatus(event, state, events) {
    if (event.properties.sessionID !== state.sessionId) {
        return;
    }
    const { status } = event.properties;
    if (status.type === "idle") {
        resetOpenCodeTurnTrackingState(state);
        events.push({ type: "turn_completed", provider: "opencode", usage: undefined });
        return;
    }
    if (status.type === "retry") {
        // Mirror what opencode's TUI shows: retry attempts are visible activity, not
        // terminal. opencode itself never gives up — it backs off and tries again
        // forever. If we silently swallow these the user sees a spinner with no
        // explanation. Forwarding as a timeline error item is a no-op for old
        // clients (the schema already supports it).
        const message = typeof status.message === "string" ? status.message.trim() : "";
        const text = message
            ? `Provider retry (attempt ${status.attempt}): ${message}`
            : `Provider retry (attempt ${status.attempt})`;
        events.push({
            type: "timeline",
            provider: "opencode",
            item: { type: "error", message: text },
        });
        return;
    }
    // "busy" is transient — no terminal event, no surfaced activity.
}
function createDeferred() {
    let resolve;
    let reject;
    const promise = new Promise((res, rej) => {
        resolve = res;
        reject = rej;
    });
    return { promise, resolve, reject };
}
function unwrapOpenCodeGlobalEvent(event) {
    const record = readOpenCodeRecord(event);
    if (!record) {
        return null;
    }
    const payload = readOpenCodeRecord(record.payload);
    if (typeof payload?.type === "string") {
        return payload;
    }
    if (typeof record.type === "string") {
        return record;
    }
    return null;
}
class OpenCodeAgentSession {
    constructor(config, client, sessionId, logger, _storageRoot, modelContextWindowsByModelKey = new Map(), releaseServer, persistSession = true, agentId) {
        this.agentId = agentId;
        this.provider = "opencode";
        this.capabilities = OPENCODE_CAPABILITIES;
        this.currentMode = "default";
        this.pendingPermissions = new Map();
        this.abortController = null;
        this.pendingAbortPromise = 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.subAgentsByCallId = new Map();
        this.subAgentCallIdByChildSessionId = new Map();
        this.pendingChildToolPartsBySessionId = new Map();
        this.deletedFromProvider = false;
        this.config = config;
        this.client = client;
        this.sessionId = sessionId;
        this.logger = logger.child({ agentId: this.agentId });
        this.modelContextWindowsByModelKey = modelContextWindowsByModelKey;
        this.currentMode = normalizeOpenCodeModeId(config.modeId);
        this.releaseServer = releaseServer ?? null;
        this.persistSession = persistSession;
        this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens(config.model);
    }
    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;
        this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens(this.config.model);
    }
    async setThinkingOption(thinkingOptionId) {
        const normalizedThinkingOptionId = typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
            ? thinkingOptionId
            : null;
        this.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined;
    }
    async run(prompt, options) {
        return runProviderTurn({
            prompt,
            runOptions: options,
            startTurn: (p, o) => this.startTurn(p, o),
            subscribe: (callback) => this.subscribe(callback),
            getSessionId: () => this.sessionId,
        });
    }
    async interrupt() {
        const turnId = this.activeForegroundTurnId;
        const turnAbortController = this.abortController;
        turnAbortController?.abort();
        // COMPAT(opencodeSlowAbort): OpenCode 1.14.42+ blocks session.abort until
        // the running tool actually stops, which can be tens of seconds for
        // long-running tools. Cap the wait so the user-visible cancel lands
        // quickly while still giving OpenCode a chance to confirm the abort
        // cleanly. Drop the timeout once upstream returns abort acknowledgement
        // before tool teardown.
        const abortPromise = this.beginSessionAbort(turnId, "interrupt");
        await withTimeout(abortPromise, 2000, "OpenCode session.abort").catch((error) => {
            this.logger.warn({ err: error, sessionId: this.sessionId, turnId }, "OpenCode session.abort exceeded the cancel cap; proceeding with local cancel");
        });
        if (turnId) {
            this.finishForegroundTurn({ type: "turn_canceled", provider: "opencode", reason: "interrupted" }, turnId);
        }
    }
    beginSessionAbort(turnId, reason) {
        const abortPromise = this.client.session
            .abort({
            sessionID: this.sessionId,
            directory: this.config.cwd,
        })
            .then(() => undefined)
            .catch((error) => {
            this.logger.warn({ err: error, sessionId: this.sessionId, turnId, reason }, "OpenCode session.abort rejected");
        });
        const trackedAbortPromise = abortPromise.finally(() => {
            if (this.pendingAbortPromise === trackedAbortPromise) {
                this.pendingAbortPromise = null;
            }
        });
        this.pendingAbortPromise = trackedAbortPromise;
        return trackedAbortPromise;
    }
    async awaitPendingAbortBeforeStartingTurn() {
        const pendingAbortPromise = this.pendingAbortPromise;
        if (!pendingAbortPromise) {
            return;
        }
        await withTimeout(pendingAbortPromise, OPENCODE_PENDING_ABORT_START_TIMEOUT_MS, "OpenCode pending session.abort").catch((error) => {
            this.logger.warn({ err: error, sessionId: this.sessionId }, "OpenCode session.abort was still pending before starting the next turn");
        });
    }
    async startTurn(prompt, options) {
        if (this.activeForegroundTurnId) {
            throw new Error("A foreground turn is already active");
        }
        await this.awaitPendingAbortBeforeStartingTurn();
        this.runningToolCalls.clear();
        this.subAgentsByCallId.clear();
        this.subAgentCallIdByChildSessionId.clear();
        this.pendingChildToolPartsBySessionId.clear();
        const turnAbortController = new AbortController();
        this.abortController = turnAbortController;
        await this.ensureMcpServersConfigured();
        const contextWindowMaxTokens = this.resolveSelectedModelContextWindowMaxTokens();
        this.accumulatedUsage = contextWindowMaxTokens !== undefined ? { contextWindowMaxTokens } : {};
        const parts = buildOpenCodePromptParts(prompt);
        const model = this.parseModel(this.config.model);
        const thinkingOptionId = this.config.thinkingOptionId;
        const effectiveVariant = thinkingOptionId ?? undefined;
        const effectiveMode = resolveOpenCodeRuntimeAgentId(this.currentMode);
        const turnId = this.createTurnId();
        this.activeForegroundTurnId = turnId;
        // OpenCode's /event SSE endpoint does NOT replay past events. If we send
        // the prompt before our reader is connected, terminal events fired early
        // by the server (e.g. session.error / session.idle for invalid model or
        // mode) are missed and the turn hangs forever. Wait for the subscription
        // to be established before sending anything.
        const subscriptionReady = createDeferred();
        void this.consumeEventStream(turnId, turnAbortController, subscriptionReady);
        try {
            await subscriptionReady.promise;
        }
        catch {
            // consumeEventStream already finished the turn with the subscription error.
            return { turnId };
        }
        this.notifySubscribers({ type: "turn_started", provider: "opencode" }, turnId);
        const slashCommand = await this.resolveSlashCommandInvocation(prompt);
        if (slashCommand) {
            if (slashCommand.commandName === "compact" || slashCommand.commandName === "summarize") {
                void this.client.session
                    .summarize({
                    sessionID: this.sessionId,
                    directory: this.config.cwd,
                    ...(model ? { providerID: model.providerID, modelID: model.modelID } : {}),
                })
                    .then((response) => {
                    if (response.error) {
                        this.finishForegroundTurn({
                            type: "turn_failed",
                            provider: "opencode",
                            error: toDiagnosticErrorMessage(response.error),
                        }, turnId);
                    }
                    else {
                        this.finishForegroundTurn({ type: "turn_completed", provider: "opencode", usage: undefined }, turnId);
                    }
                    return;
                })
                    .catch((error) => {
                    this.finishForegroundTurn({
                        type: "turn_failed",
                        provider: "opencode",
                        error: toDiagnosticErrorMessage(error),
                    }, turnId);
                });
                return { turnId };
            }
            // command() blocks until the server finishes processing. OpenCode's SSE
            // endpoint does NOT replay past events, so if the command completes before
            // our SSE reader connects, we miss `session.idle` and the turn hangs.
            // Handle both success and error in the response handler as a fallback —
            // finishForegroundTurn's guard prevents duplicate terminal events if the
            // SSE stream already delivered the event.
            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) {
                    if (isOpenCodeHeadersTimeoutFailure(response.error)) {
                        this.logger.warn({
                            err: response.error,
                            commandName: slashCommand.commandName,
                            turnId,
                        }, "OpenCode slash command hit a header timeout; waiting for SSE terminal event");
                        return;
                    }
                    const errorMsg = toDiagnosticErrorMessage(response.error);
                    this.finishForegroundTurn({ type: "turn_failed", provider: "opencode", error: errorMsg }, turnId);
                }
                else {
                    this.finishForegroundTurn({ type: "turn_completed", provider: "opencode", usage: undefined }, turnId);
                }
                return;
            })
                .catch((err) => {
                if (isOpenCodeHeadersTimeoutFailure(err)) {
                    this.logger.warn({
                        err,
                        commandName: slashCommand.commandName,
                        turnId,
                    }, "OpenCode slash command hit a header timeout; waiting for SSE terminal event");
                    return;
                }
                this.finishForegroundTurn({ type: "turn_failed", provider: "opencode", error: toDiagnosticErrorMessage(err) }, turnId);
            });
        }
        else {
            // Wrap in an async IIFE so a synchronous throw from promptAsync (e.g.
            // SDK input validation) is caught alongside async rejections. A plain
            // `.then().catch()` chain would let a sync throw escape unhandled.
            void (async () => {
                this.traceOpenCode("provider.opencode.prompt_async.start", {
                    turnId,
                    sessionId: this.sessionId,
                    model,
                    effectiveMode,
                    effectiveVariant,
                    partTypes: parts.map((p) => p.type),
                });
                try {
                    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 } : {}),
                    });
                    this.traceOpenCode("provider.opencode.prompt_async.response", {
                        turnId,
                        hasError: promptResponse.error !== undefined,
                        error: promptResponse.error,
                        data: promptResponse.data,
                    });
                    if (promptResponse.error) {
                        this.finishForegroundTurn({
                            type: "turn_failed",
                            provider: "opencode",
                            error: toDiagnosticErrorMessage(promptResponse.error),
                        }, turnId);
                    }
                }
                catch (error) {
                    this.traceOpenCode("provider.opencode.prompt_async.throw", {
                        turnId,
                        error: error instanceof Error
                            ? { name: error.name, message: error.message, stack: error.stack }
                            : String(error),
                    });
                    this.finishForegroundTurn({
                        type: "turn_failed",
                        provider: "opencode",
                        error: toDiagnosticErrorMessage(error),
                    }, turnId);
                }
            })();
        }
        return { turnId };
    }
    subscribe(callback) {
        this.subscribers.add(callback);
        return () => {
            this.subscribers.delete(callback);
        };
    }
    async consumeEventStream(turnId, turnAbortController, subscriptionReady) {
        this.traceOpenCode("provider.opencode.subscribe.start", {
            turnId,
            sessionId: this.sessionId,
            cwd: this.config.cwd,
        });
        try {
            const result = await this.client.global.event({
                signal: turnAbortController.signal,
                sseMaxRetryAttempts: 0,
            });
            let eventCount = 0;
            let subscriptionReadyResolved = false;
            for await (const rawEvent of result.stream) {
                eventCount += 1;
                if (!subscriptionReadyResolved) {
                    subscriptionReadyResolved = true;
                    this.traceOpenCode("provider.opencode.subscribe.ready", {
                        turnId,
                        sessionId: this.sessionId,
                    });
                    subscriptionReady.resolve();
                }
                const shouldContinue = await this.consumeOpenCodeStreamEvent({
                    rawEvent,
                    eventCount,
                    turnId,
                    turnAbortController,
                });
                if (!shouldContinue) {
                    return;
                }
            }
            this.traceOpenCode("provider.opencode.stream.eof", {
                turnId,
                eventCount,
                aborted: turnAbortController.signal.aborted,
                stillActive: this.activeForegroundTurnId === turnId,
            });
            if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
                this.traceOpenCode("provider.opencode.turn.fail_eof", { turnId, eventCount });
                if (!subscriptionReadyResolved) {
                    subscriptionReady.reject(new Error("OpenCode event stream ended before it became ready"));
                }
                this.finishForegroundTurn({
                    type: "turn_failed",
                    provider: "opencode",
                    error: "OpenCode event stream ended before the turn reached a terminal state",
                }, turnId);
            }
        }
        catch (error) {
            this.traceOpenCode("provider.opencode.subscribe.error", {
                turnId,
                error: error instanceof Error ? { name: error.name, message: error.message } : String(error),
            });
            subscriptionReady.reject(error);
            if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
                this.finishForegroundTurn({
                    type: "turn_failed",
                    provider: "opencode",
                    error: toDiagnosticErrorMessage(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;
            }
        }
    }
    async consumeOpenCodeStreamEvent(params) {
        const { rawEvent, eventCount, turnId, turnAbortController } = params;
        const event = unwrapOpenCodeGlobalEvent(rawEvent);
        this.traceOpenCode("provider.opencode.raw_event", {
            turnId,
            n: eventCount,
            type: event?.type,
            rawType: readOpenCodeRecord(rawEvent)?.type,
            directory: readOpenCodeRecord(rawEvent)?.directory,
            rawEvent,
            properties: event?.properties,
        });
        if (!event) {
            return true;
        }
        if (turnAbortController.signal.aborted || this.activeForegroundTurnId !== turnId) {
            this.traceOpenCode("provider.opencode.event.skip", {
                turnId,
                n: eventCount,
                aborted: turnAbortController.signal.aborted,
                activeTurnId: this.activeForegroundTurnId,
            });
            return false;
        }
        const translated = await this.translateEvent(event);
        this.traceOpenCode("provider.opencode.parsed_event", {
            turnId,
            n: eventCount,
            count: translated.length,
            types: translated.map((t) => t.type),
            events: translated,
        });
        for (const e of translated) {
            if (this.activeForegroundTurnId !== turnId) {
                this.traceOpenCode("provider.opencode.parsed_event.skip_active", { turnId, type: e.type });
                return false;
            }
            if (e.type === "timeline" && e.item.type === "tool_call") {
                this.trackToolCall(e.item);
            }
            const terminalEvent = toTerminalTurnEvent(e);
            if (terminalEvent) {
                this.traceOpenCode("provider.opencode.event.terminal", {
                    turnId,
                    type: terminalEvent.type,
                });
                this.finishForegroundTurn(terminalEvent, turnId);
                return false;
            }
            this.notifySubscribers(e, turnId);
        }
        return true;
    }
    finishForegroundTurn(event, turnId) {
        this.traceOpenCode("provider.opencode.finish_foreground_turn", {
            turnId,
            activeTurnId: this.activeForegroundTurnId,
            type: event.type,
            error: event.type === "turn_failed" ? event.error : undefined,
            reason: event.type === "turn_canceled" ? event.reason : undefined,
        });
        if (this.activeForegroundTurnId !== turnId) {
            return;
        }
        if (event.type === "turn_canceled" || event.type === "turn_failed") {
            this.synthesizeInterruptedToolCalls(turnId);
        }
        else {
            this.runningToolCalls.clear();
        }
        this.activeForegroundTurnId = null;
        // Abort the SSE connection so the SDK tears down the underlying fetch.
        this.abortController?.abort();
        this.abortController = 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()) {
            const error = { message: "Tool execution aborted" };
            this.notifySubscribers({
                type: "timeline",
                provider: "opencode",
                item: {
                    ...item,
                    status: "failed",
                    error,
                    detail: item.detail.type === "sub_agent"
                        ? {
                            ...item.detail,
                            log: [item.detail.log, error.message]
                                .filter((entry) => entry.trim().length > 0)
                                .join("\n"),
                        }
                        : item.detail,
                },
            }, turnId);
        }
        this.runningToolCalls.clear();
    }
    notifySubscribers(event, turnIdOverride) {
        const turnId = turnIdOverride ?? this.activeForegroundTurnId;
        const tagged = turnId ? { ...event, turnId } : event;
        this.traceOpenCode("provider.opencode.event_emit", {
            turnId: getAgentStreamEventTurnId(tagged),
            event: tagged,
        });
        for (const callback of this.subscribers) {
            try {
                callback(tagged);
            }
            catch {
                // Subscriber callback error isolation
            }
        }
    }
    createTurnId() {
        return `opencode-turn-${this.nextTurnOrdinal++}`;
    }
    traceOpenCode(msg, data = {}) {
        this.logger.trace({
            agentId: this.agentId,
            provider: "opencode",
            sessionId: this.sessionId,
            turnId: data.turnId ?? this.activeForegroundTurnId ?? undefined,
            ...data,
        }, msg);
    }
    async *streamHistory() {
        const response = await this.client.session.messages({
            sessionID: this.sessionId,
            directory: this.config.cwd,
        });
        if (response.error || !response.data) {
            return;
        }
        for (const { info, parts } of response.data) {
            if (info.role === "user") {
                const text = parts
                    .filter((p) => p.type === "text")
                    .map((p) => p.text)
                    .join("");
                if (text) {
                    yield buildOpenCodeReplayTimelineEvent({
                        item: { type: "user_message", text },
                        message: info,
                    });
                }
            }
            else {
                let emittedAssistantText = false;
                for (const part of parts) {
                    if (part.type === "text" && part.text) {
                        emittedAssistantText = true;
                    }
                    const event = buildOpenCodeReplayPartTimelineEvent({ part, message: info });
                    if (event) {
                        yield event;
                    }
                }
                if (!emittedAssistantText) {
                    const text = stringifyStructuredAssistantMessage(info.structured);
                    if (text) {
                        yield buildOpenCodeReplayTimelineEvent({
                            item: { type: "assistant_message", text },
                            message: info,
                        });
                    }
                }
            }
        }
    }
    async getAvailableModes() {
        if (this.availableModesCache) {
            return this.availableModesCache;
        }
        const response = await this.client.app.agents({
            directory: this.config.cwd,
        });
        const agents = response.error || !response.data ? [] : response.data;
        const discoveredModes = agents.filter(isSelectableOpenCodeAgent).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 = mergeOpenCodeModes(discoveredModes);
        return this.availableModesCache;
    }
    async getCurrentMode() {
        return this.currentMode;
    }
    async listCommands() {
        const result = await this.client.command.list({
            directory: this.config.cwd,
        });
        const commandsByName = new Map(OPENCODE_HANDLED_BUILTIN_SLASH_COMMANDS.map((command) => [command.name, command]));
        if (result.error || !result.data) {
            return Array.from(commandsByName.values());
        }
        for (const cmd of result.data) {
            commandsByName.set(cmd.name, {
                name: cmd.name,
                description: cmd.description ?? "",
                argumentHint: cmd.hints?.length ? cmd.hints.join(" ") : "",
            });
        }
        return Array.from(commandsByName.values());
    }
    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() {
        try {
            this.abortController?.abort();
            await reconcileOpenCodeSessionClose({
                client: this.client,
                sessionId: this.sessionId,
                directory: this.config.cwd,
                logger: this.logger,
            });
            await this.deleteProviderSessionIfEphemeral();
            this.subscribers.clear();
            this.activeForegroundTurnId = null;
        }
        finally {
            this.releaseServer?.();
            this.releaseServer = null;
        }
    }
    async deleteProviderSessionIfEphemeral() {
        if (this.persistSession || this.deletedFromProvider) {
            return;
        }
        this.deletedFromProvider = true;
        try {
            const response = await this.client.session.delete({
                sessionID: this.sessionId,
                directory: this.config.cwd,
            });
            if (response.error) {
                throw new Error(`OpenCode session.delete failed: ${JSON.stringify(response.error)}`);
            }
        }
        catch (error) {
            this.logger.debug({ err: error, sessionId: this.sessionId }, "Failed to delete non-persistent OpenCode session");
        }
    }
    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) {
        await Promise.all(Object.entries(mcpServers).map(([name, serverConfig]) => this.registerMcpServer(name, toOpenCodeMcpConfig(serverConfig))));
    }
    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}': ${toDiagnosticErrorMessage(error)}`);
    }
    async translateEvent(event) {
        const translated = translateOpenCodeEvent(event, {
            sessionId: this.sessionId,
            cwd: this.config.cwd,
            messageRoles: this.messageRoles,
            accumulatedUsage: this.accumulatedUsage,
            streamedPartKeys: this.streamedPartKeys,
            emittedStructuredMessageIds: this.emittedStructuredMessageIds,
            partTypes: this.partTypes,
            subAgentsByCallId: this.subAgentsByCallId,
            subAgentCallIdByChildSessionId: this.subAgentCallIdByChildSessionId,
            pendingChildToolPartsBySessionId: this.pendingChildToolPartsBySessionId,
            modelContextWindowsByModelKey: this.modelContextWindowsByModelKey,
            onAssistantModelContextWindowResolved: (contextWindowMaxTokens) => {
                this.accumulatedUsage.contextWindowMaxTokens = contextWindowMaxTokens;
                if (!this.config.model) {
                    this.selectedModelContextWindowMaxTokens = contextWindowMaxTokens;
                }
            },
        });
        const events = [];
        for (const translatedEvent of translated) {
            if (translatedEvent.type === "permission_requested") {
                const autoApproved = await this.tryAutoApproveToolPermission(translatedEvent.request);
                if (autoApproved) {
                    continue;
                }
                this.pendingPermissions.set(translatedEvent.request.id, translatedEvent.request);
            }
            if (translatedEvent.type === "turn_completed") {
                if (hasNormalizedOpenCodeUsage(this.accumulatedUsage)) {
                    translatedEvent.usage = this.accumulatedUsage;
                }
                const contextWindowMaxTokens = this.resolveSelectedModelContextWindowMaxTokens();
                this.accumulatedUsage =
                    contextWindowMaxTokens !== undefined ? { contextWindowMaxTokens } : {};
            }
            events.push(translatedEvent);
        }
        return events;
    }
    async tryAutoApproveToolPermission(request) {
        if (this.currentMode !== OPENCODE_FULL_ACCESS_MODE_ID || request.kind !== "tool") {
            return false;
        }
        try {
            await this.client.permission.reply({
                requestID: request.id,
                directory: this.config.cwd,
                reply: "once",
            });
            return true;
        }
        catch (error) {
            this.logger.warn({ err: error, requestId: request.id }, "Failed to auto-approve OpenCode tool permission");
            return false;
        }
    }
    resolveSelectedModelContextWindowMaxTokens() {
        return this.selectedModelContextWindowMaxTokens;
    }
    resolveConfiguredModelContextWindowMaxTokens(modelId) {
        const modelLookupKey = parseOpenCodeModelLookupKey(modelId);
        if (!modelLookupKey) {
            return undefined;
        }
        return this.modelContextWindowsByModelKey.get(modelLookupKey);
    }
}
//# sourceMappingURL=opencode-agent.js.map