export function getStatusPluginFactorySource(options: { emitSessionStart: boolean }): string[] { return [ '// Why: accept the factory argument as an optional opaque parameter instead', '// of destructuring (`async ({ client }) => …`). OpenCode can invoke the', '// plugin factory with undefined during startup, which makes the', '// destructuring form throw synchronously and crash OpenCode with an opaque', '// UnknownError before any event is ever dispatched.', 'export const OrcaOpenCodeStatusPlugin = async (_ctx) => {', ' const client = _ctx?.client;', ' const factoryID = ++nextFactoryID;', ' activeFactoryIDs.add(factoryID);', ' let disposed = false;', ' return {', ' event: async ({ event }) => {', ' if (disposed || !event?.type) return;', ' const authorityRevision = stateArrivalRevision;', ' const statusType = getStatusType(event);', '', ' // Why: cache the message role BEFORE the async isChildSession check.', ' // OpenCode fires message.updated (user) and message.part.updated (text)', ' // back-to-back; if we awaited isChildSession first, the part.updated', ' // handler could reach messageRoleById.get(...) while the user message.updated', ' // is still suspended on that await — so the part would see an empty cache', ' // and drop the user prompt. Caching is a cheap Map.set with bounded size,', ' // safe to run even for child sessions (the part POST still filters them).', ' if (event.type === "message.updated") {', ' const info = event.properties && event.properties.info;', ' rememberMessageRole(info && info.id, info && info.role);', ' }', '', ' const sessionID = event.properties?.sessionID;', ' const updatedPart = event.properties?.part;', ...(options.emitSessionStart ? [ ' if (event.type === "session.created") {', ' const info = event.properties?.info;', ' if (!info?.id || info.parentID) return;', ' rememberSessionRoot(info.id, info.id);', ' await enqueueLifecycle(() =>', ' disposed ? undefined : post("SessionStart", { sessionID: info.id })', ' );', ' return;', ' }', '' ] : []), ' if (', ' event.type === "message.part.updated" &&', ' updatedPart?.type === "tool" &&', ' updatedPart.tool === "question" &&', ' (updatedPart.state?.status === "completed" || updatedPart.state?.status === "error")', ' ) {', ' await enqueueLifecycle(async () => {', ' if (disposed) return;', ' // Why: stored ownership clears child questions without waiting on', ' // ancestry lookup even though ordinary child message parts stay hidden.', ' const rootSessionID = clearQuestionForToolPart(updatedPart, sessionID, factoryID);', ' if (!rootSessionID) return;', ' await publishOwnershipChange(factoryID, rootSessionID);', ' });', ' return;', ' }', '', ' if (', ' event.type === "session.status" ||', ' event.type === "session.idle" ||', ' event.type === "session.error" ||', ' event.type === "permission.asked" ||', ' event.type === "question.asked" ||', ' event.type === "permission.replied" ||', ' event.type === "question.replied" ||', ' event.type === "question.rejected"', ' ) {', ' await enqueueLifecycle(() =>', ' disposed ? undefined : handleLifecycleEvent(client, event, factoryID)', ' );', ' return;', ' }', '', ' if (event.type === "message.part.delta") {', ' const properties = event.properties || {};', ' if (', ' properties.field === "text" &&', ' typeof properties.delta === "string" &&', ' properties.delta.length > 0', ' ) {', ' await recoverBusyFromDelta(client, sessionID, factoryID);', ' }', ' return;', ' }', '', ' if (sessionID && (await isChildSession(client, sessionID)) !== false) {', ' return;', ' }', ' if (disposed) return;', ' if (authorityRevision !== stateArrivalRevision) return;', ' if (desiredStatus === "waiting") return;', '', ' if (event.type === "message.updated") {', ' // Why: role is already cached above the isChildSession await so the', ' // back-to-back message.part.updated for the same messageID is not', ' // racing against this handler. Nothing more to do here — return to', ' // avoid falling through to the part/session handlers below.', ' return;', ' }', '', ' if (event.type === "message.part.updated") {', ' // Why: a TextPart carries the actual user prompt or assistant reply', ' // text. Skip non-text parts (tool, reasoning, file, …) so we only', ' // forward what the dashboard renders. Role came from the earlier', ' // message.updated event; if we never saw one (e.g. plugin loaded', ' // mid-turn) the role is unknown, and mislabeling the part — a user', ' // prompt displayed as the assistant reply, or vice versa — is worse', ' // than silently dropping a single in-flight text chunk. The next', ' // message.updated event will re-seed the role cache, so subsequent', ' // parts in the same session flow normally.', ' const part = event.properties && event.properties.part;', ' if (!part || part.type !== "text" || !part.text) return;', ' // Why: OpenCode injects a finished background task back into the', ' // parent turn as a synthetic `` text part. It is machinery,', ' // not what the human typed or the agent replied — OpenCode hides it', ' // from its own prompt too — so it must not replace the pane preview.', ' if (part.synthetic === true) return;', ' const role = messageRoleById.get(part.messageID);', ' if (!role) return;', ' if (role === "user") {', ' // Why: user prompts arrive as a single event, not a stream — post', ' // immediately (still capped) so the throttle slot stays free for', ' // the assistant reply that follows within the same window.', ' await postMessagePart(', ' { role, text: capMessagePartText(part.text), messageID: part.messageID, sessionID },', ' factoryID', ' );', ' return;', ' }', ' queueAssistantPart({', ' role,', ' text: part.text,', ' messageID: part.messageID,', ' sessionID,', ' authorityRevision,', ' factoryID,', ' });', ' return;', ' }', '', ' },', ' dispose: async () => {', ' if (disposed) return;', ' disposed = true;', ' disposingFactoryIDs.add(factoryID);', ' await enqueueLifecycle(async () => {', ' // An older MessagePart must settle before disposal publishes the', ' // replacement state, or its late Working update could win.', ' while (messagePartPostInFlight) await messagePartPostInFlight;', ' for (const [sessionID, ownerID] of busyRootOwnerBySessionID) {', ' if (ownerID === factoryID) busyRootOwnerBySessionID.delete(sessionID);', ' }', ' for (const [key, provisional] of provisionalBusyByKey) {', ' if (provisional.factoryID === factoryID) provisionalBusyByKey.delete(key);', ' }', ' for (const [key, busyChild] of busyChildRootByKey) {', ' if (busyChild.factoryID === factoryID) busyChildRootByKey.delete(key);', ' }', ' for (const [key, attention] of pendingAttentionByKey) {', ' if (attention.factoryID === factoryID) pendingAttentionByKey.delete(key);', ' }', ' if (pendingAssistantPart?.factoryID === factoryID) {', ' if (assistantPartFlushTimer) clearTimeout(assistantPartFlushTimer);', ' assistantPartFlushTimer = null;', ' pendingAssistantPart = null;', ' }', ' const ownsDeliveredMessagePart = deliveredMessagePartFactoryID === factoryID;', ' if (desiredFactoryID === factoryID || ownsDeliveredMessagePart) {', ' clearStatusRetry();', ' statusRevision += 1;', ' // A MessagePart may have changed the listener to Working after the', ' // same lifecycle key was delivered; force that key to be reasserted.', ' statusDeliveryDirty = ownsDeliveredMessagePart;', ' busyRecoveryUsed = false;', ' busyRecoveryEndpointKey = "";', ' const fallbackFactoryID = Array.from(activeFactoryIDs).find(', ' (id) => id !== factoryID', ' );', ' if (fallbackFactoryID !== undefined) {', ' await publishAggregateStatus(', ' fallbackFactoryID,', ' desiredStatusProperties?.sessionID', ' );', ' } else {', ' // Why: Instance disposal can happen while the PTY stays alive;', ' // publish a final idle so Orca does not retain a dead owner.', ' if (!deliveredStatusKey.startsWith("idle:") || ownsDeliveredMessagePart) {', ' await setStatus(', ' "idle",', ' { sessionID: desiredStatusProperties?.sessionID },', ' factoryID', ' );', ' }', ' clearStatusRetry();', ' desiredStatus = "idle";', ' desiredHookEventName = "SessionIdle";', ' desiredStatusKey = "idle:";', ' desiredStatusProperties = {};', ' desiredFactoryID = null;', ' }', ' }', ' activeFactoryIDs.delete(factoryID);', ' disposingFactoryIDs.delete(factoryID);', ' });', ' },', ' };', '};', '', '// Why: OpenCode also resolves plugins through the module default export, and that', '// loader rejects the module unless the default exposes `server()` ("must default', '// export an object with server()"). `setup()` does not satisfy it. Keep the named', '// export so the factory-based loader still finds the same instance.', 'export default {', ' id: "orca-opencode-status",', ' server: OrcaOpenCodeStatusPlugin,', '};', '' ] }