Files
orca/src/main/opencode2/status-plugin-setup-source.ts
T
NeilandClaude 5dd5294294 perf(orcad): keep the server bundle ASCII so V8 stores it at half the size (#31495)
* perf(orcad): keep the server bundle ASCII so V8 stores it one byte per character

129 non-ASCII characters in regex literals (and three in a String.raw plugin
template) made V8 hold the whole ~11 MB orcad-server.js source as a two-byte
string. Write them as \uXXXX escapes and fail the build if any return.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(orcad): keep license comments ASCII and name the module the guard trips on

esbuild gathers legal comments at the end of the bundle; escape that comment so
a dependency's license text cannot fail the ASCII guard, while licenses still
ship with the code. The guard now reports the offset and, from an unminified
rebuild, the module the character came from. A dark-switch build is covered.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-21 04:06:49 -07:00

173 lines
8.6 KiB
TypeScript

import { getOpenCode2TuiSource } from './status-plugin-tui-source'
import { openCodeTuiPluginDirName } from '../../shared/opencode-tui-plugin-install'
import { getLegacyOpenCodeTuiSource } from '../opencode/status-plugin-legacy-tui-source'
/** The OpenCode 2 entry points (server setup and TUI reporter) plus the constants they share. */
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()
]
}
// Interpolated so the server bundle stays ASCII; String.raw keeps the generated source's bytes.
const EM_DASH = '\u2014'
export function getOpenCode2SetupSource(): string[] {
return String.raw`
// Why: OpenCode owns a form under a session id, and Orca retires a blocker when
// that session goes idle. An owner that is not a real session has no idle, so a
// blocker minted for it can only ever be retired by an exact reply ${EM_DASH} add an id
// here to drop forms Orca could otherwise strand. OpenCode's own schema calls
// "global" a temporary MCP-elicitation sentinel it intends to replace with real
// session ids; when it does, this set stops matching and those forms block.
const NON_SESSION_FORM_OWNERS = new Set(["global"]);
// Why one translation: the TUI reporter builds its Needs input payloads with it, so a blocker
// reaches Orca in the same shape whichever process reported it.
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;
// Why: block on every form whose owner is a real session. "metadata" is
// optional in OpenCode's schema and its "kind" is a convention no
// producer is obliged to stamp, so an unknown shape must surface a
// blocker the user can clear rather than vanish while OpenCode waits.
if (!form || NON_SESSION_FORM_OWNERS.has(form.sessionID)) return null;
// A malformed form must not throw: that would 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") {
// A resolution for an ignored form is inert: the blocker key carries the
// form id, so it simply matches nothing.
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 };
}
// Why: every OpenCode 2 server runs as a "serve" process (the shared service or a
// --standalone child) whose env names only the pane that spawned it, so each full TUI
// reports its own pane through the TUI copy of this plugin instead.
async function tuiReportsPaneLifecycle() {
if (!process.argv.includes("serve")) return false;
try {
const { statSync } = await import("node:fs");
// Why: an installer that predates the TUI copy (e.g. an older SSH relay) writes only
// this file; without the TUI copy nothing else would report, so keep reporting.
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;
// Why: OpenCode may probe setup() with no context during startup, and the setup
// API shape can drift between releases. Never throw from setup ${EM_DASH} a throw surfaces
// as an 'orca-opencode-status' plugin failed error in the TUI, which is worse
// than silently running without status reporting.
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();
// Why the envelope: OpenCode 2's plugin adapter unwraps a single-property
// { data } success schema, so ctx.session.get resolves to the bare record ${EM_DASH}
// but the shared lineage lookup only accepts result?.data?.id === sessionID.
// Without it, resolveRootSessionID returns null for every session and a
// subagent's work publishes as if it were the root's.
const client = { session: { get: async (input, options) => { const result = await ctx.session.get(input, options); return result && typeof result.id === "string" ? { data: result } : result; } } };
// Why: this host disposes plugins on a hot reload while turns keep running.
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();
// Each owner must finish cleanup even when an earlier disposer rejects.
try {
await promptRegistration?.dispose?.();
} finally {
try {
await consuming;
} finally {
await hooks.dispose?.();
}
}
} catch {
// Why: cleanup runs during plugin unload; a throw here also fails the plugin.
}
};
} 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;',
'}',
''
]
}