@khalilgharbaoui/opencode-claude-code-plugin 0.55.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/index.d.ts +121 -0
- package/dist/index.js +2798 -1593
- package/dist/index.js.map +1 -1
- package/dist/tui.js +206 -8
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +42 -3
package/dist/tui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tui.ts","../src/subagent-sidebar.ts","../src/spawn-record-store.ts"],"sourcesContent":["/**\n * The TUI half of the package: a `Subagents` section at the bottom of\n * opencode's session sidebar listing the current session's subagents, running\n * first, each with a spinner (or ✓ / ✗), its agent type, its task description,\n * a `bg` marker for background ones, and a muted line saying what it really\n * runs as (`sonnet-5.5 · high · alpha`). Clicking a row opens that subagent's\n * session; opencode's own `up` key goes back to the parent.\n *\n * Always on: there is no option. One module serves both majors (h #g234):\n *\n * - opencode 1.x loads TUI plugins from `tui.json`'s `plugin` list, never\n * from `opencode.json`'s, resolves a package to `exports[\"./tui\"]`, and\n * wants a default export `{ id, tui }` with no `server`.\n * - opencode 2.x resolves the SAME `plugins` entry it already has\n * (`<repo>/dist`) to `<dir>/tui`, and wants `{ id, setup }`. 1.x ignores a\n * `setup` key and 2.x a `tui` key, so the default export carries both.\n *\n * Everything that decides WHAT to show lives in `src/subagent-sidebar.ts`\n * (pure, tested). Each major gets a thin adapter that gathers that module's\n * inputs from its own TUI state and supplies its own theme colours; the\n * section itself is drawn by one renderer. Nothing in here may throw into the\n * host or log: on the TUI's own thread stderr is the screen (h #g193), so every\n * failure degrades to drawing less.\n */\nimport { RGBA } from \"@opentui/core\"\nimport { createElement, createTextNode, effect, insert, insertNode, setProp } from \"@opentui/solid\"\nimport { createMemo, createSignal, onCleanup } from \"solid-js\"\nimport {\n SIDEBAR_ROW_WIDTH,\n SPINNER_FRAMES,\n collapsedSummary,\n completionsIn,\n deriveSubagentRows,\n layoutRow,\n partChildID,\n type ChildRunState,\n type ChildSessionInput,\n type CompletionInput,\n type Segment,\n type SidebarInput,\n type SubagentRow,\n type TaskPartInput,\n} from \"./subagent-sidebar.js\"\nimport { createSpawnRecordReader, type SpawnRecord } from \"./spawn-record-store.js\"\n\nexport const SUBAGENTS_TUI_PLUGIN_ID = \"opencode-claude-code-plugin.subagents\"\n\n/** After opencode 1.x's own sidebar sections (files is 500). */\nconst SECTION_ORDER = 900\nconst SPINNER_INTERVAL_MS = 100\n/** How often the spawn records and the finished rows' fade are rechecked. */\nconst REFRESH_INTERVAL_MS = 1500\n\n/** The tool names a subagent dispatch has: `task` on 1.x, `subagent` on 2.x. */\nconst TASK_TOOLS = new Set([\"task\", \"subagent\"])\n\ntype Color = RGBA\n\n/** The colours the section paints with, whatever theme shape the host has. */\nexport interface SidebarColors {\n text: Color\n muted: Color\n spinner: Color\n done: Color\n error: Color\n /** The effort on a row's second line: the theme's warning colour. */\n effort: Color\n effortFaded: Color\n hover: Color\n}\n\n/** What one major has to supply; the renderer below does everything else. */\ninterface SidebarHost {\n colors(): SidebarColors\n /** The inputs for one parent session, minus the clock and the records. */\n gather(parentID: string): Pick<SidebarInput, \"children\" | \"parts\" | \"completions\" | \"status\" | \"stoppedAt\">\n navigate(sessionID: string): void\n}\n\nfunction attempt<T>(fn: () => T, fallback: T): T {\n try {\n return fn()\n } catch {\n return fallback\n }\n}\n\n/**\n * `a` moved `weight` of the way to `b`, so a finished glyph reads as muted.\n * Works in whatever scale the host's RGBA uses, because it only ever hands\n * the class back values it read from the same class.\n */\nfunction mix(a: Color, b: Color, weight: number): Color {\n return attempt(\n () =>\n RGBA.fromValues(\n a.r + (b.r - a.r) * weight,\n a.g + (b.g - a.g) * weight,\n a.b + (b.b - a.b) * weight,\n a.a,\n ),\n a,\n )\n}\n\nfunction roleColor(colors: SidebarColors, role: Segment[\"role\"]): Color {\n switch (role) {\n case \"spinner\":\n return colors.spinner\n case \"done\":\n return colors.done\n case \"error\":\n return colors.error\n case \"effort\":\n return colors.effort\n case \"effort-faded\":\n return colors.effortFaded\n case \"agent\":\n case \"description\":\n return colors.text\n default:\n return colors.muted\n }\n}\n\n/**\n * The section, its clocks and the spawn-record reader, for one host. Returns\n * the slot renderer and a disposer.\n */\nfunction createSidebar(host: SidebarHost) {\n const [frame, setFrame] = createSignal(0)\n const [now, setNow] = createSignal(Date.now())\n const records = createSpawnRecordReader()\n let recordMap: Map<string, SpawnRecord> = attempt(() => records.read(), new Map())\n const [recordVersion, setRecordVersion] = createSignal(0)\n\n // One spinner clock and one refresh clock for every mounted section, each\n // running only while something needs it: nothing ticks while no subagent is\n // listed, and the spinner stops the moment the last one finishes.\n const sectionsRunning = new Set<symbol>()\n const sectionsShowing = new Set<symbol>()\n let spinner: ReturnType<typeof setInterval> | undefined\n let refresh: ReturnType<typeof setInterval> | undefined\n const syncClocks = () => {\n if (sectionsRunning.size > 0 && !spinner) {\n spinner = setInterval(() => setFrame((value) => (value + 1) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS)\n } else if (sectionsRunning.size === 0 && spinner) {\n clearInterval(spinner)\n spinner = undefined\n }\n if (sectionsShowing.size > 0 && !refresh) {\n refresh = setInterval(() => {\n const next = attempt(() => records.read(), recordMap)\n if (next !== recordMap) {\n recordMap = next\n setRecordVersion((value) => value + 1)\n }\n setNow(Date.now())\n }, REFRESH_INTERVAL_MS)\n } else if (sectionsShowing.size === 0 && refresh) {\n clearInterval(refresh)\n refresh = undefined\n }\n }\n\n const deriveRows = (parentID: string): SubagentRow[] => {\n recordVersion()\n return deriveSubagentRows({\n ...host.gather(parentID),\n record: (id) => recordMap.get(id),\n now: now(),\n })\n }\n\n const buildRow = (row: SubagentRow): OpentuiNode => {\n const box = createElement(\"box\")\n const layout = layoutRow(row, SIDEBAR_ROW_WIDTH)\n const first = createElement(\"text\")\n setProp(first, \"wrapMode\", \"none\")\n layout.first.forEach((segment, index) => {\n const span = createElement(\"span\")\n if (index === 0 && row.state === \"running\") {\n insert(span, () => SPINNER_FRAMES[frame() % SPINNER_FRAMES.length])\n } else {\n insertNode(span, createTextNode(segment.text))\n }\n effect(() => setProp(span, \"style\", { fg: roleColor(host.colors(), segment.role) }))\n insertNode(first, span)\n })\n insertNode(box, first)\n if (layout.second.length > 0) {\n // One line of spans, like the first, so the effort can take its own colour.\n const second = createElement(\"text\")\n setProp(second, \"wrapMode\", \"none\")\n for (const segment of layout.second) {\n const span = createElement(\"span\")\n insertNode(span, createTextNode(segment.text))\n effect(() => setProp(span, \"style\", { fg: roleColor(host.colors(), segment.role) }))\n insertNode(second, span)\n }\n insertNode(box, second)\n }\n if (row.sessionID) {\n const sessionID = row.sessionID\n setProp(box, \"onMouseDown\", () => attempt(() => host.navigate(sessionID), undefined))\n setProp(box, \"onMouseOver\", () =>\n attempt(() => setProp(box, \"backgroundColor\", host.colors().hover), undefined),\n )\n setProp(box, \"onMouseOut\", () =>\n attempt(() => setProp(box, \"backgroundColor\", \"transparent\"), undefined),\n )\n }\n return box\n }\n\n const buildSection = (\n list: SubagentRow[],\n expanded: () => boolean,\n setExpanded: (next: (value: boolean) => boolean) => boolean,\n ): OpentuiNode => {\n const section = createElement(\"box\")\n // Heading, styled like opencode 1.x's own MCP and Todo sections: a bold\n // title, and with more than two rows a caret that folds the list away\n // behind a one-line summary.\n const header = createElement(\"box\")\n setProp(header, \"flexDirection\", \"row\")\n setProp(header, \"gap\", 1)\n setProp(header, \"onMouseDown\", () => list.length > 2 && setExpanded((value) => !value))\n if (list.length > 2) {\n const caret = createElement(\"text\")\n insert(caret, () => (expanded() ? \"▼\" : \"▶\"))\n effect(() => setProp(caret, \"fg\", host.colors().text))\n insertNode(header, caret)\n }\n const title = createElement(\"text\")\n const bold = createElement(\"b\")\n insertNode(bold, createTextNode(\"Subagents\"))\n insertNode(title, bold)\n const summary = createElement(\"span\")\n insert(summary, () => (expanded() ? \"\" : ` (${collapsedSummary(list)})`))\n effect(() => setProp(summary, \"style\", { fg: host.colors().muted }))\n insertNode(title, summary)\n effect(() => setProp(title, \"fg\", host.colors().text))\n insertNode(header, title)\n insertNode(section, header)\n\n const body = createElement(\"box\")\n insert(body, () => (expanded() || list.length <= 2 ? list.map(buildRow) : null))\n insertNode(section, body)\n return section\n }\n\n const render = (sessionID: () => string | undefined): OpentuiNode => {\n const root = createElement(\"box\")\n const key = Symbol(\"subagents-section\")\n const [expanded, setExpanded] = createSignal(true)\n const rows = createMemo(() => {\n const parentID = sessionID()\n if (!parentID) return [] as SubagentRow[]\n return attempt(() => deriveRows(parentID), [] as SubagentRow[])\n })\n effect(() => {\n const list = rows()\n if (list.some((row) => row.state === \"running\")) sectionsRunning.add(key)\n else sectionsRunning.delete(key)\n if (list.length > 0) sectionsShowing.add(key)\n else sectionsShowing.delete(key)\n syncClocks()\n })\n onCleanup(() => {\n sectionsRunning.delete(key)\n sectionsShowing.delete(key)\n syncClocks()\n })\n insert(root, () => {\n const list = rows()\n if (list.length === 0) return null\n return attempt(() => buildSection(list, expanded, setExpanded), null)\n })\n return root\n }\n\n const dispose = () => {\n sectionsRunning.clear()\n sectionsShowing.clear()\n syncClocks()\n }\n return { render, dispose }\n}\n\n// ---------------------------------------------------------------------------\n// opencode 1.x\n// ---------------------------------------------------------------------------\n\n// The structural slice of opencode 1.x's `TuiPluginApi` this module reads.\n// Hand-written, like `src/opencode-types.ts`, so nothing is imported from\n// `@opencode-ai/plugin`.\ninterface V1SessionInfo {\n id: string\n parentID?: string\n title?: string\n agent?: string\n time?: { created?: number; updated?: number }\n}\ninterface V1Part {\n type: string\n tool?: string\n synthetic?: boolean\n text?: string\n state?: {\n status?: string\n input?: Record<string, unknown>\n metadata?: Record<string, unknown>\n title?: string\n time?: { start?: number; end?: number }\n }\n}\ninterface V1Api {\n state: {\n session: {\n get(sessionID: string): V1SessionInfo | undefined\n messages(sessionID: string): ReadonlyArray<{ id: string; role: string; time?: { created?: number } }> | undefined\n status(sessionID: string): { type?: string } | undefined\n }\n part(messageID: string): ReadonlyArray<V1Part> | undefined\n }\n theme: {\n current: {\n text: Color\n textMuted: Color\n accent: Color\n success: Color\n warning: Color\n error: Color\n backgroundElement: Color\n }\n }\n route: { navigate(name: string, params?: Record<string, unknown>): void }\n event: { on(type: string, handler: (event: { properties?: any }) => void): () => void }\n client?: { session?: { children?: (input: { sessionID: string }) => Promise<{ data?: V1SessionInfo[] }> } }\n slots: { register(plugin: { order?: number; slots: Record<string, (ctx: unknown, props: any) => OpentuiNode> }): string }\n lifecycle?: { onDispose(fn: () => void): () => void }\n}\n\nexport function registerV1(api: V1Api): void {\n // Child sessions seen per parent. 1.x has no session list in its TUI state,\n // so they come from its events, from a one-off `children` fetch the first\n // time a parent is shown (a TUI started after the dispatch still lists\n // them), and from finished parts' metadata.\n const childrenByParent = new Map<string, Map<string, ChildSessionInput>>()\n const fetched = new Set<string>()\n const stoppedAt = new Map<string, number>()\n const lastStatus = new Map<string, string>()\n const [version, setVersion] = createSignal(0)\n const bump = () => setVersion((value) => value + 1)\n\n const remember = (info: V1SessionInfo | undefined) => {\n if (!info?.id || !info.parentID) return false\n let children = childrenByParent.get(info.parentID)\n if (!children) {\n children = new Map()\n childrenByParent.set(info.parentID, children)\n }\n children.set(info.id, {\n id: info.id,\n title: info.title,\n agent: info.agent,\n created: info.time?.created,\n updated: info.time?.updated,\n })\n return true\n }\n\n const unsubscribe: Array<() => void> = []\n const listen = (type: string, handler: (properties: any) => void) => {\n const off = attempt(\n () => api.event.on(type, (event) => attempt(() => handler(event?.properties ?? {}), undefined)),\n undefined,\n )\n if (off) unsubscribe.push(off)\n }\n listen(\"session.created\", (properties) => remember(properties.info) && bump())\n listen(\"session.updated\", (properties) => remember(properties.info) && bump())\n listen(\"session.deleted\", (properties) => {\n const info = properties.info as V1SessionInfo | undefined\n if (info?.parentID && childrenByParent.get(info.parentID)?.delete(info.id)) bump()\n })\n listen(\"session.status\", (properties) => {\n const id = properties.sessionID as string | undefined\n const type = properties.status?.type as string | undefined\n if (!id || !type) return\n const previous = lastStatus.get(id)\n lastStatus.set(id, type)\n if (type === \"idle\" && previous && previous !== \"idle\") stoppedAt.set(id, Date.now())\n if (type !== \"idle\") stoppedAt.delete(id)\n })\n\n const fetchChildren = (parentID: string) => {\n if (fetched.has(parentID)) return\n fetched.add(parentID)\n const session = api.client?.session\n if (!session?.children) return\n void Promise.resolve()\n .then(() => session.children!.call(session, { sessionID: parentID }))\n .then((response) => {\n let changed = false\n for (const info of response?.data ?? []) changed = remember(info) || changed\n if (changed) bump()\n })\n .catch(() => undefined)\n }\n\n const host: SidebarHost = {\n colors() {\n const theme = api.theme.current\n return {\n text: theme.text,\n muted: theme.textMuted,\n spinner: theme.accent,\n done: mix(theme.success, theme.textMuted, 0.45),\n error: mix(theme.error, theme.textMuted, 0.35),\n effort: theme.warning ?? theme.textMuted,\n effortFaded: mix(theme.warning ?? theme.textMuted, theme.textMuted, 0.45),\n hover: theme.backgroundElement,\n }\n },\n gather(parentID) {\n version()\n fetchChildren(parentID)\n const parts: TaskPartInput[] = []\n const completions: CompletionInput[] = []\n for (const message of api.state.session.messages(parentID) ?? []) {\n const messageParts = api.state.part(message.id) ?? []\n if (message.role === \"assistant\") {\n for (const part of messageParts) {\n if (part.type !== \"tool\" || !TASK_TOOLS.has(part.tool ?? \"\")) continue\n const state = part.state ?? {}\n parts.push({\n status: state.status,\n input: state.input,\n metadata: state.metadata,\n title: state.title,\n start: state.time?.start,\n end: state.time?.end,\n })\n }\n } else if (message.role === \"user\") {\n // A background child's end is a synthetic user part in the parent.\n for (const part of messageParts) {\n if (part.type !== \"text\" || part.synthetic !== true || !part.text) continue\n completions.push(...completionsIn(part.text, message.time?.created))\n }\n }\n }\n const known = new Map(childrenByParent.get(parentID) ?? [])\n for (const part of parts) {\n const id = partChildID(part)\n if (!id || known.has(id)) continue\n const info = attempt(() => api.state.session.get(id), undefined)\n known.set(id, {\n id,\n title: info?.title,\n agent: info?.agent,\n created: info?.time?.created ?? part.start,\n updated: info?.time?.updated,\n })\n }\n return {\n children: [...known.values()],\n parts,\n completions,\n status: (id: string): ChildRunState => {\n const type = attempt(() => api.state.session.status(id)?.type, undefined)\n if (type === \"busy\" || type === \"retry\") return \"busy\"\n if (type === \"idle\") return \"idle\"\n return \"unknown\"\n },\n stoppedAt: (id: string) => stoppedAt.get(id),\n }\n },\n navigate(sessionID) {\n api.route.navigate(\"session\", { sessionID })\n },\n }\n\n const sidebar = createSidebar(host)\n api.slots.register({\n order: SECTION_ORDER,\n slots: {\n sidebar_content(_ctx: unknown, props: { session_id: string }) {\n return sidebar.render(() => props.session_id)\n },\n },\n })\n api.lifecycle?.onDispose(() => {\n for (const off of unsubscribe) attempt(off, undefined)\n sidebar.dispose()\n })\n}\n\n// ---------------------------------------------------------------------------\n// opencode 2.x\n// ---------------------------------------------------------------------------\n\n// The structural slice of opencode 2.x's TUI `Context` this module reads,\n// measured on 2.0.22 (h #g234). Its sessions carry `parentID`, its status is\n// `\"running\" | \"idle\"`, an assistant message holds its tool calls inline as\n// `content[]` items named `subagent` (with `metadata.sessionID` set while the\n// call still runs, unlike 1.x), and a background child's end is a message of\n// type `synthetic` whose text is `<subagent sessionID=\"...\" state=\"...\">`.\ninterface V2Session {\n id: string\n parentID?: string\n title?: string\n agent?: string\n time?: { created?: number; updated?: number }\n}\ninterface V2Message {\n type?: string\n text?: string\n time?: { created?: number }\n content?: Array<{\n type?: string\n name?: string\n time?: { created?: number; completed?: number }\n state?: { status?: string; input?: Record<string, unknown>; metadata?: Record<string, unknown> }\n }>\n}\ntype V2Shade = Record<string, Color>\ninterface V2Context {\n data: {\n session: {\n list(): V2Session[] | undefined\n status(sessionID: string): string\n message: { list(sessionID: string): V2Message[] | undefined }\n }\n }\n theme: {\n text: { base: Color; muted: Color; feedback: { success: V2Shade; warning?: V2Shade; error: V2Shade } }\n background: { raised: { base: Color } }\n hue: { accent: V2Shade }\n }\n themeMode?: \"dark\" | \"light\"\n ui: {\n router: { navigate(destination: { type: \"session\"; sessionID: string }): void }\n slot(claim: { append: string; render: (input: { sessionID: string }) => OpentuiNode }): () => void\n }\n}\n\nexport function setupV2(context: V2Context): () => void {\n const host: SidebarHost = {\n colors() {\n const theme = context.theme\n const muted = theme.text.muted\n // The accent hue at the shade that reads on this mode's background.\n const accent = theme.hue.accent[context.themeMode === \"light\" ? \"500\" : \"200\"] ?? theme.text.base\n return {\n text: theme.text.base,\n muted,\n spinner: accent,\n done: mix(theme.text.feedback.success.muted ?? theme.text.feedback.success.base, muted, 0.45),\n error: mix(theme.text.feedback.error.muted ?? theme.text.feedback.error.base, muted, 0.35),\n effort: theme.text.feedback.warning?.base ?? muted,\n effortFaded: mix(theme.text.feedback.warning?.base ?? muted, muted, 0.45),\n hover: theme.background.raised.base,\n }\n },\n gather(parentID) {\n const children: ChildSessionInput[] = (context.data.session.list() ?? [])\n .filter((session) => session.parentID === parentID)\n .map((session) => ({\n id: session.id,\n title: session.title,\n agent: session.agent,\n created: session.time?.created,\n updated: session.time?.updated,\n }))\n const parts: TaskPartInput[] = []\n const completions: CompletionInput[] = []\n for (const message of context.data.session.message.list(parentID) ?? []) {\n if (message.type === \"assistant\") {\n for (const item of message.content ?? []) {\n if (item.type !== \"tool\" || !TASK_TOOLS.has(item.name ?? \"\")) continue\n parts.push({\n status: item.state?.status,\n input: item.state?.input,\n metadata: item.state?.metadata,\n start: item.time?.created,\n end: item.time?.completed,\n })\n }\n } else if (message.type === \"synthetic\" && message.text) {\n completions.push(...completionsIn(message.text, message.time?.created))\n }\n }\n return {\n children,\n parts,\n completions,\n status: (id: string): ChildRunState => {\n const status = attempt(() => context.data.session.status(id), undefined)\n if (status === \"running\") return \"busy\"\n if (status === \"idle\") return \"idle\"\n return \"unknown\"\n },\n stoppedAt: () => undefined,\n }\n },\n navigate(sessionID) {\n context.ui.router.navigate({ type: \"session\", sessionID })\n },\n }\n\n const sidebar = createSidebar(host)\n const release = attempt(\n () =>\n context.ui.slot({\n append: \"sidebar.content\",\n render: (input) => sidebar.render(() => input.sessionID),\n }),\n undefined,\n )\n return () => {\n if (release) attempt(release, undefined)\n sidebar.dispose()\n }\n}\n\nconst tui = async (api: V1Api): Promise<void> => {\n attempt(() => registerV1(api), undefined)\n}\n\nconst setup = (context: V2Context): (() => void) | void =>\n attempt(() => setupV2(context), undefined)\n\nexport default { id: SUBAGENTS_TUI_PLUGIN_ID, tui, setup }\n","/**\n * The model behind the sidebar's `Subagents` section (`src/tui.ts`): which\n * subagents of one opencode session to show, in what state, in what order, and\n * the exact text of each row.\n *\n * Pure on purpose. It imports nothing, so the TUI bundle carries no logger (a\n * logger on the TUI's main thread writes to the terminal it is drawing on,\n * h #g193) and every rule here is testable without a terminal. The TUI half\n * only gathers the inputs and paints the segments this module returns.\n *\n * Measured on opencode 1.18.35 (h #g234), and each one shapes a rule below:\n *\n * - A RUNNING `task` part carries no `state.metadata` in the TUI's store, so\n * the child session id is not on the part until it completes. Rows are\n * built from the child SESSIONS (`parentID` is the current session) and a\n * part is matched to its child by the title opencode gives the child,\n * `<description> (@<agent> subagent)`.\n * - A background dispatch's part completes at once with a `<task id\n * state=\"running\">` envelope; the child's end arrives later as a synthetic\n * user part `<task id=\"ses_...\" state=\"completed\">` in the parent (opencode\n * 2 writes `<subagent sessionID=\"...\" state=\"...\">`). That envelope is the\n * authoritative end of a background child.\n * - `metadata.model` on a finished part is what opencode ASKED for, not what\n * ran: an agent's `forceModel`, its `reasoningEffort`, the dispatch form and\n * the fallback chain all change the spawn without telling opencode. So the\n * label prefers the spawn record the server half writes\n * (`src/spawn-record-store.ts`) and falls back to the part.\n */\n\nexport type SubagentState = \"running\" | \"done\" | \"error\"\n\n/** One child session as either major reports it. */\nexport interface ChildSessionInput {\n id: string\n title?: string\n agent?: string\n created?: number\n updated?: number\n}\n\n/** A `task` / `subagent` tool part in the parent, as either major reports it. */\nexport interface TaskPartInput {\n status?: string\n input?: Record<string, unknown>\n metadata?: Record<string, unknown>\n title?: string\n start?: number\n end?: number\n}\n\n/** A background child's end, read out of the parent's synthetic user parts. */\nexport interface CompletionInput {\n sessionID: string\n state: string\n at?: number\n}\n\n/** What the server half recorded about a session's real spawn. */\nexport interface SpawnRecordInput {\n model?: string\n effort?: string\n account?: string\n at?: number\n}\n\nexport type ChildRunState = \"busy\" | \"idle\" | \"unknown\"\n\nexport interface SidebarInput {\n children: ChildSessionInput[]\n parts: TaskPartInput[]\n completions: CompletionInput[]\n status: (sessionID: string) => ChildRunState\n record: (sessionID: string) => SpawnRecordInput | undefined\n /** When the TUI first saw a child stop, by session id. */\n stoppedAt: (sessionID: string) => number | undefined\n now: number\n}\n\nexport interface SubagentRow {\n /** The child session, absent while a dispatch has not created one yet. */\n sessionID?: string\n agent: string\n description: string\n background: boolean\n state: SubagentState\n /** `sonnet-5.5 · high · alpha`, empty when nothing is known. */\n label: string\n /** The effort piece of `label`, drawn in the theme's warning colour. */\n effort?: string\n started: number\n finished?: number\n}\n\n/** How long a finished subagent stays in the list, and how many of them. */\nexport const FINISHED_TTL_MS = 10 * 60_000\nexport const FINISHED_KEEP = 3\n/**\n * A background child that reads idle with no envelope yet is still starting\n * for this long: opencode creates the session before it marks it busy.\n */\nexport const STARTING_GRACE_MS = 10_000\n/** The usable width of a row in opencode 1.18.35's 42-column sidebar. */\nexport const SIDEBAR_ROW_WIDTH = 36\nexport const SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"]\n\nconst TITLE_SUFFIX = /\\s*\\(@([^)\\s]+) subagent\\)\\s*$/\n\n/** `Fix the form (@implementor subagent)` -> description and agent. */\nexport function parseChildTitle(title: string | undefined): { description: string; agent?: string } {\n const text = (title ?? \"\").trim()\n const match = TITLE_SUFFIX.exec(text)\n if (!match) return { description: text }\n return { description: text.slice(0, match.index).trim(), agent: match[1] }\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined\n}\n\n/** The child session a part names, on either major's metadata. */\nexport function partChildID(part: TaskPartInput): string | undefined {\n const metadata = part.metadata ?? {}\n return text(metadata.sessionId) ?? text(metadata.sessionID)\n}\n\nfunction partAgent(part: TaskPartInput): string | undefined {\n const input = part.input ?? {}\n return text(input.subagent_type) ?? text(input.agent)\n}\n\nfunction partDescription(part: TaskPartInput): string | undefined {\n return text(part.input?.description) ?? text(part.title)\n}\n\nfunction partBackground(part: TaskPartInput): boolean {\n return part.input?.background === true || part.metadata?.background === true\n}\n\nconst COMPLETION_ENVELOPE =\n /<(?:task\\s+id|subagent\\s+sessionID)=\"(ses_[A-Za-z0-9]+)\"\\s+state=\"([a-z_]+)\"/g\n\n/**\n * Every background end a text names, in order. Both majors' envelopes:\n * `<task id=\"ses_...\" state=\"completed\">` (1.x) and\n * `<subagent sessionID=\"ses_...\" state=\"completed\">` (2.x). `running` is the\n * dispatch's own answer, not an end, so it is skipped.\n */\nexport function completionsIn(value: string, at?: number): CompletionInput[] {\n const found: CompletionInput[] = []\n for (const match of value.matchAll(COMPLETION_ENVELOPE)) {\n if (match[2] === \"running\") continue\n found.push({ sessionID: match[1], state: match[2], at })\n }\n return found\n}\n\nfunction endState(state: string): SubagentState {\n return state === \"completed\" ? \"done\" : \"error\"\n}\n\n/**\n * `claude-opus-5-5` -> `opus-5.5`, `claude-sonnet-4-5-20250929@alpha` ->\n * `sonnet-4.5`, `claude-opus-5-5-fast` -> `opus-5.5 fast`,\n * `claude-opus-5[1m]` -> `opus-5 1m`. Anything that is not a Claude id is\n * returned without its account marker and otherwise untouched.\n */\nexport function shortModelName(modelID: string | undefined): string | undefined {\n let id = text(modelID)\n if (!id) return undefined\n const at = id.indexOf(\"@\")\n if (at > 0) id = id.slice(0, at)\n let suffix = \"\"\n const window = /\\[(\\d+[km])\\]$/i.exec(id)\n if (window) {\n suffix += ` ${window[1].toLowerCase()}`\n id = id.slice(0, window.index)\n }\n if (id.endsWith(\"-fast\")) {\n suffix = ` fast${suffix}`\n id = id.slice(0, -\"-fast\".length)\n }\n if (!id.startsWith(\"claude-\")) return `${id}${suffix}`\n id = id.slice(\"claude-\".length).replace(/-\\d{8}$/, \"\")\n const match = /^([a-z]+)-(\\d+)(?:-(\\d+))?$/.exec(id)\n if (!match) return `${id}${suffix}`\n const version = match[3] === undefined ? match[2] : `${match[2]}.${match[3]}`\n return `${match[1]}-${version}${suffix}`\n}\n\n/**\n * The account a provider id names on a multi-account install\n * (`claude-code-alpha` -> `alpha`). A bare `claude-code` (one account) and a\n * provider that is not this plugin's name none: there is nothing to tell apart.\n */\nexport function accountFromProvider(providerID: string | undefined): string | undefined {\n const id = text(providerID)\n if (!id?.startsWith(\"claude-code-\")) return undefined\n return id.slice(\"claude-code-\".length) || undefined\n}\n\n/** `sonnet-5.5 · high · alpha`; pieces that are not known are left out. */\nexport function subagentLabel(\n record: SpawnRecordInput | undefined,\n part: TaskPartInput | undefined,\n): string {\n const asked = part?.metadata?.model as { modelID?: unknown; providerID?: unknown } | undefined\n const askedModel = typeof asked?.modelID === \"string\" ? asked.modelID : undefined\n const askedProvider = typeof asked?.providerID === \"string\" ? asked.providerID : undefined\n const model = shortModelName(record?.model ?? askedModel)\n const effort = text(record?.effort)\n const account = record?.model ? text(record.account) : accountFromProvider(askedProvider)\n return [model, effort, account].filter(Boolean).join(\" · \")\n}\n\n/**\n * Cut `value` to `width` cells with a trailing ellipsis. Counts code points,\n * which is a cell each for the text a task description holds; a wide glyph\n * can overrun by one cell and is left to the renderer's own clipping.\n */\nexport function truncate(value: string, width: number): string {\n const chars = Array.from(value.replace(/\\s+/g, \" \").trim())\n if (width <= 0) return \"\"\n if (chars.length <= width) return chars.join(\"\")\n if (width === 1) return \"…\"\n return `${chars.slice(0, width - 1).join(\"\").trimEnd()}…`\n}\n\n/**\n * The rows for one session: running first (oldest first), then finished\n * (newest first), at most `FINISHED_KEEP` of those and none older than\n * `FINISHED_TTL_MS`. Empty when nothing is worth showing, which is what makes\n * the section disappear.\n */\nexport function deriveSubagentRows(input: SidebarInput): SubagentRow[] {\n const completions = new Map<string, CompletionInput>()\n for (const completion of input.completions) completions.set(completion.sessionID, completion)\n\n const byChild = new Map<string, TaskPartInput>()\n const unmatched: TaskPartInput[] = []\n for (const part of input.parts) {\n const child = partChildID(part)\n if (child) byChild.set(child, part)\n else unmatched.push(part)\n }\n\n const rows: SubagentRow[] = []\n const seen = new Set<string>()\n for (const child of input.children) {\n if (seen.has(child.id)) continue\n seen.add(child.id)\n const parsed = parseChildTitle(child.title)\n let part = byChild.get(child.id)\n if (!part) {\n // A running part has no child id yet: match it on the title opencode\n // gave the child, agent and description both.\n const index = unmatched.findIndex(\n (candidate) =>\n partDescription(candidate) === parsed.description &&\n (parsed.agent === undefined || partAgent(candidate) === parsed.agent),\n )\n if (index >= 0) part = unmatched.splice(index, 1)[0]\n }\n rows.push(rowFor(input, child, parsed, part, completions.get(child.id)))\n }\n // A dispatch opencode has not created a session for yet: shown, not clickable.\n for (const part of unmatched) {\n if (part.status !== \"pending\" && part.status !== \"running\") continue\n const description = partDescription(part)\n const agent = partAgent(part)\n if (!description && !agent) continue\n rows.push({\n agent: agent ?? \"subagent\",\n description: description ?? \"\",\n background: partBackground(part),\n state: \"running\",\n label: subagentLabel(undefined, part),\n started: part.start ?? input.now,\n })\n }\n\n const running = rows\n .filter((row) => row.state === \"running\")\n .sort((a, b) => a.started - b.started)\n const finished = rows\n .filter((row) => row.state !== \"running\")\n .filter((row) => input.now - (row.finished ?? row.started) <= FINISHED_TTL_MS)\n .sort((a, b) => (b.finished ?? b.started) - (a.finished ?? a.started))\n .slice(0, FINISHED_KEEP)\n return [...running, ...finished]\n}\n\nfunction rowFor(\n input: SidebarInput,\n child: ChildSessionInput,\n parsed: { description: string; agent?: string },\n part: TaskPartInput | undefined,\n completion: CompletionInput | undefined,\n): SubagentRow {\n const background = part ? partBackground(part) : false\n const started = part?.start ?? child.created ?? input.now\n const status = input.status(child.id)\n let state: SubagentState\n let finished: number | undefined\n if (part && !background && part.status !== \"pending\" && part.status !== \"running\") {\n state = part.status === \"completed\" ? \"done\" : \"error\"\n finished = part.end\n } else if (completion) {\n state = endState(completion.state)\n finished = completion.at\n } else if (status === \"busy\") {\n state = \"running\"\n } else if (part && !background) {\n // pending or running: the parent is still waiting on this child.\n state = \"running\"\n } else if (status === \"idle\" && input.now - started < STARTING_GRACE_MS) {\n state = \"running\"\n } else if (status === \"unknown\" && background) {\n // Nothing says it stopped: a background child keeps its spinner until\n // its envelope arrives or opencode reports it idle.\n state = \"running\"\n } else {\n state = \"done\"\n }\n if (state !== \"running\" && finished === undefined) {\n finished = input.stoppedAt(child.id) ?? child.updated ?? started\n }\n return {\n sessionID: child.id,\n agent: parsed.agent ?? (part ? partAgent(part) : undefined) ?? text(child.agent) ?? \"subagent\",\n description: (part ? partDescription(part) : undefined) ?? parsed.description,\n background,\n state,\n label: subagentLabel(input.record(child.id), part),\n effort: text(input.record(child.id)?.effort),\n started,\n finished,\n }\n}\n\n/** A run of text with the role the TUI maps onto a theme colour. */\nexport interface Segment {\n text: string\n role: \"spinner\" | \"done\" | \"error\" | \"agent\" | \"description\" | \"muted\" | \"effort\" | \"effort-faded\"\n}\n\n/**\n * The two lines of one row, laid out for `width` cells:\n *\n * ⠋ implementor Fix the dispatch f… bg\n * sonnet-5.5 · high · alpha\n *\n * The `bg` marker is right-aligned, the description takes what is left, and a\n * finished row is drawn in the muted role throughout apart from its glyph.\n */\nexport function layoutRow(\n row: SubagentRow,\n width: number = SIDEBAR_ROW_WIDTH,\n frame = 0,\n): { first: Segment[]; second: Segment[] } {\n const finished = row.state !== \"running\"\n const glyph =\n row.state === \"running\"\n ? SPINNER_FRAMES[frame % SPINNER_FRAMES.length]\n : row.state === \"done\"\n ? \"✓\"\n : \"✗\"\n const marker = row.background ? \"bg\" : \"\"\n const reserved = marker ? marker.length + 1 : 0\n const agentWidth = Math.max(1, Math.min(Array.from(row.agent).length, width - 2 - reserved - 6))\n const agent = truncate(row.agent, agentWidth)\n const room = width - 2 - Array.from(agent).length - 1 - reserved\n const description = room > 0 ? truncate(row.description, room) : \"\"\n const used = 2 + Array.from(agent).length + (description ? 1 + Array.from(description).length : 0)\n const first: Segment[] = [\n { text: glyph, role: row.state === \"running\" ? \"spinner\" : row.state === \"done\" ? \"done\" : \"error\" },\n { text: \" \", role: \"muted\" },\n { text: agent, role: finished ? \"muted\" : \"agent\" },\n ]\n if (description) {\n first.push({ text: \" \", role: \"muted\" })\n first.push({ text: description, role: finished ? \"muted\" : \"description\" })\n }\n if (marker) {\n first.push({ text: \" \".repeat(Math.max(1, width - used - marker.length)), role: \"muted\" })\n first.push({ text: marker, role: \"muted\" })\n }\n // The effort is the one piece in colour: it is what changes from one\n // subagent to the next and what costs the most, so it is the one to spot.\n const second: Segment[] = []\n if (row.label) {\n const pieces = truncate(row.label, width - 2).split(\" · \")\n const effortAt = row.effort ? pieces.indexOf(row.effort) : -1\n second.push({ text: \" \", role: \"muted\" })\n pieces.forEach((piece, index) => {\n if (index > 0) second.push({ text: \" · \", role: \"muted\" })\n const role = index !== effortAt ? \"muted\" : finished ? \"effort-faded\" : \"effort\"\n second.push({ text: piece, role })\n })\n }\n return { first, second }\n}\n\n/** The collapsed heading's count: `2 running` or `3 done`. */\nexport function collapsedSummary(rows: SubagentRow[]): string {\n const running = rows.filter((row) => row.state === \"running\").length\n return running > 0 ? `${running} running` : `${rows.length} done`\n}\n","/**\n * What each opencode session's `claude` process REALLY spawned as: model,\n * effort and account. Written by the server half on every turn that reaches\n * the CLI, read by the TUI half's `Subagents` sidebar section (`src/tui.ts`).\n *\n * ## Why a file, and why the TUI cannot read this anywhere else\n *\n * Measured on opencode 1.18.35 (h #g234): an `implementor` subagent whose\n * agent file says `forceModel: claude-sonnet-5-5` and `reasoningEffort: high`\n * spawned as exactly that, while the finished `task` part's `metadata.model`\n * said `claude-code-default/claude-opus-5-5` and no message carried an effort\n * at all. Everything that changes a spawn on the plugin side (an agent's\n * `forceModel` and `reasoningEffort`, `defaultSubagentModel`, the dispatch form\n * of h #g227, the fallback chain of h #g162, an account failover) is invisible\n * to opencode by construction, so showing opencode's own view would be showing\n * the wrong model exactly when the operator configured it not to be. The two\n * halves do not share memory on either major (1.x runs the server half in a\n * worker thread, 2.x in a subprocess), so the record goes through the state\n * directory the resume and dispatch stores already use.\n *\n * ## Shape, and what is never written\n *\n * ```json\n * { \"version\": 1, \"sessions\": {\n * \"ses_abc\": { \"model\": \"claude-sonnet-5-5\", \"effort\": \"high\", \"account\": \"alpha\", \"at\": 1760000000000 } } }\n * ```\n *\n * A model id, an effort level and an account NAME: the same three strings the\n * dispatch store keeps (h #g228), none secret. No prompt, no path, no session\n * key. `account` is written only on a multi-account install, where it tells\n * two configured accounts apart; on a single-account install there is nothing\n * to tell apart and it stays out.\n *\n * ## Cost and failure\n *\n * One write per session per process, and another only when what it spawns as\n * changes: `recordSessionSpawn` compares against what this process last wrote\n * and returns before touching the disk otherwise. Same discipline as the\n * resume store (h #g197): merge-on-write so two opencode processes do not erase\n * each other, atomic rename, `0600` in the `0700` directory, bounded at\n * `MAX_SPAWN_RECORDS` newest sessions and `SPAWN_RECORD_MAX_AGE_MS`. A missing,\n * unreadable or half-written file is an empty store, which costs the sidebar\n * its effort and falls back to the model opencode asked for. Two writers racing\n * can drop one entry; the next turn of that session writes it again, and the\n * only thing it ever costs is a label.\n *\n * Imports nothing that logs, because the TUI bundle reads it on the terminal's\n * own thread (h #g193): every function here is silent and never throws.\n */\nimport { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\nexport interface SpawnRecord {\n model?: string\n effort?: string\n account?: string\n at: number\n}\n\n/** Sessions worth remembering at once. A subagent fan-out names a handful. */\nexport const MAX_SPAWN_RECORDS = 256\n\n/** A session not spawned this long ago is not one the sidebar is showing. */\nexport const SPAWN_RECORD_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000\n\nlet pathOverride: string | null = null\nconst lastWritten = new Map<string, string>()\n\nexport function spawnRecordStorePath(): string {\n if (pathOverride) return pathOverride\n const stateRoot = process.env.XDG_STATE_HOME || join(homedir(), \".local\", \"state\")\n return join(stateRoot, \"opencode-claude-code-plugin\", \"session-spawns.json\")\n}\n\nfunction isRecord(value: unknown): value is SpawnRecord {\n const record = value as SpawnRecord | null\n if (!record || typeof record !== \"object\" || Array.isArray(record)) return false\n if (typeof record.at !== \"number\" || !Number.isFinite(record.at)) return false\n const ok = (field: unknown) => field === undefined || typeof field === \"string\"\n if (!ok(record.model) || !ok(record.effort) || !ok(record.account)) return false\n return !!record.model\n}\n\nfunction readEntries(file: string): Map<string, SpawnRecord> {\n const out = new Map<string, SpawnRecord>()\n try {\n const parsed = JSON.parse(readFileSync(file, \"utf8\"))\n const entries = parsed && typeof parsed === \"object\" ? parsed.sessions : null\n if (!entries || typeof entries !== \"object\") return out\n const cutoff = Date.now() - SPAWN_RECORD_MAX_AGE_MS\n const valid = Object.entries(entries as Record<string, unknown>)\n .filter((entry): entry is [string, SpawnRecord] => isRecord(entry[1]))\n .filter(([, record]) => record.at >= cutoff)\n .sort((a, b) => a[1].at - b[1].at)\n for (const [key, record] of valid) {\n out.set(key, {\n model: record.model,\n ...(record.effort ? { effort: record.effort } : {}),\n ...(record.account ? { account: record.account } : {}),\n at: record.at,\n })\n }\n } catch {\n // ENOENT before the first spawn, or a file another process is renaming\n // into place: the empty store is the answer.\n }\n return out\n}\n\n/**\n * Record what `sessionID` spawned as. Returns whether the disk was written,\n * which is false for an unchanged record, an unusable id and a failed write\n * alike: the caller has nothing to do about any of them.\n */\nexport function recordSessionSpawn(\n sessionID: string,\n spawn: { model: string; effort?: string; account?: string },\n now: number = Date.now(),\n): boolean {\n if (!sessionID || sessionID === \"default\" || !spawn.model) return false\n const record: SpawnRecord = {\n model: spawn.model,\n ...(spawn.effort ? { effort: spawn.effort } : {}),\n ...(spawn.account ? { account: spawn.account } : {}),\n at: now,\n }\n const fingerprint = `${record.model}\\n${record.effort ?? \"\"}\\n${record.account ?? \"\"}`\n if (lastWritten.get(sessionID) === fingerprint) return false\n const file = spawnRecordStorePath()\n const merged = readEntries(file)\n merged.delete(sessionID)\n merged.set(sessionID, record)\n const cutoff = now - SPAWN_RECORD_MAX_AGE_MS\n const kept = [...merged.entries()]\n .filter(([, entry]) => entry.at >= cutoff)\n .sort((a, b) => a[1].at - b[1].at)\n .slice(-MAX_SPAWN_RECORDS)\n try {\n mkdirSync(join(file, \"..\"), { recursive: true, mode: 0o700 })\n const tmp = `${file}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {\n mode: 0o600,\n })\n renameSync(tmp, file)\n } catch {\n return false\n }\n if (lastWritten.size >= MAX_SPAWN_RECORDS) lastWritten.clear()\n lastWritten.set(sessionID, fingerprint)\n return true\n}\n\n/**\n * A reader that re-parses the file only when it changed. The TUI calls `read()`\n * on every refresh; an unchanged file costs one `stat`.\n */\nexport function createSpawnRecordReader(file: () => string = spawnRecordStorePath) {\n let identity = \"\"\n let cached = new Map<string, SpawnRecord>()\n return {\n read(): Map<string, SpawnRecord> {\n const path = file()\n let next = \"\"\n try {\n const stats = statSync(path)\n next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`\n } catch {\n next = \"missing\"\n }\n if (next !== identity) {\n identity = next\n cached = next === \"missing\" ? new Map() : readEntries(path)\n }\n return cached\n },\n }\n}\n\nexport function _setSpawnRecordStorePathForTests(path: string | null): void {\n pathOverride = path\n lastWritten.clear()\n}\n"],"mappings":"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","names":["text"]}
|
|
1
|
+
{"version":3,"sources":["../src/tui.ts","../src/subagent-sidebar.ts","../src/spawn-record-store.ts","../src/account-usage.ts","../src/account-sidebar.ts"],"sourcesContent":["/**\n * The TUI half of the package: a `Subagents` section at the bottom of\n * opencode's session sidebar listing the current session's subagents, running\n * first, each with a spinner (or ✓ / ✗), its agent type, its task description,\n * a `bg` marker for background ones, and a muted line saying what it really\n * runs as (`sonnet-5.5 · high · alpha`). Clicking a row opens that subagent's\n * session; opencode's own `up` key goes back to the parent.\n *\n * Below it, in the same slot, an `Accounts` section: one line per account\n * with a current usage reading (`appical · 5h 72% · resets 18:00`), from the\n * store the server half writes on every `rate_limit_event`\n * (`src/account-usage.ts`, laid out by `src/account-sidebar.ts`, h #g244). It\n * shares the slot rather than claiming a second one, so neither major has a\n * second registration to accept, and draws nothing until a reading exists.\n *\n * Always on: there is no option. One module serves both majors (h #g234):\n *\n * - opencode 1.x loads TUI plugins from `tui.json`'s `plugin` list, never\n * from `opencode.json`'s, resolves a package to `exports[\"./tui\"]`, and\n * wants a default export `{ id, tui }` with no `server`.\n * - opencode 2.x resolves the SAME `plugins` entry it already has\n * (`<repo>/dist`) to `<dir>/tui`, and wants `{ id, setup }`. 1.x ignores a\n * `setup` key and 2.x a `tui` key, so the default export carries both.\n *\n * Everything that decides WHAT to show lives in `src/subagent-sidebar.ts`\n * (pure, tested). Each major gets a thin adapter that gathers that module's\n * inputs from its own TUI state and supplies its own theme colours; the\n * section itself is drawn by one renderer. Nothing in here may throw into the\n * host or log: on the TUI's own thread stderr is the screen (h #g193), so every\n * failure degrades to drawing less.\n */\nimport { RGBA } from \"@opentui/core\"\nimport { createElement, createTextNode, effect, insert, insertNode, setProp } from \"@opentui/solid\"\nimport { createMemo, createSignal, onCleanup } from \"solid-js\"\nimport {\n SIDEBAR_ROW_WIDTH,\n SPINNER_FRAMES,\n collapsedSummary,\n completionsIn,\n deriveSubagentRows,\n layoutRow,\n partChildID,\n type ChildRunState,\n type ChildSessionInput,\n type CompletionInput,\n type Segment,\n type SidebarInput,\n type SubagentRow,\n type TaskPartInput,\n} from \"./subagent-sidebar.js\"\nimport { createSpawnRecordReader, type SpawnRecord } from \"./spawn-record-store.js\"\nimport { createAccountUsageReader, type AccountUsage } from \"./account-usage.js\"\nimport { accountUsageLines, type AccountUsageLine } from \"./account-sidebar.js\"\n\nexport const SUBAGENTS_TUI_PLUGIN_ID = \"opencode-claude-code-plugin.subagents\"\n\n/** After opencode 1.x's own sidebar sections (files is 500). */\nconst SECTION_ORDER = 900\nconst SPINNER_INTERVAL_MS = 100\n/** How often the spawn records and the finished rows' fade are rechecked. */\nconst REFRESH_INTERVAL_MS = 1500\n\n/** The tool names a subagent dispatch has: `task` on 1.x, `subagent` on 2.x. */\nconst TASK_TOOLS = new Set([\"task\", \"subagent\"])\n\ntype Color = RGBA\n\n/** The colours the section paints with, whatever theme shape the host has. */\nexport interface SidebarColors {\n text: Color\n muted: Color\n spinner: Color\n done: Color\n error: Color\n /** The effort on a row's second line: the theme's warning colour. */\n effort: Color\n effortFaded: Color\n hover: Color\n}\n\n/** What one major has to supply; the renderer below does everything else. */\ninterface SidebarHost {\n colors(): SidebarColors\n /** The inputs for one parent session, minus the clock and the records. */\n gather(parentID: string): Pick<SidebarInput, \"children\" | \"parts\" | \"completions\" | \"status\" | \"stoppedAt\">\n navigate(sessionID: string): void\n}\n\nfunction attempt<T>(fn: () => T, fallback: T): T {\n try {\n return fn()\n } catch {\n return fallback\n }\n}\n\n/**\n * `a` moved `weight` of the way to `b`, so a finished glyph reads as muted.\n * Works in whatever scale the host's RGBA uses, because it only ever hands\n * the class back values it read from the same class.\n */\nfunction mix(a: Color, b: Color, weight: number): Color {\n return attempt(\n () =>\n RGBA.fromValues(\n a.r + (b.r - a.r) * weight,\n a.g + (b.g - a.g) * weight,\n a.b + (b.b - a.b) * weight,\n a.a,\n ),\n a,\n )\n}\n\nfunction roleColor(colors: SidebarColors, role: Segment[\"role\"]): Color {\n switch (role) {\n case \"spinner\":\n return colors.spinner\n case \"done\":\n return colors.done\n case \"error\":\n return colors.error\n case \"effort\":\n return colors.effort\n case \"effort-faded\":\n return colors.effortFaded\n case \"agent\":\n case \"description\":\n return colors.text\n default:\n return colors.muted\n }\n}\n\n/**\n * The section, its clocks and the spawn-record reader, for one host. Returns\n * the slot renderer and a disposer.\n */\nfunction createSidebar(host: SidebarHost) {\n const [frame, setFrame] = createSignal(0)\n const [now, setNow] = createSignal(Date.now())\n const records = createSpawnRecordReader()\n let recordMap: Map<string, SpawnRecord> = attempt(() => records.read(), new Map())\n const [recordVersion, setRecordVersion] = createSignal(0)\n // The `Accounts` lines (h #g244): every account's last usage reading, from\n // the store the server half writes on each `rate_limit_event`. Its own clock\n // value, so a tick that only rechecks the readings does not re-derive the\n // subagent rows, which walk the session's messages.\n const usageReader = createAccountUsageReader()\n const [usage, setUsage] = createSignal<AccountUsage>(attempt(() => usageReader.read(), new Map()))\n const [usageNow, setUsageNow] = createSignal(Date.now())\n\n // One spinner clock and one refresh clock for every mounted section, each\n // running only while something needs it: nothing ticks while no subagent is\n // listed, and the spinner stops the moment the last one finishes.\n const sectionsRunning = new Set<symbol>()\n const sectionsShowing = new Set<symbol>()\n // Every mounted sidebar, whatever it shows: a usage reading arrives with no\n // event the TUI can see, so the one `stat` that finds it runs while a\n // sidebar is on screen, and nothing runs once none is.\n const sectionsMounted = new Set<symbol>()\n let spinner: ReturnType<typeof setInterval> | undefined\n let refresh: ReturnType<typeof setInterval> | undefined\n const syncClocks = () => {\n if (sectionsRunning.size > 0 && !spinner) {\n spinner = setInterval(() => setFrame((value) => (value + 1) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS)\n } else if (sectionsRunning.size === 0 && spinner) {\n clearInterval(spinner)\n spinner = undefined\n }\n const wanted = sectionsShowing.size > 0 || sectionsMounted.size > 0\n if (wanted && !refresh) {\n refresh = setInterval(() => {\n if (sectionsShowing.size > 0) {\n const next = attempt(() => records.read(), recordMap)\n if (next !== recordMap) {\n recordMap = next\n setRecordVersion((value) => value + 1)\n }\n setNow(Date.now())\n }\n // The reader hands back the same map while the file is unchanged, and\n // the clock moves once a minute (a reset is shown to the minute), so\n // an idle tick re-lays nothing out.\n setUsage(attempt(() => usageReader.read(), usage()))\n const minute = Math.floor(Date.now() / 60_000) * 60_000\n if (minute !== usageNow()) setUsageNow(minute)\n }, REFRESH_INTERVAL_MS)\n } else if (!wanted && refresh) {\n clearInterval(refresh)\n refresh = undefined\n }\n }\n\n const deriveRows = (parentID: string): SubagentRow[] => {\n recordVersion()\n return deriveSubagentRows({\n ...host.gather(parentID),\n record: (id) => recordMap.get(id),\n now: now(),\n })\n }\n\n const buildRow = (row: SubagentRow): OpentuiNode => {\n const box = createElement(\"box\")\n const layout = layoutRow(row, SIDEBAR_ROW_WIDTH)\n const first = createElement(\"text\")\n setProp(first, \"wrapMode\", \"none\")\n layout.first.forEach((segment, index) => {\n const span = createElement(\"span\")\n if (index === 0 && row.state === \"running\") {\n insert(span, () => SPINNER_FRAMES[frame() % SPINNER_FRAMES.length])\n } else {\n insertNode(span, createTextNode(segment.text))\n }\n effect(() => setProp(span, \"style\", { fg: roleColor(host.colors(), segment.role) }))\n insertNode(first, span)\n })\n insertNode(box, first)\n if (layout.second.length > 0) {\n // One line of spans, like the first, so the effort can take its own colour.\n const second = createElement(\"text\")\n setProp(second, \"wrapMode\", \"none\")\n for (const segment of layout.second) {\n const span = createElement(\"span\")\n insertNode(span, createTextNode(segment.text))\n effect(() => setProp(span, \"style\", { fg: roleColor(host.colors(), segment.role) }))\n insertNode(second, span)\n }\n insertNode(box, second)\n }\n if (row.sessionID) {\n const sessionID = row.sessionID\n setProp(box, \"onMouseDown\", () => attempt(() => host.navigate(sessionID), undefined))\n setProp(box, \"onMouseOver\", () =>\n attempt(() => setProp(box, \"backgroundColor\", host.colors().hover), undefined),\n )\n setProp(box, \"onMouseOut\", () =>\n attempt(() => setProp(box, \"backgroundColor\", \"transparent\"), undefined),\n )\n }\n return box\n }\n\n const buildSection = (\n list: SubagentRow[],\n expanded: () => boolean,\n setExpanded: (next: (value: boolean) => boolean) => boolean,\n ): OpentuiNode => {\n const section = createElement(\"box\")\n // Heading, styled like opencode 1.x's own MCP and Todo sections: a bold\n // title, and with more than two rows a caret that folds the list away\n // behind a one-line summary.\n const header = createElement(\"box\")\n setProp(header, \"flexDirection\", \"row\")\n setProp(header, \"gap\", 1)\n setProp(header, \"onMouseDown\", () => list.length > 2 && setExpanded((value) => !value))\n if (list.length > 2) {\n const caret = createElement(\"text\")\n insert(caret, () => (expanded() ? \"▼\" : \"▶\"))\n effect(() => setProp(caret, \"fg\", host.colors().text))\n insertNode(header, caret)\n }\n const title = createElement(\"text\")\n const bold = createElement(\"b\")\n insertNode(bold, createTextNode(\"Subagents\"))\n insertNode(title, bold)\n const summary = createElement(\"span\")\n insert(summary, () => (expanded() ? \"\" : ` (${collapsedSummary(list)})`))\n effect(() => setProp(summary, \"style\", { fg: host.colors().muted }))\n insertNode(title, summary)\n effect(() => setProp(title, \"fg\", host.colors().text))\n insertNode(header, title)\n insertNode(section, header)\n\n const body = createElement(\"box\")\n insert(body, () => (expanded() || list.length <= 2 ? list.map(buildRow) : null))\n insertNode(section, body)\n return section\n }\n\n const buildAccounts = (lines: AccountUsageLine[]): OpentuiNode => {\n const section = createElement(\"box\")\n const title = createElement(\"text\")\n const bold = createElement(\"b\")\n insertNode(bold, createTextNode(\"Accounts\"))\n insertNode(title, bold)\n effect(() => setProp(title, \"fg\", host.colors().text))\n insertNode(section, title)\n for (const line of lines) {\n const text = createElement(\"text\")\n setProp(text, \"wrapMode\", \"none\")\n for (const segment of line.segments) {\n const span = createElement(\"span\")\n insertNode(span, createTextNode(segment.text))\n effect(() => setProp(span, \"style\", { fg: roleColor(host.colors(), segment.role) }))\n insertNode(text, span)\n }\n insertNode(section, text)\n }\n return section\n }\n\n const render = (sessionID: () => string | undefined): OpentuiNode => {\n const root = createElement(\"box\")\n const subagentsBox = createElement(\"box\")\n const accountsBox = createElement(\"box\")\n insertNode(root, subagentsBox)\n insertNode(root, accountsBox)\n const key = Symbol(\"subagents-section\")\n sectionsMounted.add(key)\n const [expanded, setExpanded] = createSignal(true)\n const rows = createMemo(() => {\n const parentID = sessionID()\n if (!parentID) return [] as SubagentRow[]\n return attempt(() => deriveRows(parentID), [] as SubagentRow[])\n })\n effect(() => {\n const list = rows()\n if (list.some((row) => row.state === \"running\")) sectionsRunning.add(key)\n else sectionsRunning.delete(key)\n if (list.length > 0) sectionsShowing.add(key)\n else sectionsShowing.delete(key)\n syncClocks()\n })\n onCleanup(() => {\n sectionsRunning.delete(key)\n sectionsShowing.delete(key)\n sectionsMounted.delete(key)\n syncClocks()\n })\n insert(subagentsBox, () => {\n const list = rows()\n if (list.length === 0) return null\n return attempt(() => buildSection(list, expanded, setExpanded), null)\n })\n const accountLines = createMemo(() =>\n attempt(() => accountUsageLines(usage(), usageNow()), [] as AccountUsageLine[]),\n )\n insert(accountsBox, () => {\n const lines = accountLines()\n if (lines.length === 0) return null\n return attempt(() => buildAccounts(lines), null)\n })\n syncClocks()\n return root\n }\n\n const dispose = () => {\n sectionsRunning.clear()\n sectionsShowing.clear()\n sectionsMounted.clear()\n syncClocks()\n }\n return { render, dispose }\n}\n\n// ---------------------------------------------------------------------------\n// opencode 1.x\n// ---------------------------------------------------------------------------\n\n// The structural slice of opencode 1.x's `TuiPluginApi` this module reads.\n// Hand-written, like `src/opencode-types.ts`, so nothing is imported from\n// `@opencode-ai/plugin`.\ninterface V1SessionInfo {\n id: string\n parentID?: string\n title?: string\n agent?: string\n time?: { created?: number; updated?: number }\n}\ninterface V1Part {\n type: string\n tool?: string\n synthetic?: boolean\n text?: string\n state?: {\n status?: string\n input?: Record<string, unknown>\n metadata?: Record<string, unknown>\n title?: string\n time?: { start?: number; end?: number }\n }\n}\ninterface V1Api {\n state: {\n session: {\n get(sessionID: string): V1SessionInfo | undefined\n messages(sessionID: string): ReadonlyArray<{ id: string; role: string; time?: { created?: number } }> | undefined\n status(sessionID: string): { type?: string } | undefined\n }\n part(messageID: string): ReadonlyArray<V1Part> | undefined\n }\n theme: {\n current: {\n text: Color\n textMuted: Color\n accent: Color\n success: Color\n warning: Color\n error: Color\n backgroundElement: Color\n }\n }\n route: { navigate(name: string, params?: Record<string, unknown>): void }\n event: { on(type: string, handler: (event: { properties?: any }) => void): () => void }\n client?: { session?: { children?: (input: { sessionID: string }) => Promise<{ data?: V1SessionInfo[] }> } }\n slots: { register(plugin: { order?: number; slots: Record<string, (ctx: unknown, props: any) => OpentuiNode> }): string }\n lifecycle?: { onDispose(fn: () => void): () => void }\n}\n\nexport function registerV1(api: V1Api): void {\n // Child sessions seen per parent. 1.x has no session list in its TUI state,\n // so they come from its events, from a one-off `children` fetch the first\n // time a parent is shown (a TUI started after the dispatch still lists\n // them), and from finished parts' metadata.\n const childrenByParent = new Map<string, Map<string, ChildSessionInput>>()\n const fetched = new Set<string>()\n const stoppedAt = new Map<string, number>()\n const lastStatus = new Map<string, string>()\n const [version, setVersion] = createSignal(0)\n const bump = () => setVersion((value) => value + 1)\n\n const remember = (info: V1SessionInfo | undefined) => {\n if (!info?.id || !info.parentID) return false\n let children = childrenByParent.get(info.parentID)\n if (!children) {\n children = new Map()\n childrenByParent.set(info.parentID, children)\n }\n children.set(info.id, {\n id: info.id,\n title: info.title,\n agent: info.agent,\n created: info.time?.created,\n updated: info.time?.updated,\n })\n return true\n }\n\n const unsubscribe: Array<() => void> = []\n const listen = (type: string, handler: (properties: any) => void) => {\n const off = attempt(\n () => api.event.on(type, (event) => attempt(() => handler(event?.properties ?? {}), undefined)),\n undefined,\n )\n if (off) unsubscribe.push(off)\n }\n listen(\"session.created\", (properties) => remember(properties.info) && bump())\n listen(\"session.updated\", (properties) => remember(properties.info) && bump())\n listen(\"session.deleted\", (properties) => {\n const info = properties.info as V1SessionInfo | undefined\n if (info?.parentID && childrenByParent.get(info.parentID)?.delete(info.id)) bump()\n })\n listen(\"session.status\", (properties) => {\n const id = properties.sessionID as string | undefined\n const type = properties.status?.type as string | undefined\n if (!id || !type) return\n const previous = lastStatus.get(id)\n lastStatus.set(id, type)\n if (type === \"idle\" && previous && previous !== \"idle\") stoppedAt.set(id, Date.now())\n if (type !== \"idle\") stoppedAt.delete(id)\n })\n\n const fetchChildren = (parentID: string) => {\n if (fetched.has(parentID)) return\n fetched.add(parentID)\n const session = api.client?.session\n if (!session?.children) return\n void Promise.resolve()\n .then(() => session.children!.call(session, { sessionID: parentID }))\n .then((response) => {\n let changed = false\n for (const info of response?.data ?? []) changed = remember(info) || changed\n if (changed) bump()\n })\n .catch(() => undefined)\n }\n\n const host: SidebarHost = {\n colors() {\n const theme = api.theme.current\n return {\n text: theme.text,\n muted: theme.textMuted,\n spinner: theme.accent,\n done: mix(theme.success, theme.textMuted, 0.45),\n error: mix(theme.error, theme.textMuted, 0.35),\n effort: theme.warning ?? theme.textMuted,\n effortFaded: mix(theme.warning ?? theme.textMuted, theme.textMuted, 0.45),\n hover: theme.backgroundElement,\n }\n },\n gather(parentID) {\n version()\n fetchChildren(parentID)\n const parts: TaskPartInput[] = []\n const completions: CompletionInput[] = []\n for (const message of api.state.session.messages(parentID) ?? []) {\n const messageParts = api.state.part(message.id) ?? []\n if (message.role === \"assistant\") {\n for (const part of messageParts) {\n if (part.type !== \"tool\" || !TASK_TOOLS.has(part.tool ?? \"\")) continue\n const state = part.state ?? {}\n parts.push({\n status: state.status,\n input: state.input,\n metadata: state.metadata,\n title: state.title,\n start: state.time?.start,\n end: state.time?.end,\n })\n }\n } else if (message.role === \"user\") {\n // A background child's end is a synthetic user part in the parent.\n for (const part of messageParts) {\n if (part.type !== \"text\" || part.synthetic !== true || !part.text) continue\n completions.push(...completionsIn(part.text, message.time?.created))\n }\n }\n }\n const known = new Map(childrenByParent.get(parentID) ?? [])\n for (const part of parts) {\n const id = partChildID(part)\n if (!id || known.has(id)) continue\n const info = attempt(() => api.state.session.get(id), undefined)\n known.set(id, {\n id,\n title: info?.title,\n agent: info?.agent,\n created: info?.time?.created ?? part.start,\n updated: info?.time?.updated,\n })\n }\n return {\n children: [...known.values()],\n parts,\n completions,\n status: (id: string): ChildRunState => {\n const type = attempt(() => api.state.session.status(id)?.type, undefined)\n if (type === \"busy\" || type === \"retry\") return \"busy\"\n if (type === \"idle\") return \"idle\"\n return \"unknown\"\n },\n stoppedAt: (id: string) => stoppedAt.get(id),\n }\n },\n navigate(sessionID) {\n api.route.navigate(\"session\", { sessionID })\n },\n }\n\n const sidebar = createSidebar(host)\n api.slots.register({\n order: SECTION_ORDER,\n slots: {\n sidebar_content(_ctx: unknown, props: { session_id: string }) {\n return sidebar.render(() => props.session_id)\n },\n },\n })\n api.lifecycle?.onDispose(() => {\n for (const off of unsubscribe) attempt(off, undefined)\n sidebar.dispose()\n })\n}\n\n// ---------------------------------------------------------------------------\n// opencode 2.x\n// ---------------------------------------------------------------------------\n\n// The structural slice of opencode 2.x's TUI `Context` this module reads,\n// measured on 2.0.22 (h #g234). Its sessions carry `parentID`, its status is\n// `\"running\" | \"idle\"`, an assistant message holds its tool calls inline as\n// `content[]` items named `subagent` (with `metadata.sessionID` set while the\n// call still runs, unlike 1.x), and a background child's end is a message of\n// type `synthetic` whose text is `<subagent sessionID=\"...\" state=\"...\">`.\ninterface V2Session {\n id: string\n parentID?: string\n title?: string\n agent?: string\n time?: { created?: number; updated?: number }\n}\ninterface V2Message {\n type?: string\n text?: string\n time?: { created?: number }\n content?: Array<{\n type?: string\n name?: string\n time?: { created?: number; completed?: number }\n state?: { status?: string; input?: Record<string, unknown>; metadata?: Record<string, unknown> }\n }>\n}\ntype V2Shade = Record<string, Color>\ninterface V2Context {\n data: {\n session: {\n list(): V2Session[] | undefined\n status(sessionID: string): string\n message: { list(sessionID: string): V2Message[] | undefined }\n }\n }\n theme: {\n text: { base: Color; muted: Color; feedback: { success: V2Shade; warning?: V2Shade; error: V2Shade } }\n background: { raised: { base: Color } }\n hue: { accent: V2Shade }\n }\n themeMode?: \"dark\" | \"light\"\n ui: {\n router: { navigate(destination: { type: \"session\"; sessionID: string }): void }\n slot(claim: { append: string; render: (input: { sessionID: string }) => OpentuiNode }): () => void\n }\n}\n\nexport function setupV2(context: V2Context): () => void {\n const host: SidebarHost = {\n colors() {\n const theme = context.theme\n const muted = theme.text.muted\n // The accent hue at the shade that reads on this mode's background.\n const accent = theme.hue.accent[context.themeMode === \"light\" ? \"500\" : \"200\"] ?? theme.text.base\n return {\n text: theme.text.base,\n muted,\n spinner: accent,\n done: mix(theme.text.feedback.success.muted ?? theme.text.feedback.success.base, muted, 0.45),\n error: mix(theme.text.feedback.error.muted ?? theme.text.feedback.error.base, muted, 0.35),\n effort: theme.text.feedback.warning?.base ?? muted,\n effortFaded: mix(theme.text.feedback.warning?.base ?? muted, muted, 0.45),\n hover: theme.background.raised.base,\n }\n },\n gather(parentID) {\n const children: ChildSessionInput[] = (context.data.session.list() ?? [])\n .filter((session) => session.parentID === parentID)\n .map((session) => ({\n id: session.id,\n title: session.title,\n agent: session.agent,\n created: session.time?.created,\n updated: session.time?.updated,\n }))\n const parts: TaskPartInput[] = []\n const completions: CompletionInput[] = []\n for (const message of context.data.session.message.list(parentID) ?? []) {\n if (message.type === \"assistant\") {\n for (const item of message.content ?? []) {\n if (item.type !== \"tool\" || !TASK_TOOLS.has(item.name ?? \"\")) continue\n parts.push({\n status: item.state?.status,\n input: item.state?.input,\n metadata: item.state?.metadata,\n start: item.time?.created,\n end: item.time?.completed,\n })\n }\n } else if (message.type === \"synthetic\" && message.text) {\n completions.push(...completionsIn(message.text, message.time?.created))\n }\n }\n return {\n children,\n parts,\n completions,\n status: (id: string): ChildRunState => {\n const status = attempt(() => context.data.session.status(id), undefined)\n if (status === \"running\") return \"busy\"\n if (status === \"idle\") return \"idle\"\n return \"unknown\"\n },\n stoppedAt: () => undefined,\n }\n },\n navigate(sessionID) {\n context.ui.router.navigate({ type: \"session\", sessionID })\n },\n }\n\n const sidebar = createSidebar(host)\n const release = attempt(\n () =>\n context.ui.slot({\n append: \"sidebar.content\",\n render: (input) => sidebar.render(() => input.sessionID),\n }),\n undefined,\n )\n return () => {\n if (release) attempt(release, undefined)\n sidebar.dispose()\n }\n}\n\nconst tui = async (api: V1Api): Promise<void> => {\n attempt(() => registerV1(api), undefined)\n}\n\nconst setup = (context: V2Context): (() => void) | void =>\n attempt(() => setupV2(context), undefined)\n\nexport default { id: SUBAGENTS_TUI_PLUGIN_ID, tui, setup }\n","/**\n * The model behind the sidebar's `Subagents` section (`src/tui.ts`): which\n * subagents of one opencode session to show, in what state, in what order, and\n * the exact text of each row.\n *\n * Pure on purpose. It imports nothing, so the TUI bundle carries no logger (a\n * logger on the TUI's main thread writes to the terminal it is drawing on,\n * h #g193) and every rule here is testable without a terminal. The TUI half\n * only gathers the inputs and paints the segments this module returns.\n *\n * Measured on opencode 1.18.35 (h #g234), and each one shapes a rule below:\n *\n * - A RUNNING `task` part carries no `state.metadata` in the TUI's store, so\n * the child session id is not on the part until it completes. Rows are\n * built from the child SESSIONS (`parentID` is the current session) and a\n * part is matched to its child by the title opencode gives the child,\n * `<description> (@<agent> subagent)`.\n * - A background dispatch's part completes at once with a `<task id\n * state=\"running\">` envelope; the child's end arrives later as a synthetic\n * user part `<task id=\"ses_...\" state=\"completed\">` in the parent (opencode\n * 2 writes `<subagent sessionID=\"...\" state=\"...\">`). That envelope is the\n * authoritative end of a background child.\n * - `metadata.model` on a finished part is what opencode ASKED for, not what\n * ran: an agent's `forceModel`, its `reasoningEffort`, the dispatch form and\n * the fallback chain all change the spawn without telling opencode. So the\n * label prefers the spawn record the server half writes\n * (`src/spawn-record-store.ts`) and falls back to the part.\n */\n\nexport type SubagentState = \"running\" | \"done\" | \"error\"\n\n/** One child session as either major reports it. */\nexport interface ChildSessionInput {\n id: string\n title?: string\n agent?: string\n created?: number\n updated?: number\n}\n\n/** A `task` / `subagent` tool part in the parent, as either major reports it. */\nexport interface TaskPartInput {\n status?: string\n input?: Record<string, unknown>\n metadata?: Record<string, unknown>\n title?: string\n start?: number\n end?: number\n}\n\n/** A background child's end, read out of the parent's synthetic user parts. */\nexport interface CompletionInput {\n sessionID: string\n state: string\n at?: number\n}\n\n/** What the server half recorded about a session's real spawn. */\nexport interface SpawnRecordInput {\n model?: string\n effort?: string\n account?: string\n at?: number\n}\n\nexport type ChildRunState = \"busy\" | \"idle\" | \"unknown\"\n\nexport interface SidebarInput {\n children: ChildSessionInput[]\n parts: TaskPartInput[]\n completions: CompletionInput[]\n status: (sessionID: string) => ChildRunState\n record: (sessionID: string) => SpawnRecordInput | undefined\n /** When the TUI first saw a child stop, by session id. */\n stoppedAt: (sessionID: string) => number | undefined\n now: number\n}\n\nexport interface SubagentRow {\n /** The child session, absent while a dispatch has not created one yet. */\n sessionID?: string\n agent: string\n description: string\n background: boolean\n state: SubagentState\n /** `sonnet-5.5 · high · alpha`, empty when nothing is known. */\n label: string\n /** The effort piece of `label`, drawn in the theme's warning colour. */\n effort?: string\n started: number\n finished?: number\n}\n\n/** How long a finished subagent stays in the list, and how many of them. */\nexport const FINISHED_TTL_MS = 10 * 60_000\nexport const FINISHED_KEEP = 3\n/**\n * A background child that reads idle with no envelope yet is still starting\n * for this long: opencode creates the session before it marks it busy.\n */\nexport const STARTING_GRACE_MS = 10_000\n/** The usable width of a row in opencode 1.18.35's 42-column sidebar. */\nexport const SIDEBAR_ROW_WIDTH = 36\nexport const SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"]\n\nconst TITLE_SUFFIX = /\\s*\\(@([^)\\s]+) subagent\\)\\s*$/\n\n/** `Fix the form (@implementor subagent)` -> description and agent. */\nexport function parseChildTitle(title: string | undefined): { description: string; agent?: string } {\n const text = (title ?? \"\").trim()\n const match = TITLE_SUFFIX.exec(text)\n if (!match) return { description: text }\n return { description: text.slice(0, match.index).trim(), agent: match[1] }\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined\n}\n\n/** The child session a part names, on either major's metadata. */\nexport function partChildID(part: TaskPartInput): string | undefined {\n const metadata = part.metadata ?? {}\n return text(metadata.sessionId) ?? text(metadata.sessionID)\n}\n\nfunction partAgent(part: TaskPartInput): string | undefined {\n const input = part.input ?? {}\n return text(input.subagent_type) ?? text(input.agent)\n}\n\nfunction partDescription(part: TaskPartInput): string | undefined {\n return text(part.input?.description) ?? text(part.title)\n}\n\nfunction partBackground(part: TaskPartInput): boolean {\n return part.input?.background === true || part.metadata?.background === true\n}\n\nconst COMPLETION_ENVELOPE =\n /<(?:task\\s+id|subagent\\s+sessionID)=\"(ses_[A-Za-z0-9]+)\"\\s+state=\"([a-z_]+)\"/g\n\n/**\n * Every background end a text names, in order. Both majors' envelopes:\n * `<task id=\"ses_...\" state=\"completed\">` (1.x) and\n * `<subagent sessionID=\"ses_...\" state=\"completed\">` (2.x). `running` is the\n * dispatch's own answer, not an end, so it is skipped.\n */\nexport function completionsIn(value: string, at?: number): CompletionInput[] {\n const found: CompletionInput[] = []\n for (const match of value.matchAll(COMPLETION_ENVELOPE)) {\n if (match[2] === \"running\") continue\n found.push({ sessionID: match[1], state: match[2], at })\n }\n return found\n}\n\nfunction endState(state: string): SubagentState {\n return state === \"completed\" ? \"done\" : \"error\"\n}\n\n/**\n * `claude-opus-5-5` -> `opus-5.5`, `claude-sonnet-4-5-20250929@alpha` ->\n * `sonnet-4.5`, `claude-opus-5-5-fast` -> `opus-5.5 fast`,\n * `claude-opus-5[1m]` -> `opus-5 1m`. Anything that is not a Claude id is\n * returned without its account marker and otherwise untouched.\n */\nexport function shortModelName(modelID: string | undefined): string | undefined {\n let id = text(modelID)\n if (!id) return undefined\n const at = id.indexOf(\"@\")\n if (at > 0) id = id.slice(0, at)\n let suffix = \"\"\n const window = /\\[(\\d+[km])\\]$/i.exec(id)\n if (window) {\n suffix += ` ${window[1].toLowerCase()}`\n id = id.slice(0, window.index)\n }\n if (id.endsWith(\"-fast\")) {\n suffix = ` fast${suffix}`\n id = id.slice(0, -\"-fast\".length)\n }\n if (!id.startsWith(\"claude-\")) return `${id}${suffix}`\n id = id.slice(\"claude-\".length).replace(/-\\d{8}$/, \"\")\n const match = /^([a-z]+)-(\\d+)(?:-(\\d+))?$/.exec(id)\n if (!match) return `${id}${suffix}`\n const version = match[3] === undefined ? match[2] : `${match[2]}.${match[3]}`\n return `${match[1]}-${version}${suffix}`\n}\n\n/**\n * The account a provider id names on a multi-account install\n * (`claude-code-alpha` -> `alpha`). A bare `claude-code` (one account) and a\n * provider that is not this plugin's name none: there is nothing to tell apart.\n */\nexport function accountFromProvider(providerID: string | undefined): string | undefined {\n const id = text(providerID)\n if (!id?.startsWith(\"claude-code-\")) return undefined\n return id.slice(\"claude-code-\".length) || undefined\n}\n\n/** `sonnet-5.5 · high · alpha`; pieces that are not known are left out. */\nexport function subagentLabel(\n record: SpawnRecordInput | undefined,\n part: TaskPartInput | undefined,\n): string {\n const asked = part?.metadata?.model as { modelID?: unknown; providerID?: unknown } | undefined\n const askedModel = typeof asked?.modelID === \"string\" ? asked.modelID : undefined\n const askedProvider = typeof asked?.providerID === \"string\" ? asked.providerID : undefined\n const model = shortModelName(record?.model ?? askedModel)\n const effort = text(record?.effort)\n const account = record?.model ? text(record.account) : accountFromProvider(askedProvider)\n return [model, effort, account].filter(Boolean).join(\" · \")\n}\n\n/**\n * Cut `value` to `width` cells with a trailing ellipsis. Counts code points,\n * which is a cell each for the text a task description holds; a wide glyph\n * can overrun by one cell and is left to the renderer's own clipping.\n */\nexport function truncate(value: string, width: number): string {\n const chars = Array.from(value.replace(/\\s+/g, \" \").trim())\n if (width <= 0) return \"\"\n if (chars.length <= width) return chars.join(\"\")\n if (width === 1) return \"…\"\n return `${chars.slice(0, width - 1).join(\"\").trimEnd()}…`\n}\n\n/**\n * The rows for one session: running first (oldest first), then finished\n * (newest first), at most `FINISHED_KEEP` of those and none older than\n * `FINISHED_TTL_MS`. Empty when nothing is worth showing, which is what makes\n * the section disappear.\n */\nexport function deriveSubagentRows(input: SidebarInput): SubagentRow[] {\n const completions = new Map<string, CompletionInput>()\n for (const completion of input.completions) completions.set(completion.sessionID, completion)\n\n const byChild = new Map<string, TaskPartInput>()\n const unmatched: TaskPartInput[] = []\n for (const part of input.parts) {\n const child = partChildID(part)\n if (child) byChild.set(child, part)\n else unmatched.push(part)\n }\n\n const rows: SubagentRow[] = []\n const seen = new Set<string>()\n for (const child of input.children) {\n if (seen.has(child.id)) continue\n seen.add(child.id)\n const parsed = parseChildTitle(child.title)\n let part = byChild.get(child.id)\n if (!part) {\n // A running part has no child id yet: match it on the title opencode\n // gave the child, agent and description both.\n const index = unmatched.findIndex(\n (candidate) =>\n partDescription(candidate) === parsed.description &&\n (parsed.agent === undefined || partAgent(candidate) === parsed.agent),\n )\n if (index >= 0) part = unmatched.splice(index, 1)[0]\n }\n rows.push(rowFor(input, child, parsed, part, completions.get(child.id)))\n }\n // A dispatch opencode has not created a session for yet: shown, not clickable.\n for (const part of unmatched) {\n if (part.status !== \"pending\" && part.status !== \"running\") continue\n const description = partDescription(part)\n const agent = partAgent(part)\n if (!description && !agent) continue\n rows.push({\n agent: agent ?? \"subagent\",\n description: description ?? \"\",\n background: partBackground(part),\n state: \"running\",\n label: subagentLabel(undefined, part),\n started: part.start ?? input.now,\n })\n }\n\n const running = rows\n .filter((row) => row.state === \"running\")\n .sort((a, b) => a.started - b.started)\n const finished = rows\n .filter((row) => row.state !== \"running\")\n .filter((row) => input.now - (row.finished ?? row.started) <= FINISHED_TTL_MS)\n .sort((a, b) => (b.finished ?? b.started) - (a.finished ?? a.started))\n .slice(0, FINISHED_KEEP)\n return [...running, ...finished]\n}\n\nfunction rowFor(\n input: SidebarInput,\n child: ChildSessionInput,\n parsed: { description: string; agent?: string },\n part: TaskPartInput | undefined,\n completion: CompletionInput | undefined,\n): SubagentRow {\n const background = part ? partBackground(part) : false\n const started = part?.start ?? child.created ?? input.now\n const status = input.status(child.id)\n let state: SubagentState\n let finished: number | undefined\n if (part && !background && part.status !== \"pending\" && part.status !== \"running\") {\n state = part.status === \"completed\" ? \"done\" : \"error\"\n finished = part.end\n } else if (completion) {\n state = endState(completion.state)\n finished = completion.at\n } else if (status === \"busy\") {\n state = \"running\"\n } else if (part && !background) {\n // pending or running: the parent is still waiting on this child.\n state = \"running\"\n } else if (status === \"idle\" && input.now - started < STARTING_GRACE_MS) {\n state = \"running\"\n } else if (status === \"unknown\" && background) {\n // Nothing says it stopped: a background child keeps its spinner until\n // its envelope arrives or opencode reports it idle.\n state = \"running\"\n } else {\n state = \"done\"\n }\n if (state !== \"running\" && finished === undefined) {\n finished = input.stoppedAt(child.id) ?? child.updated ?? started\n }\n return {\n sessionID: child.id,\n agent: parsed.agent ?? (part ? partAgent(part) : undefined) ?? text(child.agent) ?? \"subagent\",\n description: (part ? partDescription(part) : undefined) ?? parsed.description,\n background,\n state,\n label: subagentLabel(input.record(child.id), part),\n effort: text(input.record(child.id)?.effort),\n started,\n finished,\n }\n}\n\n/** A run of text with the role the TUI maps onto a theme colour. */\nexport interface Segment {\n text: string\n role: \"spinner\" | \"done\" | \"error\" | \"agent\" | \"description\" | \"muted\" | \"effort\" | \"effort-faded\"\n}\n\n/**\n * The two lines of one row, laid out for `width` cells:\n *\n * ⠋ implementor Fix the dispatch f… bg\n * sonnet-5.5 · high · alpha\n *\n * The `bg` marker is right-aligned, the description takes what is left, and a\n * finished row is drawn in the muted role throughout apart from its glyph.\n */\nexport function layoutRow(\n row: SubagentRow,\n width: number = SIDEBAR_ROW_WIDTH,\n frame = 0,\n): { first: Segment[]; second: Segment[] } {\n const finished = row.state !== \"running\"\n const glyph =\n row.state === \"running\"\n ? SPINNER_FRAMES[frame % SPINNER_FRAMES.length]\n : row.state === \"done\"\n ? \"✓\"\n : \"✗\"\n const marker = row.background ? \"bg\" : \"\"\n const reserved = marker ? marker.length + 1 : 0\n const agentWidth = Math.max(1, Math.min(Array.from(row.agent).length, width - 2 - reserved - 6))\n const agent = truncate(row.agent, agentWidth)\n const room = width - 2 - Array.from(agent).length - 1 - reserved\n const description = room > 0 ? truncate(row.description, room) : \"\"\n const used = 2 + Array.from(agent).length + (description ? 1 + Array.from(description).length : 0)\n const first: Segment[] = [\n { text: glyph, role: row.state === \"running\" ? \"spinner\" : row.state === \"done\" ? \"done\" : \"error\" },\n { text: \" \", role: \"muted\" },\n { text: agent, role: finished ? \"muted\" : \"agent\" },\n ]\n if (description) {\n first.push({ text: \" \", role: \"muted\" })\n first.push({ text: description, role: finished ? \"muted\" : \"description\" })\n }\n if (marker) {\n first.push({ text: \" \".repeat(Math.max(1, width - used - marker.length)), role: \"muted\" })\n first.push({ text: marker, role: \"muted\" })\n }\n // The effort is the one piece in colour: it is what changes from one\n // subagent to the next and what costs the most, so it is the one to spot.\n const second: Segment[] = []\n if (row.label) {\n const pieces = truncate(row.label, width - 2).split(\" · \")\n const effortAt = row.effort ? pieces.indexOf(row.effort) : -1\n second.push({ text: \" \", role: \"muted\" })\n pieces.forEach((piece, index) => {\n if (index > 0) second.push({ text: \" · \", role: \"muted\" })\n const role = index !== effortAt ? \"muted\" : finished ? \"effort-faded\" : \"effort\"\n second.push({ text: piece, role })\n })\n }\n return { first, second }\n}\n\n/** The collapsed heading's count: `2 running` or `3 done`. */\nexport function collapsedSummary(rows: SubagentRow[]): string {\n const running = rows.filter((row) => row.state === \"running\").length\n return running > 0 ? `${running} running` : `${rows.length} done`\n}\n","/**\n * What each opencode session's `claude` process REALLY spawned as: model,\n * effort and account. Written by the server half on every turn that reaches\n * the CLI, read by the TUI half's `Subagents` sidebar section (`src/tui.ts`).\n *\n * ## Why a file, and why the TUI cannot read this anywhere else\n *\n * Measured on opencode 1.18.35 (h #g234): an `implementor` subagent whose\n * agent file says `forceModel: claude-sonnet-5-5` and `reasoningEffort: high`\n * spawned as exactly that, while the finished `task` part's `metadata.model`\n * said `claude-code-default/claude-opus-5-5` and no message carried an effort\n * at all. Everything that changes a spawn on the plugin side (an agent's\n * `forceModel` and `reasoningEffort`, `defaultSubagentModel`, the dispatch form\n * of h #g227, the fallback chain of h #g162, an account failover) is invisible\n * to opencode by construction, so showing opencode's own view would be showing\n * the wrong model exactly when the operator configured it not to be. The two\n * halves do not share memory on either major (1.x runs the server half in a\n * worker thread, 2.x in a subprocess), so the record goes through the state\n * directory the resume and dispatch stores already use.\n *\n * ## Shape, and what is never written\n *\n * ```json\n * { \"version\": 1, \"sessions\": {\n * \"ses_abc\": { \"model\": \"claude-sonnet-5-5\", \"effort\": \"high\", \"account\": \"alpha\", \"at\": 1760000000000 } } }\n * ```\n *\n * A model id, an effort level and an account NAME: the same three strings the\n * dispatch store keeps (h #g228), none secret. No prompt, no path, no session\n * key. `account` is written only on a multi-account install, where it tells\n * two configured accounts apart; on a single-account install there is nothing\n * to tell apart and it stays out.\n *\n * ## Cost and failure\n *\n * One write per session per process, and another only when what it spawns as\n * changes: `recordSessionSpawn` compares against what this process last wrote\n * and returns before touching the disk otherwise. Same discipline as the\n * resume store (h #g197): merge-on-write so two opencode processes do not erase\n * each other, atomic rename, `0600` in the `0700` directory, bounded at\n * `MAX_SPAWN_RECORDS` newest sessions and `SPAWN_RECORD_MAX_AGE_MS`. A missing,\n * unreadable or half-written file is an empty store, which costs the sidebar\n * its effort and falls back to the model opencode asked for. Two writers racing\n * can drop one entry; the next turn of that session writes it again, and the\n * only thing it ever costs is a label.\n *\n * Imports nothing that logs, because the TUI bundle reads it on the terminal's\n * own thread (h #g193): every function here is silent and never throws.\n */\nimport { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\nexport interface SpawnRecord {\n model?: string\n effort?: string\n account?: string\n at: number\n}\n\n/** Sessions worth remembering at once. A subagent fan-out names a handful. */\nexport const MAX_SPAWN_RECORDS = 256\n\n/** A session not spawned this long ago is not one the sidebar is showing. */\nexport const SPAWN_RECORD_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000\n\nlet pathOverride: string | null = null\nconst lastWritten = new Map<string, string>()\n\nexport function spawnRecordStorePath(): string {\n if (pathOverride) return pathOverride\n const stateRoot = process.env.XDG_STATE_HOME || join(homedir(), \".local\", \"state\")\n return join(stateRoot, \"opencode-claude-code-plugin\", \"session-spawns.json\")\n}\n\nfunction isRecord(value: unknown): value is SpawnRecord {\n const record = value as SpawnRecord | null\n if (!record || typeof record !== \"object\" || Array.isArray(record)) return false\n if (typeof record.at !== \"number\" || !Number.isFinite(record.at)) return false\n const ok = (field: unknown) => field === undefined || typeof field === \"string\"\n if (!ok(record.model) || !ok(record.effort) || !ok(record.account)) return false\n return !!record.model\n}\n\nfunction readEntries(file: string): Map<string, SpawnRecord> {\n const out = new Map<string, SpawnRecord>()\n try {\n const parsed = JSON.parse(readFileSync(file, \"utf8\"))\n const entries = parsed && typeof parsed === \"object\" ? parsed.sessions : null\n if (!entries || typeof entries !== \"object\") return out\n const cutoff = Date.now() - SPAWN_RECORD_MAX_AGE_MS\n const valid = Object.entries(entries as Record<string, unknown>)\n .filter((entry): entry is [string, SpawnRecord] => isRecord(entry[1]))\n .filter(([, record]) => record.at >= cutoff)\n .sort((a, b) => a[1].at - b[1].at)\n for (const [key, record] of valid) {\n out.set(key, {\n model: record.model,\n ...(record.effort ? { effort: record.effort } : {}),\n ...(record.account ? { account: record.account } : {}),\n at: record.at,\n })\n }\n } catch {\n // ENOENT before the first spawn, or a file another process is renaming\n // into place: the empty store is the answer.\n }\n return out\n}\n\n/**\n * Record what `sessionID` spawned as. Returns whether the disk was written,\n * which is false for an unchanged record, an unusable id and a failed write\n * alike: the caller has nothing to do about any of them.\n */\nexport function recordSessionSpawn(\n sessionID: string,\n spawn: { model: string; effort?: string; account?: string },\n now: number = Date.now(),\n): boolean {\n if (!sessionID || sessionID === \"default\" || !spawn.model) return false\n const record: SpawnRecord = {\n model: spawn.model,\n ...(spawn.effort ? { effort: spawn.effort } : {}),\n ...(spawn.account ? { account: spawn.account } : {}),\n at: now,\n }\n const fingerprint = `${record.model}\\n${record.effort ?? \"\"}\\n${record.account ?? \"\"}`\n if (lastWritten.get(sessionID) === fingerprint) return false\n const file = spawnRecordStorePath()\n const merged = readEntries(file)\n merged.delete(sessionID)\n merged.set(sessionID, record)\n const cutoff = now - SPAWN_RECORD_MAX_AGE_MS\n const kept = [...merged.entries()]\n .filter(([, entry]) => entry.at >= cutoff)\n .sort((a, b) => a[1].at - b[1].at)\n .slice(-MAX_SPAWN_RECORDS)\n try {\n mkdirSync(join(file, \"..\"), { recursive: true, mode: 0o700 })\n const tmp = `${file}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {\n mode: 0o600,\n })\n renameSync(tmp, file)\n } catch {\n return false\n }\n if (lastWritten.size >= MAX_SPAWN_RECORDS) lastWritten.clear()\n lastWritten.set(sessionID, fingerprint)\n return true\n}\n\n/**\n * A reader that re-parses the file only when it changed. The TUI calls `read()`\n * on every refresh; an unchanged file costs one `stat`.\n */\nexport function createSpawnRecordReader(file: () => string = spawnRecordStorePath) {\n let identity = \"\"\n let cached = new Map<string, SpawnRecord>()\n return {\n read(): Map<string, SpawnRecord> {\n const path = file()\n let next = \"\"\n try {\n const stats = statSync(path)\n next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`\n } catch {\n next = \"missing\"\n }\n if (next !== identity) {\n identity = next\n cached = next === \"missing\" ? new Map() : readEntries(path)\n }\n return cached\n },\n }\n}\n\nexport function _setSpawnRecordStorePathForTests(path: string | null): void {\n pathOverride = path\n lastWritten.clear()\n}\n","/**\n * What each account's usage windows looked like the last time the CLI said,\n * so the operator hears about a window filling up BEFORE it runs out\n * (`usageWarnThresholds`, `usageSwitchAt`, h #g244).\n *\n * ## Where the numbers come from\n *\n * The headless CLI emits `{type:\"rate_limit_event\", rate_limit_info:{status,\n * rateLimitType, resetsAt, utilization, ...}}` whenever its view of the\n * account's limits CHANGES (h #g194). Before this module the plugin kept only\n * a REJECTED one (`rememberAccountLimit`), so the first thing an operator saw\n * was the limit itself. Every event is a reading now, keyed account -> window.\n *\n * Two measured facts shape the record:\n *\n * - `utilization` is a FRACTION, not a percent. Read off the live plugin log\n * on 2026-10-16 and 2026-10-17 (CLI 2.1.293): `allowed_warning` events\n * carrying `0.75` and `0.9`, which the existing NOTICE already rendered as\n * \"75% used\" and \"90% used\". It is stored exactly as the CLI sent it.\n * - An `allowed` event on an account with headroom carries NO utilization at\n * all (`utilization: null` in the CLI's own frame, absent after parsing).\n * That is \"unknown\", never zero: a null reading crosses no threshold and\n * the sidebar shows `ok`, not `0%`.\n *\n * The interactive transport has no event, so the only thing it records is a\n * limit reply: status `rejected`, utilization unknown, and whatever reset the\n * reply's own text names (`limitFactsFromText`, h #g208).\n *\n * ## A window's crossings, and when they clear\n *\n * Each window keeps `noted`, the thresholds (in percent) a `▌ **usage:**` note\n * already announced, so a reused process's next turn, another session on the\n * same account and a restarted opencode all stay quiet about a crossing that\n * was already said. `noted` carries over to a new reading for as long as the\n * PREVIOUS reading is still current, and is cleared once its reset has passed:\n * the reset is what ends a window, and comparing two `resetsAt` values for\n * equality would re-announce a crossing every time the CLI rounded the reset\n * differently. A reading with no reset time (the interactive transport's) is\n * current for `UNTIMED_READING_TTL_MS`.\n *\n * ## File and failure\n *\n * `$XDG_STATE_HOME/opencode-claude-code-plugin/account-usage.json`, the fifth\n * file the plugin writes there, with the same discipline as the resume and\n * spawn stores (h #g197, h #g234): merge-on-write so two opencode processes do\n * not erase each other, atomic rename, `0600` in a `0700` directory, bounded,\n * and an unreadable or half-written file is an empty store. It holds account\n * NAMES, window names, numbers and times, and nothing else.\n *\n * Imports nothing that logs, because the TUI bundle reads it on the terminal's\n * own thread (h #g193): every function here is silent and never throws.\n */\nimport { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\n/** One reading of one window, as the CLI reported it. */\nexport interface UsageReading {\n /** The CLI's fraction (0.75 is 75%), or null when the event carried none. */\n utilization: number | null\n /** Epoch ms the window resets at, when the CLI said. */\n resetsAt?: number\n /**\n * The TUI's own reset phrase (`3pm (Europe/Amsterdam)`), for a limit that\n * reached us only as text. Shown only when `resetsAt` is unknown.\n */\n resetsText?: string\n /** `allowed`, `allowed_warning` or `rejected`, as the CLI sent it. */\n status: string\n}\n\nexport interface WindowUsage extends UsageReading {\n /** Epoch ms this reading was recorded. */\n observedAt: number\n /** Thresholds, in percent, a note already announced in this window. */\n noted: number[]\n}\n\n/** account -> window (the CLI's `rateLimitType`) -> reading. */\nexport type AccountUsage = Map<string, Map<string, WindowUsage>>\n\nexport const DEFAULT_USAGE_WARN_THRESHOLDS: readonly number[] = [50, 75, 90, 95]\nexport const DEFAULT_USAGE_SWITCH_AT = 90\n\n/**\n * How long a reading with no reset time stays current. The only such reading\n * is the interactive transport's limit reply, and the window it names is the\n * five-hour one (`limitFactsFromText`), so five hours is the longest that\n * reading could still be true.\n */\nexport const UNTIMED_READING_TTL_MS = 5 * 60 * 60 * 1000\n\n/** A reading not refreshed in this long is dropped: past every window's reset. */\nexport const USAGE_READING_MAX_AGE_MS = 8 * 24 * 60 * 60 * 1000\n\nexport const MAX_USAGE_ACCOUNTS = 32\nexport const MAX_USAGE_WINDOWS = 8\n\nconst NAME = /^[a-z0-9_-]{1,64}$/\nconst STATUS = /^[a-z_]{1,32}$/\nconst MAX_RESETS_TEXT = 80\n\nlet pathOverride: string | null = null\nlet cache: { path: string; usage: AccountUsage } | null = null\n\nexport function accountUsageStorePath(): string {\n if (pathOverride) return pathOverride\n const stateRoot = process.env.XDG_STATE_HOME || join(homedir(), \".local\", \"state\")\n return join(stateRoot, \"opencode-claude-code-plugin\", \"account-usage.json\")\n}\n\nfunction finite(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value)\n}\n\nfunction validEntry(value: unknown): WindowUsage | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null\n const entry = value as Record<string, unknown>\n if (!finite(entry.observedAt)) return null\n if (typeof entry.status !== \"string\" || !STATUS.test(entry.status)) return null\n if (entry.utilization !== null && !finite(entry.utilization)) return null\n if (entry.resetsAt !== undefined && !finite(entry.resetsAt)) return null\n if (\n entry.resetsText !== undefined &&\n (typeof entry.resetsText !== \"string\" || entry.resetsText.length > MAX_RESETS_TEXT)\n ) {\n return null\n }\n const noted = Array.isArray(entry.noted)\n ? [...new Set(entry.noted.filter((t): t is number => finite(t) && t > 0 && t <= 100))].sort(\n (a, b) => a - b,\n )\n : []\n return {\n utilization: entry.utilization as number | null,\n ...(entry.resetsAt === undefined ? {} : { resetsAt: entry.resetsAt as number }),\n ...(entry.resetsText === undefined ? {} : { resetsText: entry.resetsText as string }),\n status: entry.status,\n observedAt: entry.observedAt,\n noted,\n }\n}\n\nfunction readStore(file: string, now: number): AccountUsage {\n const out: AccountUsage = new Map()\n try {\n const parsed = JSON.parse(readFileSync(file, \"utf8\"))\n const accounts = parsed && typeof parsed === \"object\" ? parsed.accounts : null\n if (!accounts || typeof accounts !== \"object\" || Array.isArray(accounts)) return out\n const cutoff = now - USAGE_READING_MAX_AGE_MS\n for (const [account, windows] of Object.entries(accounts as Record<string, unknown>)) {\n if (!NAME.test(account) || !windows || typeof windows !== \"object\") continue\n const kept = new Map<string, WindowUsage>()\n for (const [window, raw] of Object.entries(windows as Record<string, unknown>)) {\n if (!NAME.test(window)) continue\n const entry = validEntry(raw)\n if (entry && entry.observedAt >= cutoff) kept.set(window, entry)\n }\n if (kept.size > 0) out.set(account, kept)\n }\n } catch {\n // ENOENT before the first event, or a file another process is renaming\n // into place: the empty store is the answer.\n }\n return out\n}\n\nfunction newestObserved(windows: Map<string, WindowUsage>): number {\n let newest = 0\n for (const entry of windows.values()) newest = Math.max(newest, entry.observedAt)\n return newest\n}\n\nfunction bounded(usage: AccountUsage, now: number): AccountUsage {\n const cutoff = now - USAGE_READING_MAX_AGE_MS\n const accounts = [...usage.entries()]\n .map(([account, windows]) => {\n const kept = [...windows.entries()]\n .filter(([, entry]) => entry.observedAt >= cutoff)\n .sort((a, b) => a[1].observedAt - b[1].observedAt)\n .slice(-MAX_USAGE_WINDOWS)\n return [account, new Map(kept)] as const\n })\n .filter(([, windows]) => windows.size > 0)\n .sort((a, b) => newestObserved(a[1]) - newestObserved(b[1]))\n .slice(-MAX_USAGE_ACCOUNTS)\n return new Map(accounts)\n}\n\nfunction writeStore(file: string, usage: AccountUsage): boolean {\n const accounts: Record<string, Record<string, WindowUsage>> = {}\n for (const [account, windows] of usage) accounts[account] = Object.fromEntries(windows)\n try {\n mkdirSync(join(file, \"..\"), { recursive: true, mode: 0o700 })\n const tmp = `${file}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify({ version: 1, accounts }), { mode: 0o600 })\n renameSync(tmp, file)\n return true\n } catch {\n return false\n }\n}\n\n/** This process's view, loaded from disk once and kept current by its own writes. */\nfunction cached(now: number): AccountUsage {\n const file = accountUsageStorePath()\n if (!cache || cache.path !== file) cache = { path: file, usage: readStore(file, now) }\n return cache.usage\n}\n\n/** Re-read the file into the cache, for a decision another process may have made. */\nfunction refreshed(now: number): AccountUsage {\n const file = accountUsageStorePath()\n cache = { path: file, usage: readStore(file, now) }\n return cache.usage\n}\n\n/**\n * Whether a reading still describes the window it was taken in: its reset is\n * ahead, or, with no reset time, it is younger than `UNTIMED_READING_TTL_MS`.\n */\nexport function isReadingCurrent(entry: WindowUsage, now: number = Date.now()): boolean {\n if (entry.resetsAt !== undefined) return entry.resetsAt > now\n return now - entry.observedAt < UNTIMED_READING_TTL_MS\n}\n\n/**\n * The record a new reading leaves behind. The thresholds already announced\n * carry over only while the previous reading is still current; once its reset\n * has passed, the window has turned over and every crossing may be said again.\n */\nexport function applyReading(\n previous: WindowUsage | undefined,\n reading: UsageReading,\n now: number = Date.now(),\n): WindowUsage {\n const carried = previous && isReadingCurrent(previous, now) ? previous.noted : []\n return {\n utilization: finite(reading.utilization) ? reading.utilization : null,\n ...(finite(reading.resetsAt) ? { resetsAt: reading.resetsAt } : {}),\n ...(reading.resetsText && !finite(reading.resetsAt)\n ? { resetsText: reading.resetsText.slice(0, MAX_RESETS_TEXT) }\n : {}),\n status: STATUS.test(reading.status) ? reading.status : \"unknown\",\n observedAt: now,\n noted: [...carried],\n }\n}\n\n/** The CLI's fraction as a percent, to one decimal so `0.9` is exactly 90. */\nexport function usagePercent(utilization: number): number {\n return Math.round(utilization * 1000) / 10\n}\n\n/**\n * The thresholds this reading crosses that no note announced yet. None for a\n * reading that is no longer current, one whose utilization is unknown, and a\n * rejection: a window that ran out gets the `▌ **usage limit:**` note instead.\n */\nexport function pendingCrossings(\n entry: WindowUsage,\n thresholds: readonly number[],\n now: number = Date.now(),\n): number[] {\n if (!isReadingCurrent(entry, now)) return []\n if (entry.utilization === null || entry.status === \"rejected\") return []\n const percent = usagePercent(entry.utilization)\n return [...new Set(thresholds)]\n .filter((threshold) => percent >= threshold && !entry.noted.includes(threshold))\n .sort((a, b) => a - b)\n}\n\n/**\n * `usageWarnThresholds`: percents above 0 and up to 100, deduplicated and\n * sorted. Unset is the default list; an explicit empty list turns the notes off.\n */\nexport function resolveUsageWarnThresholds(value: unknown): number[] {\n if (!Array.isArray(value)) return [...DEFAULT_USAGE_WARN_THRESHOLDS]\n return [...new Set(value.filter((t): t is number => finite(t) && t > 0 && t <= 100))].sort(\n (a, b) => a - b,\n )\n}\n\n/**\n * `usageSwitchAt`: a percent above 0 and up to 100, or null for \"never\". `false`\n * and `0` turn it off; anything unusable is the default.\n */\nexport function resolveUsageSwitchAt(value: unknown): number | null {\n if (value === false || value === 0) return null\n if (finite(value) && value > 0 && value <= 100) return value\n return DEFAULT_USAGE_SWITCH_AT\n}\n\n/**\n * Record one reading. Returns the record left behind, or null when the account\n * or window name is unusable or the write failed (the in-process view still\n * takes the reading, so this turn's note is not lost to a read-only disk).\n */\nexport function recordUsageReading(\n account: string,\n window: string,\n reading: UsageReading,\n now: number = Date.now(),\n): WindowUsage | null {\n if (!NAME.test(account) || !NAME.test(window)) return null\n const file = accountUsageStorePath()\n const merged = readStore(file, now)\n const previous = merged.get(account)?.get(window) ?? cached(now).get(account)?.get(window)\n const entry = applyReading(previous, reading, now)\n const windows = merged.get(account) ?? new Map<string, WindowUsage>()\n windows.set(window, entry)\n merged.set(account, windows)\n const kept = bounded(merged, now)\n cache = { path: file, usage: kept }\n return writeStore(file, kept) ? entry : null\n}\n\n/** Mark thresholds announced, on the window's newest reading. */\nexport function markUsageNoted(\n account: string,\n window: string,\n thresholds: readonly number[],\n now: number = Date.now(),\n): boolean {\n if (!NAME.test(account) || !NAME.test(window) || thresholds.length === 0) return false\n const file = accountUsageStorePath()\n const merged = readStore(file, now)\n const entry = merged.get(account)?.get(window) ?? cached(now).get(account)?.get(window)\n if (!entry) return false\n const updated = {\n ...entry,\n noted: [...new Set([...entry.noted, ...thresholds])].sort((a, b) => a - b),\n }\n const windows = merged.get(account) ?? new Map<string, WindowUsage>()\n windows.set(window, updated)\n merged.set(account, windows)\n const kept = bounded(merged, now)\n cache = { path: file, usage: kept }\n return writeStore(file, kept)\n}\n\nexport interface UsageCrossing {\n window: string\n entry: WindowUsage\n /** Newly crossed thresholds, in percent, ascending. */\n crossed: number[]\n}\n\n/**\n * Every window of `account` with a threshold to announce. Answered from this\n * process's view, which costs no I/O on an ordinary turn; only when something\n * looks pending is the file re-read, so a crossing another opencode process\n * already announced is not announced twice.\n */\nexport function usageCrossings(\n account: string,\n thresholds: readonly number[],\n now: number = Date.now(),\n): UsageCrossing[] {\n if (thresholds.length === 0) return []\n const collect = (usage: AccountUsage): UsageCrossing[] => {\n const out: UsageCrossing[] = []\n for (const [window, entry] of usage.get(account) ?? []) {\n const crossed = pendingCrossings(entry, thresholds, now)\n if (crossed.length > 0) out.push({ window, entry, crossed })\n }\n return out\n }\n if (collect(cached(now)).length === 0) return []\n return collect(refreshed(now))\n}\n\n/**\n * Whether any current window of `account` is at or over `percent`, or was\n * refused outright. What a switch at a threshold reads, so it never moves new\n * turns onto an account that is no better off.\n */\nexport function accountUsageAtOrOver(\n account: string,\n percent: number,\n now: number = Date.now(),\n): boolean {\n for (const entry of cached(now).get(account)?.values() ?? []) {\n if (!isReadingCurrent(entry, now)) continue\n if (entry.status === \"rejected\") return true\n if (entry.utilization !== null && usagePercent(entry.utilization) >= percent) return true\n }\n return false\n}\n\n/** Every recorded reading, read fresh off disk. For `/claude-code-doctor`. */\nexport function snapshotAccountUsage(now: number = Date.now()): AccountUsage {\n return refreshed(now)\n}\n\n/**\n * A reader that re-parses the file only when it changed, for the TUI's\n * refresh clock: an unchanged file costs one `stat`.\n */\nexport function createAccountUsageReader(file: () => string = accountUsageStorePath) {\n let identity = \"\"\n let usage: AccountUsage = new Map()\n return {\n read(): AccountUsage {\n const path = file()\n let next = \"\"\n try {\n const stats = statSync(path)\n next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`\n } catch {\n next = \"missing\"\n }\n if (next !== identity) {\n identity = next\n usage = next === \"missing\" ? new Map() : readStore(path, Date.now())\n }\n return usage\n },\n }\n}\n\n/** Test-only. Points the store at another file and forgets the cache. */\nexport function _setAccountUsageStorePathForTests(path: string | null): void {\n pathOverride = path\n cache = null\n}\n","/**\n * The model behind the sidebar's `Accounts` section (`src/tui.ts`): one line\n * per account with a current usage reading, `appical · 5h 72% · resets 18:00`\n * (h #g244).\n *\n * Pure, like `src/subagent-sidebar.ts`, and for the same reason: the TUI bundle\n * runs on the terminal's own thread, where a logger writes over the interface\n * (h #g193). It imports only the store's reader types and that module's pure\n * helpers.\n *\n * Which window an account's line shows: a refused one first (the account is\n * out now, whatever its other windows say), else the fullest known one, else\n * any current reading (`ok`, a window the CLI reported without a utilization,\n * which is how it reports one with headroom). A reading whose reset has passed\n * describes a window that is over and is not shown at all.\n */\nimport { isReadingCurrent, usagePercent, type AccountUsage, type WindowUsage } from \"./account-usage.js\"\nimport { SIDEBAR_ROW_WIDTH, truncate, type Segment } from \"./subagent-sidebar.js\"\n\nexport interface AccountUsageLine {\n account: string\n window: string\n /** `limited` (refused), `warning` (at or past `WARNING_PERCENT`), or `ok`. */\n state: \"limited\" | \"warning\" | \"ok\"\n segments: Segment[]\n}\n\n/** Where a line takes the theme's warning colour: the CLI's own first warning level. */\nexport const WARNING_PERCENT = 75\n\nconst SHORT_WINDOWS: Record<string, string> = {\n five_hour: \"5h\",\n seven_day: \"7d\",\n seven_day_opus: \"7d opus\",\n seven_day_sonnet: \"7d sonnet\",\n seven_day_overage_included: \"7d extra\",\n overage: \"extra\",\n}\n\n/** `five_hour` -> `5h`; an unknown window name is shown as itself, `unknown` as nothing. */\nexport function shortWindowName(window: string): string {\n if (window === \"unknown\") return \"\"\n return SHORT_WINDOWS[window] ?? window.replace(/_/g, \" \")\n}\n\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n/**\n * A reset time in local time, as short as it can be unambiguous: `18:00` today,\n * `Oct 21 12:00` on another day.\n */\nexport function formatResetClock(resetsAt: number, now: number): string {\n const when = new Date(resetsAt)\n const today = new Date(now)\n const pad = (value: number) => String(value).padStart(2, \"0\")\n const clock = `${pad(when.getHours())}:${pad(when.getMinutes())}`\n const sameDay =\n when.getFullYear() === today.getFullYear() &&\n when.getMonth() === today.getMonth() &&\n when.getDate() === today.getDate()\n return sameDay ? clock : `${MONTHS[when.getMonth()]} ${when.getDate()} ${clock}`\n}\n\nfunction rank(entry: WindowUsage): number {\n if (entry.status === \"rejected\") return 1000\n if (entry.utilization === null) return -1\n return usagePercent(entry.utilization)\n}\n\n/** The window an account's line is about, or undefined when none is current. */\nexport function pressingWindow(\n windows: ReadonlyMap<string, WindowUsage>,\n now: number,\n): [string, WindowUsage] | undefined {\n let best: [string, WindowUsage] | undefined\n for (const [window, entry] of windows) {\n if (!isReadingCurrent(entry, now)) continue\n if (!best || rank(entry) > rank(best[1])) best = [window, entry]\n }\n return best\n}\n\n/** Every account with a current reading, alphabetically, each laid out for `width`. */\nexport function accountUsageLines(\n usage: AccountUsage,\n now: number,\n width: number = SIDEBAR_ROW_WIDTH,\n): AccountUsageLine[] {\n const lines: AccountUsageLine[] = []\n for (const account of [...usage.keys()].sort()) {\n const pressing = pressingWindow(usage.get(account)!, now)\n if (!pressing) continue\n const [window, entry] = pressing\n const state: AccountUsageLine[\"state\"] =\n entry.status === \"rejected\"\n ? \"limited\"\n : entry.utilization !== null && usagePercent(entry.utilization) >= WARNING_PERCENT\n ? \"warning\"\n : \"ok\"\n const short = shortWindowName(window)\n const level =\n state === \"limited\"\n ? \"limited\"\n : entry.utilization === null\n ? `${short ? `${short} ` : \"\"}ok`\n : `${short ? `${short} ` : \"\"}${Math.round(usagePercent(entry.utilization))}%`\n const resets =\n entry.resetsAt !== undefined\n ? ` · resets ${formatResetClock(entry.resetsAt, now)}`\n : entry.resetsText\n ? ` · resets ${entry.resetsText}`\n : \"\"\n const name = truncate(account, Math.max(1, Math.floor(width / 2)))\n // `truncate` trims, so the space after the name is added outside it.\n const rest = ` ${truncate(`· ${level}${resets}`, Math.max(0, width - Array.from(name).length - 1))}`\n lines.push({\n account,\n window,\n state,\n segments: [\n { text: name, role: \"agent\" },\n { text: rest, role: state === \"limited\" ? \"error\" : state === \"warning\" ? \"effort\" : \"muted\" },\n ],\n })\n }\n return lines\n}\n\n/** The plain text of a line, for tests and the collapsed summary. */\nexport function lineText(line: AccountUsageLine): string {\n return line.segments.map((segment) => segment.text).join(\"\")\n}\n"],"mappings":"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","names":["text","mkdirSync","readFileSync","renameSync","statSync","writeFileSync","homedir","join","pathOverride","pathOverride","join","homedir","readFileSync","statSync","text"]}
|
package/package.json
CHANGED
|
@@ -9,9 +9,10 @@ This plugin is `@khalilgharbaoui/opencode-claude-code-plugin`. It registers one
|
|
|
9
9
|
`claude-code*` providers and routes inference through the `claude` CLI, not opencode's
|
|
10
10
|
native Anthropic provider. Headless `--print` is the default, which is the Agent SDK
|
|
11
11
|
path. Never promise a user what subscription headless usage draws from: Anthropic's
|
|
12
|
-
policy page is the only authority and it changes (
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
policy page is the only authority and it changes (read 2026-10-16: subscription usage
|
|
13
|
+
limits by default, and since 2026-10-07 Max and Team plans also carry monthly API credits,
|
|
14
|
+
Max 20x $200, spendable through `claude -p` by giving the CLI an API key from the linked
|
|
15
|
+
Console organization; Pro has none). API-key/cloud billing depends on the CLI's
|
|
15
16
|
authentication. Confirm the user's intended account and billing method.
|
|
16
17
|
|
|
17
18
|
## How this differs from the alternatives
|
|
@@ -121,6 +122,11 @@ Defaults below describe normal headless opencode use when the key is absent.
|
|
|
121
122
|
| `subagentDispatchCrossGroup` | boolean | `false` | Let `subagentDispatch`'s account row offer accounts outside the dispatching account's own `accountGroups` group. **Off by default and never implied by anything else**: a subagent is handed the task text the main agent writes, which can quote the conversation, and it reads the repository, so offering another group is offering that group the conversation by another route. Does nothing when `accountGroups` is unset, where every account is already one group, or when `subagentDispatch` is off. |
|
|
122
123
|
| `defaultSubagentModel` | string | unset | Seed-config default for discovered `mode: subagent` agents without a full `provider/model` pin; `forceModel` takes precedence. Keeps the caller's account. Unknown ids warn and keep the inherited model. Not independently read per expanded account. |
|
|
123
124
|
| `defaultSubagentCacheTtl` | string | unset | Prompt cache TTL (`5m` / `1h`) for discovered `mode: subagent` agents that declare no `cacheTtl`; the agent's own value takes precedence. Unset leaves the CLI's default (1 hour on a subscription). Unknown values warn and change nothing. Headless and interactive spawns, never compaction. |
|
|
125
|
+
| `defaultSubagentMaxTurns` | number | unset | `--max-turns` for discovered `mode: subagent` agents that declare no `maxTurns`; the agent's own value wins. Counted by the CLI per user message, so safe on a reused process. CLI 2.1.280+; headless only (print-only flag, the interactive transport WARNs once). Part of the session key. |
|
|
126
|
+
| `defaultSubagentMaxBudgetUsd` | number | unset | `--max-budget-usd` for discovered `mode: subagent` agents that declare no `maxBudgetUsd`. Counted by the CLI over the whole process's life and reset on `--resume`, hence subagents only; a main agent's `maxBudgetUsd` is refused with a WARN. Probe-gated on `claude --help`; headless only. |
|
|
127
|
+
| `maxBudgetUsd` | number | unset | Dollar cap per opencode conversation (its turns plus every descendant subagent), from the CLI's `total_cost_usd` (cumulative per Claude session on headless, so the ledger takes deltas). Unset: no ledger at all. Warn notes at `budgetWarnAt`; at the cap the next fresh turn ends before any spawn with a `▌ **budget:**` note and `{unified:"error", raw:"budget_exceeded"}`. Not on a turn answering a parked proxy call, not on compaction; a child gets an `error` part instead of the note; `doGenerate` gets the finish only. In memory; raising it needs an opencode relaunch. |
|
|
128
|
+
| `budgetWarnAt` | number[] | `[50, 80]` | Percent thresholds of `maxBudgetUsd`, one note each per conversation (only the highest when a turn jumps several). `[]` drops the notes, keeps the stop. Invalid values WARN and keep the default. |
|
|
129
|
+
| `maxBackgroundSubagents` | number | unset | Ceiling on background subagents running at once (the doctor's running count, process-wide). A background `task`, or a `task_batch` with background items, that would exceed it is refused whole with an `isError` tool result naming the cap and the count; nothing reaches opencode. Foreground is never capped. An unreadable count (2 s budget) refuses. |
|
|
124
130
|
| `fallbackModels` | string[] | unset | Ordered models to try when the model a turn would run on is refused. Default for agents declaring no `fallbackModels`; a per-agent list replaces it rather than extending it. Same account throughout, never a switch. Armed only by the CLI refusing the model (`model_not_found`) or by a usage limit when the `accountFailover` form is not taking the turn, which is the case whenever it is `"off"` (its default) or has no other account to offer; with `"ask"` and another account the switch form wins. Entries must be registered model ids, unknown ones warn and are skipped, the current model is dropped from its own chain, each entry is tried at most once per turn, and an exhausted chain surfaces the original error. Never on compaction or title stubs; both transports. Writes a `▌ **model fallback:**` note that transcript rebuilds strip. Not independently read per expanded account. |
|
|
125
131
|
| `cwd` | string | automatic | Pin an absolute existing directory. Otherwise: session directory from SDK, usable `process.cwd()`, captured project directory, final `process.cwd()` fallback. Startup diagnostics cannot show the per-call session tier. |
|
|
126
132
|
| `skipPermissions` | boolean | `true` | Pass `--dangerously-skip-permissions` to Claude on both transports (the TUI also gets Claude Code's own `skipDangerousModePermissionPrompt` so it does not stop on that mode's confirmation; CLI 2.1.263+), even with proxies enabled. Proxied calls still use opencode permissions, but unproxied CLI tools do not. `false` removes the bypass flag; it does not by itself create human approval prompts. Ignored when `permissionMode` is `"plan"`, which always drops the flag. |
|
|
@@ -131,6 +137,7 @@ Defaults below describe normal headless opencode use when the key is absent.
|
|
|
131
137
|
| `controlRequestDenyMessage` | string | built-in text | Override ordinary deny text. `AskUserQuestion` always uses its own stop-and-wait message. |
|
|
132
138
|
| `proxyTools` | string[] | `["Bash", "Edit", "Write", "WebFetch", "Task"]` | Case-insensitive replacement list, not additive and not a capability allowlist. Known entries expose `mcp__opencode_proxy__<name>`; omitted/unknown tools are not disabled. `Task` also brings `task_batch`; `[]` disables this list, not MCP proxying. See the proxy table for exceptions. |
|
|
133
139
|
| `extraDisallowedTools` | string[] | unset | Claude built-ins to switch off outright with `--disallowedTools`, for tools that have no proxy (`["NotebookEdit"]`). Removes the capability rather than routing it. |
|
|
140
|
+
| `mapReadRules` | boolean | `true` | Carry opencode's own `read` permission **deny** rules onto the Read the CLI runs itself, as `--disallowedTools "Read(<pattern>)"`. `Read`, `Glob` and `Grep` are CLI-native by default, so without this a file opencode would refuse to read is read by Claude with no prompt (measured 2026-10-16: "read .env with your Read tool" returned a planted secret). Only `deny` is carried: an `ask` cannot be honoured on a spawn that has nobody to ask, and **opencode's own `*.env` default is an `ask`, not a deny**, so a default install maps nothing. Write `permission: { read: { "*.env": "deny" } }` to close that, or add `Read` to `proxyTools` so the read runs in opencode and its prompt applies. `glob`, `grep`, `list` and `external_directory` rules are not mapped (Claude's deny beats its allow, so "outside the working directory" cannot be said without denying the working directory); nor are agent-level `permission.read` overrides. `/claude-code-doctor` lists what was mapped, what was not, and the asks it cannot enforce. Compaction spawns get no rules. Set `false` to send nothing. |
|
|
134
141
|
| `proxyToolTimeoutMs` | object of proxy tool name to ms | unset | Optional wall-clock backstop per tool, in ms, case-insensitive keys. A proxied call normally ends on an event the plugin listens for, not on a timer: opencode's result, an abort (the CLI is interrupted), the next user message (calls the previous turn left pending are rejected as orphaned), the `claude` process exiting, the chat being deleted, or opencode exiting. Fallback 10 min (including dynamic MCP tools); `task` and `task_batch` have no deadline, so a subagent runs to completion and a chat parked in one holds its worker until one of those events; `question` 30 min. Set both task keys to cover both. A positive value replaces the default, `0` removes that tool's deadline, negative or non-numeric values are ignored, and values above 2147483647 are clamped. Bash `input.timeout` raises the resolved deadline (and restores one after `bash: 0`); executor ceilings still apply. The generated MCP client timeout is the largest effective deadline, or the CLI's maximum while any tool has none. `compress` is intercepted without a deadline. |
|
|
135
142
|
| `planModeQuestion` | boolean | `false` | Bridge `ExitPlanMode` approval to opencode's `question` and return a real CLI tool result. Requires a live question registry entry; otherwise keeps text fallback. Cannot fire on the headless transport: CLI 2.1.258 does not offer `ExitPlanMode` under `--print`, measured directly and through a full plugin probe, so the text path is what runs. On the interactive transport it fires and is verified live (1.18.34): the form's answer is pressed into the TUI's approval dialog. Headless prose yes/no is not a plan-mode unlock; interactive typed yes is. |
|
|
136
143
|
| `webSearch` | `"claude"` / `"disabled"` / `"<opencode tool name>"` | `"claude"` | Default: CLI search with the query rendered as text. Custom target forwards a tool call to an existing opencode tool accepting `query`; this is mapping, not the authenticated proxy replacement, so do not assume CLI search is suppressed. `"disabled"` disallows headless `WebSearch`. |
|
|
@@ -153,6 +160,8 @@ Defaults below describe normal headless opencode use when the key is absent.
|
|
|
153
160
|
| `resumeAfterRestart` | boolean | `true` | After an opencode restart, resume the conversation's Claude session (`--resume`) instead of replaying the thread as text. Persists session id + conversation digest per session key in `$XDG_STATE_HOME/opencode-claude-code-plugin/claude-sessions.json` (0600, 256 entries, 30 days) after each successful turn. Resumes only on the same session key and binary, with the transcript on disk and the history equal to the recorded conversation plus Claude's reply; anything else (edit, revert, compaction, account switch, open tool round trip) replays. Not on compaction. Log: `resuming the claude session from before the restart` (NOTICE) or `not resuming ...` with a reason (INFO). `false` disables reading and writing. |
|
|
154
161
|
| `resumeAcrossModelChanges` | boolean | `true` | When the operator changes a conversation's model or reasoning effort mid-conversation, continue the same Claude session (`--resume <id> --model <new>`) instead of replaying the thread as text. Both are in the session key, so changing either used to start a fresh Claude conversation (issue #91); neither needs one, because `--model` and `CLAUDE_CODE_EFFORT_LEVEL` are spawn-time and the CLI applies either to a resumed transcript (measured on 2.1.288). The session is MOVED off the old key, so one Claude conversation has exactly one owner. Refused on every term `resumeAfterRestart` is refused on (another account, a transcript that is gone, a conversation that changed) plus two more: a sibling whose transcript may still be written to, and anything that is not the same cwd, request scope, opencode session, agent and prompt-cache TTL. Not on compaction, not on an account-failover switch. It reads the record `resumeAfterRestart` writes, so `resumeAfterRestart: false` turns it off too. Log: `continuing this conversation's claude session under the new model or effort` (NOTICE). |
|
|
155
162
|
| `crossAccountResume` | boolean | `true` | On any account switch (a by-hand pick of another account's model in the same session, the switch form, and the switch back when the override expires), and only between accounts in the same `accountGroups` group, continue the same Claude conversation on the other account instead of replaying the thread as text. An account is a `CLAUDE_CONFIG_DIR` and a conversation is one `projects/<encoded cwd>/<id>.jsonl` inside it, so the switch copies that file into the target account's own projects dir (0600, dir 0700) and keeps the session id for the `--resume` both transports already build. Measured on CLI 2.1.288: a copied transcript resumes under another config dir with its context intact, the CLI resolves a session by FILENAME (not the `sessionId` in the records), it appends to the copy it found rather than to the cwd's path, the source comes out byte-identical, and no record holds a user, org or account id. Never overwrites: an older copy already at the target path (what a switch back finds) makes the carry take a fresh session id, and the live copy is the longest one. Refuses on nothing to carry, a non-file at the target path, or a failed copy, and each refusal falls back to the replay. Not on compaction, and inert with one account. Log: `carried this conversation's claude transcript to the other account` or `replaying this conversation as text on the other account` with a reason (both NOTICE). What is NOT verified live is a different Anthropic login answering another's conversation; `false` keeps the replay. |
|
|
163
|
+
| `usageWarnThresholds` | number[] | `[50, 75, 90, 95]` | Percent levels of a usage window at which a SERVED turn ends with one `▌ **usage:**` note (its own text part, stripped from rebuilt transcripts): account, window, utilization, reset in local time. Read from every headless `rate_limit_event` (not only a rejected one), whose `utilization` is a FRACTION (`0.9` is 90%) and is ABSENT on an `allowed` event with headroom, which is unknown, never zero, and crosses nothing. Readings persist per account per window in `$XDG_STATE_HOME/opencode-claude-code-plugin/account-usage.json` (`0600`, names, numbers and times only), and so does the dedup: each threshold is said once per account per window across reused processes, sessions and restarts, again only after the window's reset. A crossing in a `/compact` turn or a child session is not said there and is said by the next ordinary turn on the account. Never on `doGenerate`, never turns a served turn into a failure. `[]` turns the notes off but keeps the readings. The single-account name is `default`. Interactive transport: no event, so only a limit reply is recorded (`limited`). Log: `claude account usage crossed a warning threshold` (NOTICE). |
|
|
164
|
+
| `usageSwitchAt` | number / `false` | `90` | Percent at which work leaves an account BEFORE the wall, when another `failoverCandidates` account (same `accountGroups` group) exists and is not itself at that level per the store. `accountFailover: "auto"`: sets the existing account override (keyed on the account, until the window's reset; in memory, lost on restart) so the NEXT turn spawns on the other account, carried like a limit switch; the note says `New turns move to the "<account>" account until HH:MM`. `"ask"` / `"off"`: nothing moves and no form (the form only ends a FAILED turn); the note names the other account to pick a model from. The turn that crossed finishes as served. Also a threshold in its own right, so the switch is said exactly once per window. `false` or `0` never. Log: `claude account is near its usage limit; new turns move to another account until the window resets` (WARN). |
|
|
156
165
|
| `bridgeOpencodeSkills` | boolean | `false` | Stage the user's opencode skills for Claude's native Skill tool as `opencode-skills:<name>`, on the headless and interactive spawns (never compaction). Covers every root opencode reads: project `.opencode/`, `.claude/`, `.agents/` walking up, the opencode config dirs (`skill/` and `skills/`), and global `~/.claude/skills` and `~/.agents/skills` under opencode's own `OPENCODE_DISABLE_EXTERNAL_SKILLS` / `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` switches. Requires the CLI's `--help` to advertise `--plugin-dir`; otherwise no-op. Bridged skills are also listed in opencode's forwarded system prompt, so a large skill set costs prompt tokens twice, which is why it is off by default; `true` opts the user's skills in. Bundled skill staging ignores this option, but still requires flag support and successful discovery/staging. |
|
|
157
166
|
| `bridgeSkipNativeSkills` | boolean | `true` | Leave a skill unbridged when the Claude session already loads it: from `<CLAUDE_CONFIG_DIR>/skills`, the project's `.claude/skills`, or an installed plugin's `skills/`. Matched by resolved directory, by byte-identical SKILL.md, or (user/project scope only, since plugin skills are namespaced `<plugin>:<name>`) by name. A name match means `Skill("<name>")` answers from Claude's copy, not opencode's, so it is logged at WARN with both paths. The plugin scan reads `installed_plugins.json` and does not check whether the plugin is enabled. `false` bridges everything and reinstates the duplicates. |
|
|
158
167
|
| `transport` | `"auto"` / `"headless"` / `"interactive"` | unset (headless) | Explicit choice wins over `interactive` and its env var. `auto` prefers headless and probes required headless flags with free help/argument parsing and no prompt. Only definitive unsupported flags select the PTY; timeout, auth failure, missing binary and unknown output stay headless. Never retries a submitted request. Explicit or automatic PTY selection requires `Bun.Terminal`, otherwise errors. Title stubs do not probe. `/compact` follows the selected transport. On the PTY, `read-only` is `--restricted` plus `dontAsk` (CLI 2.1.263+, refused below rather than weakened) and plan mode parks `ExitPlanMode`'s dialog for the operator. `/claude-code-doctor usage` reports the free headless check unavailable when those flags are absent; it does not run a PTY inference prompt. |
|
|
@@ -397,6 +406,30 @@ comes from `organizationType` plus `organizationRateLimitTier` in
|
|
|
397
406
|
ever displayed or logged. Unreadable or unrecognised means no suffix. There is no
|
|
398
407
|
option for it.
|
|
399
408
|
|
|
409
|
+
### Usage before the limit
|
|
410
|
+
|
|
411
|
+
Every headless `rate_limit_event` is kept as a reading per account per window, so the
|
|
412
|
+
operator hears about a window filling up before it runs out (`usageWarnThresholds`,
|
|
413
|
+
`usageSwitchAt` in the table above):
|
|
414
|
+
|
|
415
|
+
```text
|
|
416
|
+
▌ **usage:** the Claude account "work" has used 76% of the 5-hour window, which resets
|
|
417
|
+
at 2026-10-17 18:00.
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
- One note per served turn whose account has an unannounced crossing, each threshold
|
|
421
|
+
once per account per window, persisted, so neither a reused process nor a restart
|
|
422
|
+
repeats it. Not on compaction, `doGenerate` or a child session.
|
|
423
|
+
- At `usageSwitchAt` with `"auto"` and an eligible other account, new turns move there
|
|
424
|
+
until the reset and the note says so; with `"ask"`/`"off"` the note names the account
|
|
425
|
+
to pick a model from. No form after a served turn.
|
|
426
|
+
- The CLI reports only when its view changes, so a reused process can be silent for
|
|
427
|
+
several turns; the last reading stands until a newer one or its reset.
|
|
428
|
+
- Where to look: the TUI sidebar's `Accounts` lines (`work · 5h 76% · resets 18:00`,
|
|
429
|
+
`ok` for a reading without a number, `limited` for a refusal) and the doctor's
|
|
430
|
+
`Account usage` table, which also shows which thresholds were already noted. "No
|
|
431
|
+
reading yet" there with a recent turn usually means the interactive transport.
|
|
432
|
+
|
|
400
433
|
### When an account runs out of usage
|
|
401
434
|
|
|
402
435
|
By default (`accountFailover` unset or `"off"`) a usage-limited turn ends with one
|
|
@@ -645,6 +678,8 @@ the opencode schema requires it. Markdown fallback reads top-level scalar fields
|
|
|
645
678
|
| `forceModel` | Registered bare model id, preserving the caller's account even if an account suffix is supplied. Works for any discovered agent mode. |
|
|
646
679
|
| `reasoningEffort` | `minimal`, `low`, `medium`, `high`, `xhigh`, `max`; invalid declarations warn and keep inherited effort. `minimal` maps to CLI `low`. Compaction skips this override. |
|
|
647
680
|
| `cacheTtl` | `5m` or `1h`; anything else warns and leaves the CLI's default alone. Exported as `CLAUDE_CODE_PROMPT_CACHE_TTL`, beating a shell export of the same name. Works for any discovered agent mode; compaction skips it. |
|
|
681
|
+
| `maxTurns` | Positive whole number, passed as `--max-turns` (per user message, CLI 2.1.280+). Any agent mode; headless only; part of the session key. |
|
|
682
|
+
| `maxBudgetUsd` | Positive dollars, passed as `--max-budget-usd` (per process life, reset on resume). Discovered `mode: subagent` agents only; on a main agent it is refused with a WARN. Headless only. |
|
|
648
683
|
| `fallbackModels` | Ordered registered bare model ids to try when this agent's model is refused. Both YAML spellings (`[a, b]` or a `- ` block). Replaces the provider-level `fallbackModels` rather than extending it. Keeps the caller's account; an entry carrying `@account` has it stripped. Unknown ids warn and are skipped. |
|
|
649
684
|
|
|
650
685
|
### Ask before each subagent dispatch
|
|
@@ -1339,8 +1374,12 @@ Prefer the doctor: it needs no logging change and no restart.
|
|
|
1339
1374
|
| On a turn that replayed the thread as text, Claude knows what tools the previous reply ran but not what it concluded | Plugin 0.49.0 or older kept each replayed message's first 2,000 characters, and Claude Code runs its own tools without ending a step, so a reply's tool calls and their output (one result alone is capped at 10,000 characters) sit in the same message ahead of the prose and the answer was cut | Upgrade. Each replayed message is clipped in the middle now, keeping the answer it ends on, and the newest reply gets a larger budget. Better still, avoid the replay: it only happens when the Claude session cannot be resumed, and the `▌` note or the plugin log names the reason |
|
|
1340
1375
|
| Claude forgot the whole conversation at once | Claude Code cleared it (`/clear` sent as a message, or a plan-mode exit that clears context) | Look for the `▌ **claude code reset:**` note. The plugin does not replay history there on purpose; a new opencode session gets a clean slate |
|
|
1341
1376
|
| After switching opencode agents (plan to build, build to plan) Claude ignores what was said under the other agent or answers an earlier message | Plugin older than 0.55.0: each agent runs its own `claude` process, and a reused one was sent only the newest message | Upgrade. The reused (or `--resume`d) process is now told what it missed, replayed as text ahead of the current message under `<missed_conversation>`; the log says `replaying the messages this claude session missed`. The processes stay separate on purpose (a Claude session keeps the system prompt of its first request) |
|
|
1377
|
+
| After an `/undo`, an edited message or a regenerated reply, Claude still acts on the turns that were removed | A `claude` session cannot have messages taken back out of its context, so the reused process still held them | Upgrade. The Claude session is dropped and the thread as it stands now is replayed into a fresh one, under a `▌ **conversation rewound:**` note; the log says `replaying the conversation as text: the thread was edited or undone` with `reason: history-rewound`. Not recognised when nothing of the old thread survives in place (an undo back to the first message, an edit of it), because that is also the shape of an opencode compaction, which must keep its process |
|
|
1342
1378
|
| `prompt is too long` on the first turn after a restart or after a lost Claude session, and `/compact` fails the same way | Plugin older than 0.55.0: the text replay of the thread had per-message clips but no total | Upgrade. The replay is capped at 180,000 characters, newest first, with a `[N earlier messages omitted ...]` line when it had to drop |
|
|
1343
1379
|
| opencode 2: every turn fails with `ENOENT ... posix_spawn '<claude>'` though the binary exists, only in a subdirectory of a project | Plugin older than 0.55.0 appended the session's project-relative `subpath` to its own absolute directory | Upgrade; drop a `cwd` set only to work around it |
|
|
1380
|
+
| Claude read a `.env` you thought opencode's `read` rules protected | opencode's own default for `*.env` is `ask`, not deny, and an `ask` cannot be carried onto a spawn with nobody to ask, so `mapReadRules` emits nothing for it. Check `/claude-code-doctor`'s read-rules section | Write `permission: { read: { "*.env": "deny", "*.env.*": "deny" } }` in opencode.json, which `mapReadRules` then carries onto Claude's own Read, or add `Read` to `proxyTools` so opencode's prompt governs it |
|
|
1381
|
+
| Claude read a file an opencode `read` **deny** rule covers | A deny is carried across by default. The doctor's read-rules section names an unmapped pattern (one with `..`) and the allows a deny now shadows; a `*` mid-path is narrower on Claude's side than in opencode | Reword the pattern (a trailing `/*` and a slash-free `*.env` both map exactly), proxy `Read`, or use `permissionPreset: "read-only"` |
|
|
1382
|
+
| A Claude Code hook on `Bash`, `Edit` or `Write` no longer fires | The tool is proxied, so Claude sees it as `mcp__opencode_proxy__<tool>` and the matcher misses | Name the proxied tool in the matcher (`mcp__opencode_proxy__bash`); hooks on CLI-native tools (`Read`) still fire |
|
|
1344
1383
|
| Wanting the per-turn cost in the chat | Not shown by default | Set `turnStats: true` and restart opencode |
|
|
1345
1384
|
| On the interactive transport, every turn ends with a `▌ **claude code error:**` note naming `end_turn` (or `stop_sequence` / `max_tokens`), and `turnStats` never prints | Plugin older than 0.33.0 put the stop reason in the synthesized `result`'s `subtype`, and any non-`success` subtype finishes the turn as an error, which also suppresses the stats footer | Upgrade and relaunch. A turn that reaches a terminal stop reason now synthesizes the shape a headless turn emits (`subtype: "success"`, the stop reason in a top-level `stop_reason`), so it finishes as an ordinary reply. A turn that reaches NO terminal stop reason is still an error on purpose, so truncation stays visible |
|
|
1346
1385
|
| On the interactive transport with a working directory under `/tmp` (or any symlinked path), the turn hangs until the 30-minute turn timeout and then reports no terminal stop reason | Plugin older than 0.33.0 named the transcript directory from `path.resolve`, which does not follow symlinks, so it tailed a file Claude Code never writes. On macOS `/tmp` is a symlink to `/private/tmp` | Upgrade and relaunch. The directory is now named from the cwd's resolved real path, which is what the CLI uses (`/tmp/scratch` is `~/.claude/projects/-private-tmp-scratch`). Check the `jsonlPath` in the `prepared interactive claude session` log line against the directory that actually exists under `<CLAUDE_CONFIG_DIR>/projects/` |
|