import { getOpenCode2TuiSource } from './status-plugin-tui-source' import { openCodeTuiPluginDirName } from '../../shared/opencode-tui-plugin-install' import { getLegacyOpenCodeTuiSource } from '../opencode/status-plugin-legacy-tui-source' export function getOpenCode2ModuleSource(pluginID: string, expectedAgent: string): string[] { return [ `const ORCA_TUI_PLUGIN_ENTRY = new URL("./${openCodeTuiPluginDirName(`${pluginID}.js`)}/tui.js", import.meta.url);`, `const ORCA_STATUS_AGENT = "${expectedAgent}";`, ...getLegacyOpenCodeTuiSource(), ...getOpenCode2SetupSource() ] } // Preserve ASCII server bundles while String.raw keeps generated bytes. const EM_DASH = '\u2014' export function getOpenCode2SetupSource(): string[] { return String.raw` // Non-session owners have no Idle ${EM_DASH} ignore known sentinels to avoid stranded blockers. const NON_SESSION_FORM_OWNERS = new Set(["global"]); // Normalize blockers once so server and TUI reports reach Orca in the same shape. function translateOpenCode2Event(inputType, data) { let type = inputType; let properties = data || {}; if (type === "session.created") { properties = { info: { ...properties, id: properties.sessionID } }; } else if (type === "session.execution.started") { type = "session.status"; properties = { ...properties, status: { type: "busy" } }; } else if (type === "session.execution.succeeded" || type === "session.execution.failed" || type === "session.execution.interrupted") { type = "session.status"; properties = { ...properties, status: { type: "idle" } }; } else if (type === "permission.asked") { properties = { ...properties, permission: properties.action, patterns: properties.resources }; } else if (type === "form.created") { const form = properties.form; // Metadata kind is optional; every real-session-owned form must surface a clearable blocker. if (!form || NON_SESSION_FORM_OWNERS.has(form.sessionID)) return null; // Malformed forms must not kill the subscription. const fields = Array.isArray(form.fields) ? form.fields : []; type = "question.asked"; properties = { ...form, questions: fields.map((field) => ({ header: field.title || form.title, question: field.description || field.title || form.title, options: (field.options || []).map((option) => ({ label: option.label || option.value, description: option.description || "" })), multiple: field.type === "multiselect", })), }; } else if (type === "form.replied" || type === "form.cancelled") { type = type === "form.replied" ? "question.replied" : "question.rejected"; properties = { ...properties, requestID: properties.id }; } else if (type === "session.text.started" || type === "session.text.delta" || type === "session.text.ended") { type = type.replace("session.", "session.next."); } return { type, properties }; } // Serve-process environment names only its starter pane; each TUI must report its own pane. async function tuiReportsPaneLifecycle() { if (!process.argv.includes("serve")) return false; try { const { statSync } = await import("node:fs"); // Older SSH installers may omit the TUI copy; retain server reporting as fallback. return statSync(ORCA_TUI_PLUGIN_ENTRY).isFile(); } catch { return false; } } async function setupOpenCode2Status(ctx) { const noop = async () => {}; if (isOpenCode2TuiContext(ctx)) { reportingOpenCodeMajor = 2; return setupOpenCode2Tui(ctx); } let hooks; // OpenCode probes varying setup contexts; avoid startup errors when status reporting is unavailable. try { if (!ctx || typeof ctx.session?.hook !== "function" || typeof ctx.event?.subscribe !== "function") return noop; reportingOpenCodeMajor = 2; if (await tuiReportsPaneLifecycle()) return noop; const controller = new AbortController(); // Wrap bare OpenCode 2 session records for shared lineage lookup's data.id contract. const client = { session: { get: async (input, options) => { const result = await ctx.session.get(input, options); return result && typeof result.id === "string" ? { data: result } : result; } } }; // Hot reload disposes plugins while turns continue. hooks = await OrcaOpenCodeStatusPlugin({ client, sessionsOutliveDispose: true }); if (!hooks || typeof hooks.event !== "function") return noop; const promptRegistration = await ctx.session.hook("prompt", async (properties) => { await hooks.event({ event: { type: "session.next.prompt.admitted", properties } }); }); const consume = async () => { for await (const input of ctx.event.subscribe({ signal: controller.signal })) { if (controller.signal.aborted) break; const translated = translateOpenCode2Event(input.type, input.data); if (!translated) continue; await hooks.event({ event: translated }); } }; const consuming = consume().catch((error) => { if (!controller.signal.aborted) console.warn("[orca-hook] event subscription failed:", error.message); }); return async () => { try { controller.abort(); // Finish every owner's cleanup even if an earlier disposer fails. try { await promptRegistration?.dispose?.(); } finally { try { await consuming; } finally { await hooks.dispose?.(); } } } catch { // Cleanup failures must not fail plugin unload. } }; } catch { try { await hooks?.dispose?.(); } catch {} return noop; } } ` .split('\n') .concat(getOpenCode2TuiSource()) } export function getOpenCode2EventNormalizationSource(): string[] { return [ '', 'function normalizeNextLifecycleEvent(event) {', ' if (!event || typeof event.type !== "string") return event;', ' const properties = event.properties || {};', ' if (event.type === "permission.v2.asked") return { ...event, type: "permission.asked", properties: { ...properties, id: properties.id, permission: properties.action, patterns: properties.resources } };', ' if (event.type === "permission.v2.replied") return { ...event, type: "permission.replied", properties: { ...properties } };', ' if (event.type === "question.v2.asked") return { ...event, type: "question.asked", properties: { ...properties } };', ' if (event.type === "question.v2.replied") return { ...event, type: "question.replied", properties: { ...properties } };', ' if (event.type === "question.v2.rejected") return { ...event, type: "question.rejected", properties: { ...properties } };', ' if (event.type === "session.next.step.started" || event.type === "session.next.tool.called" || event.type === "session.next.tool.progress" || event.type === "session.next.retried") {', ' return { ...event, type: "session.status", properties: { ...properties, status: { type: "busy" } } };', ' }', ' return event;', '}', '' ] }