@kolisachint/hoocode-agent 0.4.110 → 0.4.111
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/CHANGELOG.md +61 -0
- package/dist/core/format-duration.d.ts +7 -0
- package/dist/core/format-duration.d.ts.map +1 -0
- package/dist/core/format-duration.js +16 -0
- package/dist/core/format-duration.js.map +1 -0
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +10 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/lifeguard.d.ts +15 -0
- package/dist/core/lifeguard.d.ts.map +1 -1
- package/dist/core/lifeguard.js +48 -18
- package/dist/core/lifeguard.js.map +1 -1
- package/dist/core/subagent-inbox.d.ts +1 -1
- package/dist/core/subagent-inbox.d.ts.map +1 -1
- package/dist/core/subagent-inbox.js +2 -0
- package/dist/core/subagent-inbox.js.map +1 -1
- package/dist/core/subagent-pool-instance.d.ts.map +1 -1
- package/dist/core/subagent-pool-instance.js +18 -11
- package/dist/core/subagent-pool-instance.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +25 -3
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +120 -25
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/subagent-result.d.ts.map +1 -1
- package/dist/core/subagent-result.js +6 -4
- package/dist/core/subagent-result.js.map +1 -1
- package/dist/core/task-store.d.ts +45 -3
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +51 -5
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/team-view.d.ts.map +1 -1
- package/dist/core/team-view.js +2 -0
- package/dist/core/team-view.js.map +1 -1
- package/dist/core/token-budget.d.ts +13 -1
- package/dist/core/token-budget.d.ts.map +1 -1
- package/dist/core/token-budget.js +18 -1
- package/dist/core/token-budget.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +1 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +184 -53
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/core/tools/todo.d.ts.map +1 -1
- package/dist/core/tools/todo.js +39 -7
- package/dist/core/tools/todo.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +10 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +27 -1
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +22 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +302 -80
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/components/team-attach-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/team-attach-panel.js +15 -5
- package/dist/modes/interactive/components/team-attach-panel.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +8 -1
- package/dist/modes/interactive/theme/light.json +8 -1
- package/dist/modes/interactive/theme/theme-schema.json +28 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +52 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.js +5 -2
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
- package/dist/modes/rpc/jsonl.d.ts +16 -1
- package/dist/modes/rpc/jsonl.d.ts.map +1 -1
- package/dist/modes/rpc/jsonl.js +53 -8
- package/dist/modes/rpc/jsonl.js.map +1 -1
- package/dist/utils/atomic-file.d.ts +13 -0
- package/dist/utils/atomic-file.d.ts.map +1 -0
- package/dist/utils/atomic-file.js +26 -0
- package/dist/utils/atomic-file.js.map +1 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team-attach-panel.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/team-attach-panel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Attach side-panel for a hooteams role (`a` in team focus).\n *\n * Renders one role's live TeamEvents in the style of hooteams' StreamRenderer\n * (`hooteams attach <role>`): ◉ lifecycle lines, dim italic thinking, inline\n * streaming text, ✓/✗ tool results, dim per-turn usage stamps. Differences\n * from the CLI renderer are dictated by the host: colors go through hoocode's\n * theme helpers (no raw ANSI) and output lands in a bounded ring buffer\n * instead of an unbounded stdout stream.\n *\n * The panel filters the team connection's single shared /events subscription —\n * it never opens its own SSE connection — and unsubscribes on dispose(), so\n * attach/detach cycles leave no leaked subscribers.\n *\n * Approval gates: task_* lifecycle events render as stream lines, and when the\n * attached role pauses, presentApproval() embeds the AskOptions pane right\n * where the stream stopped — pick an option (or type a free-form answer) and\n * the caller answers the server; the stream then carries on under a\n * \"✓ answered: …\" stamp.\n */\n\nimport type { Component, Focusable, TUI } from \"@kolisachint/hoocode-tui\";\nimport { getKeybindings, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@kolisachint/hoocode-tui\";\nimport type { AskQuestion } from \"../../../core/extensions/types.js\";\nimport type { TeamApproval } from \"../../../core/team-approvals.js\";\nimport type { TeamViewConnection, TeamViewEvent } from \"../../../core/team-view.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { AskOptionsComponent } from \"./ask-options.js\";\n\n/** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */\nexport class RingBuffer<T> {\n\tprivate readonly slots: (T | undefined)[];\n\tprivate start = 0;\n\tprivate count = 0;\n\n\tconstructor(readonly capacity: number) {\n\t\tif (!Number.isInteger(capacity) || capacity <= 0) throw new Error(`invalid ring buffer capacity ${capacity}`);\n\t\tthis.slots = new Array<T | undefined>(capacity);\n\t}\n\n\tpush(item: T): void {\n\t\tconst end = (this.start + this.count) % this.capacity;\n\t\tthis.slots[end] = item;\n\t\tif (this.count < this.capacity) this.count++;\n\t\telse this.start = (this.start + 1) % this.capacity;\n\t}\n\n\tget length(): number {\n\t\treturn this.count;\n\t}\n\n\ttoArray(): T[] {\n\t\tconst out: T[] = [];\n\t\tfor (let i = 0; i < this.count; i++) {\n\t\t\tout.push(this.slots[(this.start + i) % this.capacity] as T);\n\t\t}\n\t\treturn out;\n\t}\n}\n\nfunction argsPreview(args: unknown, max = 60): string {\n\tconst text = JSON.stringify(args) ?? \"\";\n\treturn text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nexport interface TeamAttachPanelCallbacks {\n\t/** `q`/esc: close the panel; the role keeps running. */\n\tonDetach: () => void;\n\t/** `n`: nudge the attached role. */\n\tonNudge: (role: string) => void;\n}\n\n/** Logical event lines kept in the buffer (wrapped to width at render time). */\nconst DEFAULT_BUFFER_LINES = 200;\n\nexport class TeamAttachPanelComponent implements Component, Focusable {\n\tfocused = false;\n\n\tprivate readonly lines: RingBuffer<string>;\n\t/** Streaming tail (text/thinking deltas) not yet terminated by a line break. */\n\tprivate partial = \"\";\n\tprivate partialKind: \"text\" | \"thinking\" = \"text\";\n\tprivate unsubscribe: (() => void) | undefined;\n\t/** Gate currently embedded in the panel; input is delegated to it. */\n\tprivate approval: { component: AskOptionsComponent; settle: (answer: string | undefined) => void } | undefined;\n\n\tconstructor(\n\t\treadonly role: string,\n\t\tconnection: TeamViewConnection,\n\t\tprivate readonly callbacks: TeamAttachPanelCallbacks,\n\t\tprivate readonly ui?: TUI,\n\t\tbufferLines = DEFAULT_BUFFER_LINES,\n\t) {\n\t\tthis.lines = new RingBuffer(bufferLines);\n\t\t// Filter the shared stream down to this role; no second SSE connection.\n\t\tthis.unsubscribe = connection.subscribe((event) => {\n\t\t\tif (event.role !== this.role) return;\n\t\t\tthis.applyEvent(event);\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\t/** Detach from the shared event stream; settles any open gate as skipped. Idempotent. */\n\tdispose(): void {\n\t\tthis.unsubscribe?.();\n\t\tthis.unsubscribe = undefined;\n\t\tthis.approval?.settle(undefined);\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\t/**\n\t * Embed one approval gate in the panel (the attached role paused). Resolves\n\t * with the chosen or free-form answer; undefined when skipped (esc), when\n\t * the signal aborts (answered elsewhere), or when the panel is disposed.\n\t * Answering the server is the caller's job — on an answer the panel just\n\t * stamps \"✓ answered: …\" into the stream.\n\t */\n\tpresentApproval(approval: TeamApproval, signal: AbortSignal): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (signal.aborted || this.unsubscribe === undefined) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// The coordinator shows one gate at a time; a stray second call\n\t\t\t// settles the first as skipped instead of stacking panes.\n\t\t\tthis.approval?.settle(undefined);\n\n\t\t\tlet settled = false;\n\t\t\tconst settle = (answer: string | undefined): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\tthis.approval = undefined;\n\t\t\t\tif (answer !== undefined) {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"success\", ` ✓ answered: ${answer}`));\n\t\t\t\t}\n\t\t\t\tthis.ui?.requestRender();\n\t\t\t\tresolve(answer);\n\t\t\t};\n\t\t\tconst onAbort = (): void => settle(undefined);\n\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tconst question: AskQuestion = {\n\t\t\t\tquestion: approval.question,\n\t\t\t\tshort: approval.taskId,\n\t\t\t\tdetail: `team task \"${approval.taskId}\" is paused until answered`,\n\t\t\t\toptions: approval.options.map((label) => ({ label })),\n\t\t\t\tallowCustom: true,\n\t\t\t};\n\t\t\tconst component = new AskOptionsComponent(\n\t\t\t\t[question],\n\t\t\t\t(answers) => settle(answers[0]),\n\t\t\t\t() => settle(undefined),\n\t\t\t);\n\t\t\tcomponent.focused = this.focused;\n\t\t\tthis.approval = { component, settle };\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\thandleInput(data: string): void {\n\t\t// An open gate owns the keyboard: q/n must type into the custom row, not\n\t\t// detach or nudge. esc skips the gate (AskOptions cancel), not the panel.\n\t\tif (this.approval) {\n\t\t\tthis.approval.component.handleInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"q\") || getKeybindings().matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.callbacks.onDetach();\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"n\")) {\n\t\t\tthis.callbacks.onNudge(this.role);\n\t\t}\n\t}\n\n\tprivate styledPartial(): string {\n\t\treturn this.partialKind === \"thinking\" ? theme.italic(theme.fg(\"dim\", this.partial)) : this.partial;\n\t}\n\n\t/** Flush the streaming tail into the buffer as a finished line. */\n\tprivate breakLine(): void {\n\t\tif (this.partial.length === 0) return;\n\t\tthis.lines.push(this.styledPartial());\n\t\tthis.partial = \"\";\n\t}\n\n\t/** Append streaming delta text, completing buffer lines at embedded newlines. */\n\tprivate appendDelta(delta: string, kind: \"text\" | \"thinking\"): void {\n\t\tif (this.partialKind !== kind) this.breakLine();\n\t\tthis.partialKind = kind;\n\t\tconst parts = delta.split(\"\\n\");\n\t\tfor (let i = 0; i < parts.length - 1; i++) {\n\t\t\tthis.partial += parts[i];\n\t\t\tthis.breakLine();\n\t\t\tthis.partialKind = kind;\n\t\t}\n\t\tthis.partial += parts[parts.length - 1];\n\t}\n\n\t/** Mirrors hooteams' StreamRenderer event handling, themed and buffered. */\n\tprivate applyEvent(event: TeamViewEvent): void {\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"message_update\": {\n\t\t\t\tconst delta = event.assistantMessageEvent;\n\t\t\t\tif (!delta) break;\n\t\t\t\tif (delta.type === \"thinking_start\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"dim\", \"◉ thinking…\"));\n\t\t\t\t} else if (delta.type === \"thinking_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"thinking\");\n\t\t\t\t} else if (delta.type === \"thinking_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t} else if (delta.type === \"text_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"text\");\n\t\t\t\t} else if (delta.type === \"text_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\"accent\", \"◉ tool: \") +\n\t\t\t\t\t\ttheme.bold(theme.fg(\"accent\", event.toolName ?? \"?\")) +\n\t\t\t\t\t\ttheme.fg(\"accent\", `(${argsPreview(event.args)})`) +\n\t\t\t\t\t\t\" \" +\n\t\t\t\t\t\ttheme.fg(\"warning\", \"running…\"),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.isError\n\t\t\t\t\t\t? theme.fg(\"error\", ` ✗ ${event.toolName ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", ` ✓ ${event.toolName ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\": {\n\t\t\t\tthis.breakLine();\n\t\t\t\tconst usage = event.message?.usage;\n\t\t\t\tif (usage) {\n\t\t\t\t\tconst cost = usage.cost?.total ? ` $${usage.cost.total.toFixed(4)}` : \"\";\n\t\t\t\t\tthis.lines.push(\n\t\t\t\t\t\ttheme.fg(\"dim\", `— turn: ${usage.input ?? 0} in / ${usage.output ?? 0} out tokens${cost}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (event.message?.errorMessage) {\n\t\t\t\t\tthis.lines.push(theme.fg(\"error\", `error: ${event.message.errorMessage}`));\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"agent_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} idle`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_started\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ task ${event.taskId ?? \"?\"} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_paused\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"warning\", `⏸ awaiting approval: ${event.question ?? \"?\"}`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_resumed\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"accent\",\n\t\t\t\t\t\t`▶ task ${event.taskId ?? \"?\"} resumed${event.chosenOption ? `: ${event.chosenOption}` : \"\"}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"task_finished\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.status === \"error\"\n\t\t\t\t\t\t? theme.fg(\"error\", `✗ task ${event.taskId ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", `✓ task ${event.taskId ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Buffered logical line count (completed lines only). Exposed for tests. */\n\tbufferedLineCount(): number {\n\t\treturn this.lines.length;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst inner = Math.max(1, width);\n\n\t\t// Header: role identity left, key hints right. An open gate owns the\n\t\t// keyboard and renders its own hints, so the panel's would lie.\n\t\tconst titlePlain = `◉ ${this.role} — attached`;\n\t\tconst title =\n\t\t\ttheme.fg(\"accent\", \"◉ \") + theme.bold(theme.fg(\"accent\", this.role)) + theme.fg(\"muted\", \" — attached\");\n\t\tconst hintsPlain = this.approval ? \"\" : \"n nudge · q detach\";\n\t\tconst hints = theme.fg(\"dim\", hintsPlain);\n\t\tlet header: string;\n\t\tif (visibleWidth(titlePlain) + 2 + visibleWidth(hintsPlain) <= inner) {\n\t\t\theader = title + \" \".repeat(inner - visibleWidth(titlePlain) - visibleWidth(hintsPlain)) + hints;\n\t\t} else {\n\t\t\theader = truncateToWidth(title, inner, \"…\");\n\t\t}\n\t\tconst rule = theme.fg(\"borderMuted\", \"─\".repeat(inner));\n\n\t\t// Body: wrap each buffered line, then keep the newest rows that fit the\n\t\t// panel's height budget (the freshest output hugs the bottom, like a\n\t\t// terminal tail). Budget derives from the live terminal height so the\n\t\t// panel never asks the overlay compositor to clip it (clipping drops the\n\t\t// bottom — exactly the rows we care about).\n\t\tconst rows = this.ui?.terminal.rows ?? 24;\n\t\tconst bodyBudget = Math.max(5, Math.floor(rows * 0.6) - 3);\n\n\t\t// An open gate takes its rows out of the stream's budget: the freshest\n\t\t// output stays visible above the question for context.\n\t\tlet approvalLines: string[] = [];\n\t\tif (this.approval) {\n\t\t\tthis.approval.component.focused = this.focused;\n\t\t\tapprovalLines = this.approval.component.render(inner);\n\t\t}\n\n\t\tconst wrapped: string[] = [];\n\t\tfor (const line of this.lines.toArray()) {\n\t\t\tif (line.length === 0) {\n\t\t\t\twrapped.push(\"\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twrapped.push(...wrapTextWithAnsi(line, inner));\n\t\t}\n\t\tif (this.partial.length > 0) {\n\t\t\twrapped.push(...wrapTextWithAnsi(this.styledPartial(), inner));\n\t\t}\n\t\tconst keep = Math.max(0, bodyBudget - approvalLines.length);\n\t\tconst body = keep > 0 ? wrapped.slice(-keep) : [];\n\t\tif (body.length === 0 && approvalLines.length === 0) {\n\t\t\tbody.push(theme.fg(\"dim\", \"waiting for events…\"));\n\t\t}\n\n\t\t// The gate brings its own accent rules, so it closes the panel itself.\n\t\tif (approvalLines.length > 0) return [header, rule, ...body, ...approvalLines];\n\t\treturn [header, rule, ...body, rule];\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"team-attach-panel.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/team-attach-panel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Attach side-panel for a hooteams role (`a` in team focus).\n *\n * Renders one role's live TeamEvents in the style of hooteams' StreamRenderer\n * (`hooteams attach <role>`): ◉ lifecycle lines, dim italic thinking, inline\n * streaming text, ✓/✗ tool results, dim per-turn usage stamps. Differences\n * from the CLI renderer are dictated by the host: colors go through hoocode's\n * theme helpers (no raw ANSI) and output lands in a bounded ring buffer\n * instead of an unbounded stdout stream.\n *\n * The panel filters the team connection's single shared /events subscription —\n * it never opens its own SSE connection — and unsubscribes on dispose(), so\n * attach/detach cycles leave no leaked subscribers.\n *\n * Approval gates: task_* lifecycle events render as stream lines, and when the\n * attached role pauses, presentApproval() embeds the AskOptions pane right\n * where the stream stopped — pick an option (or type a free-form answer) and\n * the caller answers the server; the stream then carries on under a\n * \"✓ answered: …\" stamp.\n */\n\nimport type { Component, Focusable, TUI } from \"@kolisachint/hoocode-tui\";\nimport { getKeybindings, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@kolisachint/hoocode-tui\";\nimport type { AskQuestion } from \"../../../core/extensions/types.js\";\nimport type { TeamApproval } from \"../../../core/team-approvals.js\";\nimport type { TeamViewConnection, TeamViewEvent } from \"../../../core/team-view.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { AskOptionsComponent } from \"./ask-options.js\";\nimport { appKeyLabel, matchesAppKey, rawKeyHint } from \"./keybinding-hints.js\";\n\n/** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */\nexport class RingBuffer<T> {\n\tprivate readonly slots: (T | undefined)[];\n\tprivate start = 0;\n\tprivate count = 0;\n\n\tconstructor(readonly capacity: number) {\n\t\tif (!Number.isInteger(capacity) || capacity <= 0) throw new Error(`invalid ring buffer capacity ${capacity}`);\n\t\tthis.slots = new Array<T | undefined>(capacity);\n\t}\n\n\tpush(item: T): void {\n\t\tconst end = (this.start + this.count) % this.capacity;\n\t\tthis.slots[end] = item;\n\t\tif (this.count < this.capacity) this.count++;\n\t\telse this.start = (this.start + 1) % this.capacity;\n\t}\n\n\tget length(): number {\n\t\treturn this.count;\n\t}\n\n\ttoArray(): T[] {\n\t\tconst out: T[] = [];\n\t\tfor (let i = 0; i < this.count; i++) {\n\t\t\tout.push(this.slots[(this.start + i) % this.capacity] as T);\n\t\t}\n\t\treturn out;\n\t}\n}\n\nfunction argsPreview(args: unknown, max = 60): string {\n\tconst text = JSON.stringify(args) ?? \"\";\n\treturn text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nexport interface TeamAttachPanelCallbacks {\n\t/** `q`/esc: close the panel; the role keeps running. */\n\tonDetach: () => void;\n\t/** `n`: nudge the attached role. */\n\tonNudge: (role: string) => void;\n}\n\n/** Logical event lines kept in the buffer (wrapped to width at render time). */\nconst DEFAULT_BUFFER_LINES = 200;\n\nexport class TeamAttachPanelComponent implements Component, Focusable {\n\tfocused = false;\n\n\tprivate readonly lines: RingBuffer<string>;\n\t/** Streaming tail (text/thinking deltas) not yet terminated by a line break. */\n\tprivate partial = \"\";\n\tprivate partialKind: \"text\" | \"thinking\" = \"text\";\n\tprivate unsubscribe: (() => void) | undefined;\n\t/** Gate currently embedded in the panel; input is delegated to it. */\n\tprivate approval: { component: AskOptionsComponent; settle: (answer: string | undefined) => void } | undefined;\n\n\tconstructor(\n\t\treadonly role: string,\n\t\tconnection: TeamViewConnection,\n\t\tprivate readonly callbacks: TeamAttachPanelCallbacks,\n\t\tprivate readonly ui?: TUI,\n\t\tbufferLines = DEFAULT_BUFFER_LINES,\n\t) {\n\t\tthis.lines = new RingBuffer(bufferLines);\n\t\t// Filter the shared stream down to this role; no second SSE connection.\n\t\tthis.unsubscribe = connection.subscribe((event) => {\n\t\t\tif (event.role !== this.role) return;\n\t\t\tthis.applyEvent(event);\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\t/** Detach from the shared event stream; settles any open gate as skipped. Idempotent. */\n\tdispose(): void {\n\t\tthis.unsubscribe?.();\n\t\tthis.unsubscribe = undefined;\n\t\tthis.approval?.settle(undefined);\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\t/**\n\t * Embed one approval gate in the panel (the attached role paused). Resolves\n\t * with the chosen or free-form answer; undefined when skipped (esc), when\n\t * the signal aborts (answered elsewhere), or when the panel is disposed.\n\t * Answering the server is the caller's job — on an answer the panel just\n\t * stamps \"✓ answered: …\" into the stream.\n\t */\n\tpresentApproval(approval: TeamApproval, signal: AbortSignal): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (signal.aborted || this.unsubscribe === undefined) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// The coordinator shows one gate at a time; a stray second call\n\t\t\t// settles the first as skipped instead of stacking panes.\n\t\t\tthis.approval?.settle(undefined);\n\n\t\t\tlet settled = false;\n\t\t\tconst settle = (answer: string | undefined): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\tthis.approval = undefined;\n\t\t\t\tif (answer !== undefined) {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"success\", ` ✓ answered: ${answer}`));\n\t\t\t\t}\n\t\t\t\tthis.ui?.requestRender();\n\t\t\t\tresolve(answer);\n\t\t\t};\n\t\t\tconst onAbort = (): void => settle(undefined);\n\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tconst question: AskQuestion = {\n\t\t\t\tquestion: approval.question,\n\t\t\t\tshort: approval.taskId,\n\t\t\t\tdetail: `team task \"${approval.taskId}\" is paused until answered`,\n\t\t\t\toptions: approval.options.map((label) => ({ label })),\n\t\t\t\tallowCustom: true,\n\t\t\t};\n\t\t\tconst component = new AskOptionsComponent(\n\t\t\t\t[question],\n\t\t\t\t(answers) => settle(answers[0]),\n\t\t\t\t() => settle(undefined),\n\t\t\t);\n\t\t\tcomponent.focused = this.focused;\n\t\t\tthis.approval = { component, settle };\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\thandleInput(data: string): void {\n\t\t// An open gate owns the keyboard: q/n must type into the custom row, not\n\t\t// detach or nudge. esc skips the gate (AskOptions cancel), not the panel.\n\t\tif (this.approval) {\n\t\t\tthis.approval.component.handleInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"q\") || getKeybindings().matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.callbacks.onDetach();\n\t\t\treturn;\n\t\t}\n\t\tif (matchesAppKey(data, \"app.team.nudge\")) {\n\t\t\tthis.callbacks.onNudge(this.role);\n\t\t}\n\t}\n\n\tprivate styledPartial(): string {\n\t\treturn this.partialKind === \"thinking\" ? theme.italic(theme.fg(\"dim\", this.partial)) : this.partial;\n\t}\n\n\t/** Flush the streaming tail into the buffer as a finished line. */\n\tprivate breakLine(): void {\n\t\tif (this.partial.length === 0) return;\n\t\tthis.lines.push(this.styledPartial());\n\t\tthis.partial = \"\";\n\t}\n\n\t/** Append streaming delta text, completing buffer lines at embedded newlines. */\n\tprivate appendDelta(delta: string, kind: \"text\" | \"thinking\"): void {\n\t\tif (this.partialKind !== kind) this.breakLine();\n\t\tthis.partialKind = kind;\n\t\tconst parts = delta.split(\"\\n\");\n\t\tfor (let i = 0; i < parts.length - 1; i++) {\n\t\t\tthis.partial += parts[i];\n\t\t\tthis.breakLine();\n\t\t\tthis.partialKind = kind;\n\t\t}\n\t\tthis.partial += parts[parts.length - 1];\n\t}\n\n\t/** Mirrors hooteams' StreamRenderer event handling, themed and buffered. */\n\tprivate applyEvent(event: TeamViewEvent): void {\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"message_update\": {\n\t\t\t\tconst delta = event.assistantMessageEvent;\n\t\t\t\tif (!delta) break;\n\t\t\t\tif (delta.type === \"thinking_start\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"dim\", \"◉ thinking…\"));\n\t\t\t\t} else if (delta.type === \"thinking_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"thinking\");\n\t\t\t\t} else if (delta.type === \"thinking_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t} else if (delta.type === \"text_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"text\");\n\t\t\t\t} else if (delta.type === \"text_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\"accent\", \"◉ tool: \") +\n\t\t\t\t\t\ttheme.bold(theme.fg(\"accent\", event.toolName ?? \"?\")) +\n\t\t\t\t\t\ttheme.fg(\"accent\", `(${argsPreview(event.args)})`) +\n\t\t\t\t\t\t\" \" +\n\t\t\t\t\t\ttheme.fg(\"warning\", \"running…\"),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.isError\n\t\t\t\t\t\t? theme.fg(\"error\", ` ✗ ${event.toolName ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", ` ✓ ${event.toolName ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\": {\n\t\t\t\tthis.breakLine();\n\t\t\t\tconst usage = event.message?.usage;\n\t\t\t\tif (usage) {\n\t\t\t\t\tconst cost = usage.cost?.total ? ` $${usage.cost.total.toFixed(4)}` : \"\";\n\t\t\t\t\tthis.lines.push(\n\t\t\t\t\t\ttheme.fg(\"dim\", `— turn: ${usage.input ?? 0} in / ${usage.output ?? 0} out tokens${cost}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (event.message?.errorMessage) {\n\t\t\t\t\tthis.lines.push(theme.fg(\"error\", `error: ${event.message.errorMessage}`));\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"agent_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} idle`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_started\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ task ${event.taskId ?? \"?\"} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_paused\":\n\t\t\t\tthis.breakLine();\n\t\t\t\t// VS15 (U+FE0E) forces text presentation: bare ⏸/▶ carry the Unicode\n\t\t\t\t// Emoji property and emoji-font fallback renders them double-width,\n\t\t\t\t// breaking the width math that counts one cell.\n\t\t\t\tthis.lines.push(theme.fg(\"warning\", `⏸︎ awaiting approval: ${event.question ?? \"?\"}`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_resumed\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"accent\",\n\t\t\t\t\t\t`▶︎ task ${event.taskId ?? \"?\"} resumed${event.chosenOption ? `: ${event.chosenOption}` : \"\"}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"task_finished\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.status === \"error\"\n\t\t\t\t\t\t? theme.fg(\"error\", `✗ task ${event.taskId ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", `✓ task ${event.taskId ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Buffered logical line count (completed lines only). Exposed for tests. */\n\tbufferedLineCount(): number {\n\t\treturn this.lines.length;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst inner = Math.max(1, width);\n\n\t\t// Header: role identity left, key hints right. An open gate owns the\n\t\t// keyboard and renders its own hints, so the panel's would lie.\n\t\tconst titlePlain = `◉ ${this.role} — attached`;\n\t\tconst title =\n\t\t\ttheme.fg(\"accent\", \"◉ \") + theme.bold(theme.fg(\"accent\", this.role)) + theme.fg(\"muted\", \" — attached\");\n\t\t// House hint style: dim key + muted description, muted · separator. The\n\t\t// nudge key resolves from the live keybinding config (same binding the\n\t\t// task panel's team focus uses); q stays a literal by convention.\n\t\tconst nudgeKey = appKeyLabel(\"app.team.nudge\");\n\t\tconst hintsPlain = this.approval ? \"\" : `${nudgeKey} nudge · q detach`;\n\t\tconst hints = this.approval\n\t\t\t? \"\"\n\t\t\t: rawKeyHint(nudgeKey, \"nudge\") + theme.fg(\"muted\", \" · \") + rawKeyHint(\"q\", \"detach\");\n\t\tlet header: string;\n\t\tif (visibleWidth(titlePlain) + 2 + visibleWidth(hintsPlain) <= inner) {\n\t\t\theader = title + \" \".repeat(inner - visibleWidth(titlePlain) - visibleWidth(hintsPlain)) + hints;\n\t\t} else {\n\t\t\theader = truncateToWidth(title, inner, \"…\");\n\t\t}\n\t\tconst rule = theme.fg(\"borderMuted\", \"─\".repeat(inner));\n\n\t\t// Body: wrap each buffered line, then keep the newest rows that fit the\n\t\t// panel's height budget (the freshest output hugs the bottom, like a\n\t\t// terminal tail). Budget derives from the live terminal height so the\n\t\t// panel never asks the overlay compositor to clip it (clipping drops the\n\t\t// bottom — exactly the rows we care about).\n\t\tconst rows = this.ui?.terminal.rows ?? 24;\n\t\tconst bodyBudget = Math.max(5, Math.floor(rows * 0.6) - 3);\n\n\t\t// An open gate takes its rows out of the stream's budget: the freshest\n\t\t// output stays visible above the question for context.\n\t\tlet approvalLines: string[] = [];\n\t\tif (this.approval) {\n\t\t\tthis.approval.component.focused = this.focused;\n\t\t\tapprovalLines = this.approval.component.render(inner);\n\t\t}\n\n\t\tconst wrapped: string[] = [];\n\t\tfor (const line of this.lines.toArray()) {\n\t\t\tif (line.length === 0) {\n\t\t\t\twrapped.push(\"\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twrapped.push(...wrapTextWithAnsi(line, inner));\n\t\t}\n\t\tif (this.partial.length > 0) {\n\t\t\twrapped.push(...wrapTextWithAnsi(this.styledPartial(), inner));\n\t\t}\n\t\tconst keep = Math.max(0, bodyBudget - approvalLines.length);\n\t\tconst body = keep > 0 ? wrapped.slice(-keep) : [];\n\t\tif (body.length === 0 && approvalLines.length === 0) {\n\t\t\tbody.push(theme.fg(\"dim\", \"waiting for events…\"));\n\t\t}\n\n\t\t// The gate brings its own accent rules, so it closes the panel itself.\n\t\tif (approvalLines.length > 0) return [header, rule, ...body, ...approvalLines];\n\t\treturn [header, rule, ...body, rule];\n\t}\n}\n"]}
|
|
@@ -75,7 +75,14 @@
|
|
|
75
75
|
"thinkingHigh": "#b294bb",
|
|
76
76
|
"thinkingXhigh": "#d183e8",
|
|
77
77
|
|
|
78
|
-
"bashMode": "green"
|
|
78
|
+
"bashMode": "green",
|
|
79
|
+
|
|
80
|
+
"agent1": "#56b6c2",
|
|
81
|
+
"agent2": "#c678dd",
|
|
82
|
+
"agent3": "#d19a66",
|
|
83
|
+
"agent4": "#ff87af",
|
|
84
|
+
"agent5": "#4ec9b0",
|
|
85
|
+
"agent6": "#7aa2f7"
|
|
79
86
|
},
|
|
80
87
|
"export": {
|
|
81
88
|
"pageBg": "#18181e",
|
|
@@ -74,7 +74,14 @@
|
|
|
74
74
|
"thinkingHigh": "#875f87",
|
|
75
75
|
"thinkingXhigh": "#8b008b",
|
|
76
76
|
|
|
77
|
-
"bashMode": "green"
|
|
77
|
+
"bashMode": "green",
|
|
78
|
+
|
|
79
|
+
"agent1": "#0b7285",
|
|
80
|
+
"agent2": "#9c36b5",
|
|
81
|
+
"agent3": "#d9480f",
|
|
82
|
+
"agent4": "#c2255c",
|
|
83
|
+
"agent5": "#087f5b",
|
|
84
|
+
"agent6": "#3b5bdb"
|
|
78
85
|
},
|
|
79
86
|
"export": {
|
|
80
87
|
"pageBg": "#f8f8f8",
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
"title": "Pi Coding Agent Theme",
|
|
4
4
|
"description": "Theme schema for Pi coding agent",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"required": [
|
|
6
|
+
"required": [
|
|
7
|
+
"name",
|
|
8
|
+
"colors"
|
|
9
|
+
],
|
|
7
10
|
"properties": {
|
|
8
11
|
"$schema": {
|
|
9
12
|
"type": "string",
|
|
@@ -291,6 +294,30 @@
|
|
|
291
294
|
"bashMode": {
|
|
292
295
|
"$ref": "#/$defs/colorValue",
|
|
293
296
|
"description": "Editor border color in bash mode"
|
|
297
|
+
},
|
|
298
|
+
"agent1": {
|
|
299
|
+
"$ref": "#/$defs/colorValue",
|
|
300
|
+
"description": "Agent identity palette slot 1 (optional; subagent types hash into agent1-agent6, falls back to accent)"
|
|
301
|
+
},
|
|
302
|
+
"agent2": {
|
|
303
|
+
"$ref": "#/$defs/colorValue",
|
|
304
|
+
"description": "Agent identity palette slot 2 (optional; subagent types hash into agent1-agent6, falls back to accent)"
|
|
305
|
+
},
|
|
306
|
+
"agent3": {
|
|
307
|
+
"$ref": "#/$defs/colorValue",
|
|
308
|
+
"description": "Agent identity palette slot 3 (optional; subagent types hash into agent1-agent6, falls back to accent)"
|
|
309
|
+
},
|
|
310
|
+
"agent4": {
|
|
311
|
+
"$ref": "#/$defs/colorValue",
|
|
312
|
+
"description": "Agent identity palette slot 4 (optional; subagent types hash into agent1-agent6, falls back to accent)"
|
|
313
|
+
},
|
|
314
|
+
"agent5": {
|
|
315
|
+
"$ref": "#/$defs/colorValue",
|
|
316
|
+
"description": "Agent identity palette slot 5 (optional; subagent types hash into agent1-agent6, falls back to accent)"
|
|
317
|
+
},
|
|
318
|
+
"agent6": {
|
|
319
|
+
"$ref": "#/$defs/colorValue",
|
|
320
|
+
"description": "Agent identity palette slot 6 (optional; subagent types hash into agent1-agent6, falls back to accent)"
|
|
294
321
|
}
|
|
295
322
|
},
|
|
296
323
|
"additionalProperties": false
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@kolisachint/hoocode-tui";
|
|
2
2
|
import type { SourceInfo } from "../../../core/source-info.js";
|
|
3
|
-
export type ThemeColor = "accent" | "border" | "borderAccent" | "borderMuted" | "success" | "error" | "warning" | "muted" | "dim" | "text" | "thinkingText" | "userMessageText" | "customMessageText" | "customMessageLabel" | "toolTitle" | "toolOutput" | "mdHeading" | "mdLink" | "mdLinkUrl" | "mdCode" | "mdCodeBlock" | "mdCodeBlockBorder" | "mdQuote" | "mdQuoteBorder" | "mdHr" | "mdListBullet" | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext" | "syntaxComment" | "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator" | "syntaxPunctuation" | "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "bashMode";
|
|
3
|
+
export type ThemeColor = "accent" | "border" | "borderAccent" | "borderMuted" | "success" | "error" | "warning" | "muted" | "dim" | "text" | "thinkingText" | "userMessageText" | "customMessageText" | "customMessageLabel" | "toolTitle" | "toolOutput" | "mdHeading" | "mdLink" | "mdLinkUrl" | "mdCode" | "mdCodeBlock" | "mdCodeBlockBorder" | "mdQuote" | "mdQuoteBorder" | "mdHr" | "mdListBullet" | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext" | "syntaxComment" | "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator" | "syntaxPunctuation" | "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "bashMode" | "agent1" | "agent2" | "agent3" | "agent4" | "agent5" | "agent6";
|
|
4
|
+
/** The agent identity palette, in hash order. */
|
|
5
|
+
export declare const AGENT_COLOR_TOKENS: readonly ["agent1", "agent2", "agent3", "agent4", "agent5", "agent6"];
|
|
6
|
+
/**
|
|
7
|
+
* Stable color token for an agent type ("explore", "plan", a custom agent…).
|
|
8
|
+
* The name hashes into the agent palette, so the same type renders in the same
|
|
9
|
+
* color everywhere it appears — chat tool line, task panel rows/roster, and
|
|
10
|
+
* TaskOutput — for the whole session and across sessions. Distinct types spread
|
|
11
|
+
* over 6 hues; collisions are possible but the `[name]`/`#N` labels stay the
|
|
12
|
+
* primary identifier, color is the at-a-glance cue.
|
|
13
|
+
*/
|
|
14
|
+
export declare function agentColorFor(agentType: string): ThemeColor;
|
|
4
15
|
export type ThemeBg = "selectedBg" | "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
5
16
|
type ColorMode = "truecolor" | "256color";
|
|
6
17
|
export declare class Theme {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/theme/theme.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { EditorTheme, MarkdownTheme, SelectListTheme } from \"@kolisachint/hoocode-tui\";\nimport chalk from \"chalk\";\nimport { highlight, supportsLanguage } from \"cli-highlight\";\nimport { type Static, Type } from \"typebox\";\nimport { Compile } from \"typebox/compile\";\nimport { getCustomThemesDir, getThemesDir } from \"../../../config.js\";\nimport type { SourceInfo } from \"../../../core/source-info.js\";\nimport { closeWatcher, watchWithErrorHandler } from \"../../../utils/fs-watch.js\";\n\n// ============================================================================\n// Types & Schema\n// ============================================================================\n\nconst ColorValueSchema = Type.Union([\n\tType.String(), // hex \"#ff0000\", var ref \"primary\", or empty \"\"\n\tType.Integer({ minimum: 0, maximum: 255 }), // 256-color index\n]);\n\ntype ColorValue = Static<typeof ColorValueSchema>;\n\nconst ThemeJsonSchema = Type.Object({\n\t$schema: Type.Optional(Type.String()),\n\tname: Type.String(),\n\tvars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),\n\tcolors: Type.Object({\n\t\t// Core UI (10 colors)\n\t\taccent: ColorValueSchema,\n\t\tborder: ColorValueSchema,\n\t\tborderAccent: ColorValueSchema,\n\t\tborderMuted: ColorValueSchema,\n\t\tsuccess: ColorValueSchema,\n\t\terror: ColorValueSchema,\n\t\twarning: ColorValueSchema,\n\t\tmuted: ColorValueSchema,\n\t\tdim: ColorValueSchema,\n\t\ttext: ColorValueSchema,\n\t\tthinkingText: ColorValueSchema,\n\t\t// Backgrounds & Content Text (11 colors)\n\t\tselectedBg: ColorValueSchema,\n\t\tuserMessageBg: ColorValueSchema,\n\t\tuserMessageText: ColorValueSchema,\n\t\tcustomMessageBg: ColorValueSchema,\n\t\tcustomMessageText: ColorValueSchema,\n\t\tcustomMessageLabel: ColorValueSchema,\n\t\ttoolPendingBg: ColorValueSchema,\n\t\ttoolSuccessBg: ColorValueSchema,\n\t\ttoolErrorBg: ColorValueSchema,\n\t\ttoolTitle: ColorValueSchema,\n\t\ttoolOutput: ColorValueSchema,\n\t\t// Markdown (10 colors)\n\t\tmdHeading: ColorValueSchema,\n\t\tmdLink: ColorValueSchema,\n\t\tmdLinkUrl: ColorValueSchema,\n\t\tmdCode: ColorValueSchema,\n\t\tmdCodeBlock: ColorValueSchema,\n\t\tmdCodeBlockBorder: ColorValueSchema,\n\t\tmdQuote: ColorValueSchema,\n\t\tmdQuoteBorder: ColorValueSchema,\n\t\tmdHr: ColorValueSchema,\n\t\tmdListBullet: ColorValueSchema,\n\t\t// Tool Diffs (3 colors)\n\t\ttoolDiffAdded: ColorValueSchema,\n\t\ttoolDiffRemoved: ColorValueSchema,\n\t\ttoolDiffContext: ColorValueSchema,\n\t\t// Syntax Highlighting (9 colors)\n\t\tsyntaxComment: ColorValueSchema,\n\t\tsyntaxKeyword: ColorValueSchema,\n\t\tsyntaxFunction: ColorValueSchema,\n\t\tsyntaxVariable: ColorValueSchema,\n\t\tsyntaxString: ColorValueSchema,\n\t\tsyntaxNumber: ColorValueSchema,\n\t\tsyntaxType: ColorValueSchema,\n\t\tsyntaxOperator: ColorValueSchema,\n\t\tsyntaxPunctuation: ColorValueSchema,\n\t\t// Thinking Level Borders (6 colors)\n\t\tthinkingOff: ColorValueSchema,\n\t\tthinkingMinimal: ColorValueSchema,\n\t\tthinkingLow: ColorValueSchema,\n\t\tthinkingMedium: ColorValueSchema,\n\t\tthinkingHigh: ColorValueSchema,\n\t\tthinkingXhigh: ColorValueSchema,\n\t\t// Bash Mode (1 color)\n\t\tbashMode: ColorValueSchema,\n\t}),\n\texport: Type.Optional(\n\t\tType.Object({\n\t\t\tpageBg: Type.Optional(ColorValueSchema),\n\t\t\tcardBg: Type.Optional(ColorValueSchema),\n\t\t\tinfoBg: Type.Optional(ColorValueSchema),\n\t\t}),\n\t),\n});\n\ntype ThemeJson = Static<typeof ThemeJsonSchema>;\n\nconst validateThemeJson = Compile(ThemeJsonSchema);\n\nexport type ThemeColor =\n\t| \"accent\"\n\t| \"border\"\n\t| \"borderAccent\"\n\t| \"borderMuted\"\n\t| \"success\"\n\t| \"error\"\n\t| \"warning\"\n\t| \"muted\"\n\t| \"dim\"\n\t| \"text\"\n\t| \"thinkingText\"\n\t| \"userMessageText\"\n\t| \"customMessageText\"\n\t| \"customMessageLabel\"\n\t| \"toolTitle\"\n\t| \"toolOutput\"\n\t| \"mdHeading\"\n\t| \"mdLink\"\n\t| \"mdLinkUrl\"\n\t| \"mdCode\"\n\t| \"mdCodeBlock\"\n\t| \"mdCodeBlockBorder\"\n\t| \"mdQuote\"\n\t| \"mdQuoteBorder\"\n\t| \"mdHr\"\n\t| \"mdListBullet\"\n\t| \"toolDiffAdded\"\n\t| \"toolDiffRemoved\"\n\t| \"toolDiffContext\"\n\t| \"syntaxComment\"\n\t| \"syntaxKeyword\"\n\t| \"syntaxFunction\"\n\t| \"syntaxVariable\"\n\t| \"syntaxString\"\n\t| \"syntaxNumber\"\n\t| \"syntaxType\"\n\t| \"syntaxOperator\"\n\t| \"syntaxPunctuation\"\n\t| \"thinkingOff\"\n\t| \"thinkingMinimal\"\n\t| \"thinkingLow\"\n\t| \"thinkingMedium\"\n\t| \"thinkingHigh\"\n\t| \"thinkingXhigh\"\n\t| \"bashMode\";\n\nexport type ThemeBg =\n\t| \"selectedBg\"\n\t| \"userMessageBg\"\n\t| \"customMessageBg\"\n\t| \"toolPendingBg\"\n\t| \"toolSuccessBg\"\n\t| \"toolErrorBg\";\n\ntype ColorMode = \"truecolor\" | \"256color\";\n\n// ============================================================================\n// Color Utilities\n// ============================================================================\n\nfunction detectColorMode(): ColorMode {\n\tconst colorterm = process.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\treturn \"truecolor\";\n\t}\n\t// Windows Terminal supports truecolor\n\tif (process.env.WT_SESSION) {\n\t\treturn \"truecolor\";\n\t}\n\tconst term = process.env.TERM || \"\";\n\t// Fall back to 256color for truly limited terminals\n\tif (term === \"dumb\" || term === \"\" || term === \"linux\") {\n\t\treturn \"256color\";\n\t}\n\t// Terminal.app also doesn't support truecolor\n\tif (process.env.TERM_PROGRAM === \"Apple_Terminal\") {\n\t\treturn \"256color\";\n\t}\n\t// GNU screen doesn't support truecolor unless explicitly opted in via COLORTERM=truecolor.\n\t// TERM under screen is typically \"screen\", \"screen-256color\", or \"screen.xterm-256color\".\n\tif (term === \"screen\" || term.startsWith(\"screen-\") || term.startsWith(\"screen.\")) {\n\t\treturn \"256color\";\n\t}\n\t// Assume truecolor for everything else - virtually all modern terminals support it\n\treturn \"truecolor\";\n}\n\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } {\n\tconst cleaned = hex.replace(\"#\", \"\");\n\tif (cleaned.length !== 6) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\tconst r = parseInt(cleaned.substring(0, 2), 16);\n\tconst g = parseInt(cleaned.substring(2, 4), 16);\n\tconst b = parseInt(cleaned.substring(4, 6), 16);\n\tif (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\treturn { r, g, b };\n}\n\n// The 6x6x6 color cube channel values (indices 0-5)\nconst CUBE_VALUES = [0, 95, 135, 175, 215, 255];\n\n// Grayscale ramp values (indices 232-255, 24 grays from 8 to 238)\nconst GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10);\n\nfunction findClosestCubeIndex(value: number): number {\n\tlet minDist = Infinity;\n\tlet minIdx = 0;\n\tfor (let i = 0; i < CUBE_VALUES.length; i++) {\n\t\tconst dist = Math.abs(value - CUBE_VALUES[i]);\n\t\tif (dist < minDist) {\n\t\t\tminDist = dist;\n\t\t\tminIdx = i;\n\t\t}\n\t}\n\treturn minIdx;\n}\n\nfunction findClosestGrayIndex(gray: number): number {\n\tlet minDist = Infinity;\n\tlet minIdx = 0;\n\tfor (let i = 0; i < GRAY_VALUES.length; i++) {\n\t\tconst dist = Math.abs(gray - GRAY_VALUES[i]);\n\t\tif (dist < minDist) {\n\t\t\tminDist = dist;\n\t\t\tminIdx = i;\n\t\t}\n\t}\n\treturn minIdx;\n}\n\nfunction colorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {\n\t// Weighted Euclidean distance (human eye is more sensitive to green)\n\tconst dr = r1 - r2;\n\tconst dg = g1 - g2;\n\tconst db = b1 - b2;\n\treturn dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;\n}\n\nfunction rgbTo256(r: number, g: number, b: number): number {\n\t// Find closest color in the 6x6x6 cube\n\tconst rIdx = findClosestCubeIndex(r);\n\tconst gIdx = findClosestCubeIndex(g);\n\tconst bIdx = findClosestCubeIndex(b);\n\tconst cubeR = CUBE_VALUES[rIdx];\n\tconst cubeG = CUBE_VALUES[gIdx];\n\tconst cubeB = CUBE_VALUES[bIdx];\n\tconst cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx;\n\tconst cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB);\n\n\t// Find closest grayscale\n\tconst gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);\n\tconst grayIdx = findClosestGrayIndex(gray);\n\tconst grayValue = GRAY_VALUES[grayIdx];\n\tconst grayIndex = 232 + grayIdx;\n\tconst grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue);\n\n\t// Check if color has noticeable saturation (hue matters)\n\t// If max-min spread is significant, prefer cube to preserve tint\n\tconst maxC = Math.max(r, g, b);\n\tconst minC = Math.min(r, g, b);\n\tconst spread = maxC - minC;\n\n\t// Only consider grayscale if color is nearly neutral (spread < 10)\n\t// AND grayscale is actually closer\n\tif (spread < 10 && grayDist < cubeDist) {\n\t\treturn grayIndex;\n\t}\n\n\treturn cubeIndex;\n}\n\nfunction hexTo256(hex: string): number {\n\tconst { r, g, b } = hexToRgb(hex);\n\treturn rgbTo256(r, g, b);\n}\n\nfunction fgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[39m\";\n\tif (typeof color === \"number\") return `\\x1b[38;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[38;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[38;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction bgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[49m\";\n\tif (typeof color === \"number\") return `\\x1b[48;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[48;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[48;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction resolveVarRefs(\n\tvalue: ColorValue,\n\tvars: Record<string, ColorValue>,\n\tvisited = new Set<string>(),\n): string | number {\n\tif (typeof value === \"number\" || value === \"\" || value.startsWith(\"#\")) {\n\t\treturn value;\n\t}\n\tif (visited.has(value)) {\n\t\tthrow new Error(`Circular variable reference detected: ${value}`);\n\t}\n\tif (!(value in vars)) {\n\t\tthrow new Error(`Variable reference not found: ${value}`);\n\t}\n\tvisited.add(value);\n\treturn resolveVarRefs(vars[value], vars, visited);\n}\n\nfunction resolveThemeColors<T extends Record<string, ColorValue>>(\n\tcolors: T,\n\tvars: Record<string, ColorValue> = {},\n): Record<keyof T, string | number> {\n\tconst resolved: Record<string, string | number> = {};\n\tfor (const [key, value] of Object.entries(colors)) {\n\t\tresolved[key] = resolveVarRefs(value, vars);\n\t}\n\treturn resolved as Record<keyof T, string | number>;\n}\n\n// ============================================================================\n// Theme Class\n// ============================================================================\n\nexport class Theme {\n\treadonly name?: string;\n\treadonly sourcePath?: string;\n\tsourceInfo?: SourceInfo;\n\tprivate fgColors: Map<ThemeColor, string>;\n\tprivate bgColors: Map<ThemeBg, string>;\n\tprivate mode: ColorMode;\n\n\tconstructor(\n\t\tfgColors: Record<ThemeColor, string | number>,\n\t\tbgColors: Record<ThemeBg, string | number>,\n\t\tmode: ColorMode,\n\t\toptions: { name?: string; sourcePath?: string; sourceInfo?: SourceInfo } = {},\n\t) {\n\t\tthis.name = options.name;\n\t\tthis.sourcePath = options.sourcePath;\n\t\tthis.sourceInfo = options.sourceInfo;\n\t\tthis.mode = mode;\n\t\tthis.fgColors = new Map();\n\t\tfor (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {\n\t\t\tthis.fgColors.set(key, fgAnsi(value, mode));\n\t\t}\n\t\tthis.bgColors = new Map();\n\t\tfor (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {\n\t\t\tthis.bgColors.set(key, bgAnsi(value, mode));\n\t\t}\n\t}\n\n\tfg(color: ThemeColor, text: string): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[39m`; // Reset only foreground color\n\t}\n\n\tbg(color: ThemeBg, text: string): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[49m`; // Reset only background color\n\t}\n\n\tbold(text: string): string {\n\t\treturn chalk.bold(text);\n\t}\n\n\tblink(text: string): string {\n\t\t// Terminal-native blink (SGR 5). Zero overhead; terminals that support\n\t\t// blink will animate, others render it as a steady glyph.\n\t\treturn `\\x1b[5m${text}\\x1b[25m`;\n\t}\n\n\titalic(text: string): string {\n\t\treturn chalk.italic(text);\n\t}\n\n\tunderline(text: string): string {\n\t\treturn chalk.underline(text);\n\t}\n\n\tinverse(text: string): string {\n\t\treturn chalk.inverse(text);\n\t}\n\n\tstrikethrough(text: string): string {\n\t\treturn chalk.strikethrough(text);\n\t}\n\n\tgetFgAnsi(color: ThemeColor): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetBgAnsi(color: ThemeBg): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetColorMode(): ColorMode {\n\t\treturn this.mode;\n\t}\n\n\tgetThinkingBorderColor(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"): (str: string) => string {\n\t\t// Map thinking levels to dedicated theme colors\n\t\tswitch (level) {\n\t\t\tcase \"off\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingOff\", str);\n\t\t\tcase \"minimal\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingMinimal\", str);\n\t\t\tcase \"low\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingLow\", str);\n\t\t\tcase \"medium\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingMedium\", str);\n\t\t\tcase \"high\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingHigh\", str);\n\t\t\tcase \"xhigh\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingXhigh\", str);\n\t\t\tdefault:\n\t\t\t\treturn (str: string) => this.fg(\"thinkingOff\", str);\n\t\t}\n\t}\n\n\tgetBashModeBorderColor(): (str: string) => string {\n\t\treturn (str: string) => this.fg(\"bashMode\", str);\n\t}\n}\n\n// ============================================================================\n// Theme Loading\n// ============================================================================\n\nlet BUILTIN_THEMES: Record<string, ThemeJson> | undefined;\n\nfunction getBuiltinThemes(): Record<string, ThemeJson> {\n\tif (!BUILTIN_THEMES) {\n\t\tconst themesDir = getThemesDir();\n\t\tconst darkPath = path.join(themesDir, \"dark.json\");\n\t\tconst lightPath = path.join(themesDir, \"light.json\");\n\t\tBUILTIN_THEMES = {\n\t\t\tdark: JSON.parse(fs.readFileSync(darkPath, \"utf-8\")) as ThemeJson,\n\t\t\tlight: JSON.parse(fs.readFileSync(lightPath, \"utf-8\")) as ThemeJson,\n\t\t};\n\t}\n\treturn BUILTIN_THEMES;\n}\n\nexport function getAvailableThemes(): string[] {\n\tconst themes = new Set<string>(Object.keys(getBuiltinThemes()));\n\tconst customThemesDir = getCustomThemesDir();\n\tif (fs.existsSync(customThemesDir)) {\n\t\tconst files = fs.readdirSync(customThemesDir);\n\t\tfor (const file of files) {\n\t\t\tif (file.endsWith(\".json\")) {\n\t\t\t\tthemes.add(file.slice(0, -5));\n\t\t\t}\n\t\t}\n\t}\n\tfor (const name of registeredThemes.keys()) {\n\t\tthemes.add(name);\n\t}\n\treturn Array.from(themes).sort();\n}\n\nexport interface ThemeInfo {\n\tname: string;\n\tpath: string | undefined;\n}\n\nexport function getAvailableThemesWithPaths(): ThemeInfo[] {\n\tconst themesDir = getThemesDir();\n\tconst customThemesDir = getCustomThemesDir();\n\tconst result: ThemeInfo[] = [];\n\n\t// Built-in themes\n\tfor (const name of Object.keys(getBuiltinThemes())) {\n\t\tresult.push({ name, path: path.join(themesDir, `${name}.json`) });\n\t}\n\n\t// Custom themes\n\tif (fs.existsSync(customThemesDir)) {\n\t\tfor (const file of fs.readdirSync(customThemesDir)) {\n\t\t\tif (file.endsWith(\".json\")) {\n\t\t\t\tconst name = file.slice(0, -5);\n\t\t\t\tif (!result.some((t) => t.name === name)) {\n\t\t\t\t\tresult.push({ name, path: path.join(customThemesDir, file) });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const [name, theme] of registeredThemes.entries()) {\n\t\tif (!result.some((t) => t.name === name)) {\n\t\t\tresult.push({ name, path: theme.sourcePath });\n\t\t}\n\t}\n\n\treturn result.sort((a, b) => a.name.localeCompare(b.name));\n}\n\nfunction parseThemeJson(label: string, json: unknown): ThemeJson {\n\tif (!validateThemeJson.Check(json)) {\n\t\tconst errors = Array.from(validateThemeJson.Errors(json));\n\t\tconst missingColors = new Set<string>();\n\t\tconst otherErrors: string[] = [];\n\n\t\tfor (const error of errors) {\n\t\t\tif (error.keyword === \"required\" && error.instancePath === \"/colors\") {\n\t\t\t\tconst requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;\n\t\t\t\tfor (const requiredProperty of requiredProperties ?? []) {\n\t\t\t\t\tmissingColors.add(requiredProperty);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst path = error.instancePath || \"/\";\n\t\t\totherErrors.push(` - ${path}: ${error.message}`);\n\t\t}\n\n\t\tlet errorMessage = `Invalid theme \"${label}\":\\n`;\n\t\tif (missingColors.size > 0) {\n\t\t\terrorMessage += \"\\nMissing required color tokens:\\n\";\n\t\t\terrorMessage += Array.from(missingColors)\n\t\t\t\t.sort()\n\t\t\t\t.map((color) => ` - ${color}`)\n\t\t\t\t.join(\"\\n\");\n\t\t\terrorMessage += '\\n\\nPlease add these colors to your theme\\'s \"colors\" object.';\n\t\t\terrorMessage += \"\\nSee the built-in themes (dark.json, light.json) for reference values.\";\n\t\t}\n\t\tif (otherErrors.length > 0) {\n\t\t\terrorMessage += `\\n\\nOther errors:\\n${otherErrors.join(\"\\n\")}`;\n\t\t}\n\n\t\tthrow new Error(errorMessage);\n\t}\n\n\treturn json as ThemeJson;\n}\n\nfunction parseThemeJsonContent(label: string, content: string): ThemeJson {\n\tlet json: unknown;\n\ttry {\n\t\tjson = JSON.parse(content);\n\t} catch (error) {\n\t\tthrow new Error(`Failed to parse theme ${label}: ${error}`);\n\t}\n\treturn parseThemeJson(label, json);\n}\n\nfunction loadThemeJson(name: string): ThemeJson {\n\tconst builtinThemes = getBuiltinThemes();\n\tif (name in builtinThemes) {\n\t\treturn builtinThemes[name];\n\t}\n\tconst registeredTheme = registeredThemes.get(name);\n\tif (registeredTheme?.sourcePath) {\n\t\tconst content = fs.readFileSync(registeredTheme.sourcePath, \"utf-8\");\n\t\treturn parseThemeJsonContent(registeredTheme.sourcePath, content);\n\t}\n\tif (registeredTheme) {\n\t\tthrow new Error(`Theme \"${name}\" does not have a source path for export`);\n\t}\n\tconst customThemesDir = getCustomThemesDir();\n\tconst themePath = path.join(customThemesDir, `${name}.json`);\n\tif (!fs.existsSync(themePath)) {\n\t\tthrow new Error(`Theme not found: ${name}`);\n\t}\n\tconst content = fs.readFileSync(themePath, \"utf-8\");\n\treturn parseThemeJsonContent(name, content);\n}\n\nfunction createTheme(themeJson: ThemeJson, mode?: ColorMode, sourcePath?: string): Theme {\n\tconst colorMode = mode ?? detectColorMode();\n\tconst resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);\n\tconst fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>;\n\tconst bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;\n\tconst bgColorKeys: Set<string> = new Set([\n\t\t\"selectedBg\",\n\t\t\"userMessageBg\",\n\t\t\"customMessageBg\",\n\t\t\"toolPendingBg\",\n\t\t\"toolSuccessBg\",\n\t\t\"toolErrorBg\",\n\t]);\n\tfor (const [key, value] of Object.entries(resolvedColors)) {\n\t\tif (bgColorKeys.has(key)) {\n\t\t\tbgColors[key as ThemeBg] = value;\n\t\t} else {\n\t\t\tfgColors[key as ThemeColor] = value;\n\t\t}\n\t}\n\treturn new Theme(fgColors, bgColors, colorMode, {\n\t\tname: themeJson.name,\n\t\tsourcePath,\n\t});\n}\n\nexport function loadThemeFromPath(themePath: string, mode?: ColorMode): Theme {\n\tconst content = fs.readFileSync(themePath, \"utf-8\");\n\tconst themeJson = parseThemeJsonContent(themePath, content);\n\treturn createTheme(themeJson, mode, themePath);\n}\n\nfunction loadTheme(name: string, mode?: ColorMode): Theme {\n\tconst registeredTheme = registeredThemes.get(name);\n\tif (registeredTheme) {\n\t\treturn registeredTheme;\n\t}\n\tconst themeJson = loadThemeJson(name);\n\treturn createTheme(themeJson, mode);\n}\n\nexport function getThemeByName(name: string): Theme | undefined {\n\ttry {\n\t\treturn loadTheme(name);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = process.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\tconst result = bg < 8 ? \"dark\" : \"light\";\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\treturn \"dark\";\n}\n\nfunction getDefaultTheme(): string {\n\treturn detectTerminalBackground();\n}\n\n// ============================================================================\n// Global Theme Instance\n// ============================================================================\n\n// Use globalThis to share theme across module loaders (tsx + jiti in dev mode)\nconst THEME_KEY = Symbol.for(\"@kolisachint/hoocode-agent:theme\");\n\n// Export theme as a getter that reads from globalThis\n// This ensures all module instances (tsx, jiti) see the same theme\nexport const theme: Theme = new Proxy({} as Theme, {\n\tget(_target, prop) {\n\t\tconst t = (globalThis as Record<symbol, Theme>)[THEME_KEY];\n\t\tif (!t) throw new Error(\"Theme not initialized. Call initTheme() first.\");\n\t\treturn (t as unknown as Record<string | symbol, unknown>)[prop];\n\t},\n});\n\nfunction setGlobalTheme(t: Theme): void {\n\t(globalThis as Record<symbol, Theme>)[THEME_KEY] = t;\n}\n\nlet currentThemeName: string | undefined;\nlet themeWatcher: fs.FSWatcher | undefined;\nlet themeReloadTimer: NodeJS.Timeout | undefined;\nlet onThemeChangeCallback: (() => void) | undefined;\nconst registeredThemes = new Map<string, Theme>();\n\nexport function setRegisteredThemes(themes: Theme[]): void {\n\tregisteredThemes.clear();\n\tfor (const theme of themes) {\n\t\tif (theme.name) {\n\t\t\tregisteredThemes.set(theme.name, theme);\n\t\t}\n\t}\n}\n\nexport function initTheme(themeName?: string, enableWatcher: boolean = false): void {\n\tconst name = themeName ?? getDefaultTheme();\n\tcurrentThemeName = name;\n\ttry {\n\t\tsetGlobalTheme(loadTheme(name));\n\t\tif (enableWatcher) {\n\t\t\tstartThemeWatcher();\n\t\t}\n\t} catch (_error) {\n\t\t// Theme is invalid - fall back to dark theme silently\n\t\tcurrentThemeName = \"dark\";\n\t\tsetGlobalTheme(loadTheme(\"dark\"));\n\t\t// Don't start watcher for fallback theme\n\t}\n}\n\nexport function setTheme(name: string, enableWatcher: boolean = false): { success: boolean; error?: string } {\n\tcurrentThemeName = name;\n\ttry {\n\t\tsetGlobalTheme(loadTheme(name));\n\t\tif (enableWatcher) {\n\t\t\tstartThemeWatcher();\n\t\t}\n\t\tif (onThemeChangeCallback) {\n\t\t\tonThemeChangeCallback();\n\t\t}\n\t\treturn { success: true };\n\t} catch (error) {\n\t\t// Theme is invalid - fall back to dark theme\n\t\tcurrentThemeName = \"dark\";\n\t\tsetGlobalTheme(loadTheme(\"dark\"));\n\t\t// Don't start watcher for fallback theme\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t};\n\t}\n}\n\nexport function setThemeInstance(themeInstance: Theme): void {\n\tsetGlobalTheme(themeInstance);\n\tcurrentThemeName = \"<in-memory>\";\n\tstopThemeWatcher(); // Can't watch a direct instance\n\tif (onThemeChangeCallback) {\n\t\tonThemeChangeCallback();\n\t}\n}\n\nexport function onThemeChange(callback: () => void): void {\n\tonThemeChangeCallback = callback;\n}\n\nfunction startThemeWatcher(): void {\n\tstopThemeWatcher();\n\n\t// Only watch if it's a custom theme (not built-in)\n\tif (!currentThemeName || currentThemeName === \"dark\" || currentThemeName === \"light\") {\n\t\treturn;\n\t}\n\n\tconst customThemesDir = getCustomThemesDir();\n\tconst watchedThemeName = currentThemeName;\n\tconst watchedFileName = `${watchedThemeName}.json`;\n\tconst themeFile = path.join(customThemesDir, watchedFileName);\n\n\t// Only watch if the file exists\n\tif (!fs.existsSync(themeFile)) {\n\t\treturn;\n\t}\n\n\tconst scheduleReload = () => {\n\t\tif (themeReloadTimer) {\n\t\t\tclearTimeout(themeReloadTimer);\n\t\t}\n\t\tthemeReloadTimer = setTimeout(() => {\n\t\t\tthemeReloadTimer = undefined;\n\n\t\t\t// Ignore stale timers after switching themes or stopping the watcher\n\t\t\tif (currentThemeName !== watchedThemeName) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Keep the last successfully loaded theme active if the file is temporarily missing\n\t\t\tif (!fs.existsSync(themeFile)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Reload the theme from disk and refresh the registry cache\n\t\t\t\tconst reloadedTheme = loadThemeFromPath(themeFile);\n\t\t\t\tregisteredThemes.set(watchedThemeName, reloadedTheme);\n\t\t\t\tsetGlobalTheme(reloadedTheme);\n\t\t\t\t// Notify callback (to invalidate UI)\n\t\t\t\tif (onThemeChangeCallback) {\n\t\t\t\t\tonThemeChangeCallback();\n\t\t\t\t}\n\t\t\t} catch (_error) {\n\t\t\t\t// Ignore errors (file might be in invalid state while being edited)\n\t\t\t}\n\t\t}, 100);\n\t};\n\n\tthemeWatcher =\n\t\twatchWithErrorHandler(\n\t\t\tcustomThemesDir,\n\t\t\t(_eventType, filename) => {\n\t\t\t\tif (currentThemeName !== watchedThemeName) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!filename) {\n\t\t\t\t\tscheduleReload();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (filename !== watchedFileName) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tscheduleReload();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tcloseWatcher(themeWatcher);\n\t\t\t\tthemeWatcher = undefined;\n\t\t\t},\n\t\t) ?? undefined;\n}\n\nexport function stopThemeWatcher(): void {\n\tif (themeReloadTimer) {\n\t\tclearTimeout(themeReloadTimer);\n\t\tthemeReloadTimer = undefined;\n\t}\n\tcloseWatcher(themeWatcher);\n\tthemeWatcher = undefined;\n}\n\n// ============================================================================\n// HTML Export Helpers\n// ============================================================================\n\n/**\n * Convert a 256-color index to hex string.\n * Indices 0-15: basic colors (approximate)\n * Indices 16-231: 6x6x6 color cube\n * Indices 232-255: grayscale ramp\n */\nfunction ansi256ToHex(index: number): string {\n\t// Basic colors (0-15) - approximate common terminal values\n\tconst basicColors = [\n\t\t\"#000000\",\n\t\t\"#800000\",\n\t\t\"#008000\",\n\t\t\"#808000\",\n\t\t\"#000080\",\n\t\t\"#800080\",\n\t\t\"#008080\",\n\t\t\"#c0c0c0\",\n\t\t\"#808080\",\n\t\t\"#ff0000\",\n\t\t\"#00ff00\",\n\t\t\"#ffff00\",\n\t\t\"#0000ff\",\n\t\t\"#ff00ff\",\n\t\t\"#00ffff\",\n\t\t\"#ffffff\",\n\t];\n\tif (index < 16) {\n\t\treturn basicColors[index];\n\t}\n\n\t// Color cube (16-231): 6x6x6 = 216 colors\n\tif (index < 232) {\n\t\tconst cubeIndex = index - 16;\n\t\tconst r = Math.floor(cubeIndex / 36);\n\t\tconst g = Math.floor((cubeIndex % 36) / 6);\n\t\tconst b = cubeIndex % 6;\n\t\tconst toHex = (n: number) => (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, \"0\");\n\t\treturn `#${toHex(r)}${toHex(g)}${toHex(b)}`;\n\t}\n\n\t// Grayscale (232-255): 24 shades\n\tconst gray = 8 + (index - 232) * 10;\n\tconst grayHex = gray.toString(16).padStart(2, \"0\");\n\treturn `#${grayHex}${grayHex}${grayHex}`;\n}\n\n/**\n * Get resolved theme colors as CSS-compatible hex strings.\n * Used by HTML export to generate CSS custom properties.\n */\nexport function getResolvedThemeColors(themeName?: string): Record<string, string> {\n\tconst name = themeName ?? currentThemeName ?? getDefaultTheme();\n\tconst isLight = name === \"light\";\n\tconst themeJson = loadThemeJson(name);\n\tconst resolved = resolveThemeColors(themeJson.colors, themeJson.vars);\n\n\t// Default text color for empty values (terminal uses default fg color)\n\tconst defaultText = isLight ? \"#000000\" : \"#e5e5e7\";\n\n\tconst cssColors: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(resolved)) {\n\t\tif (typeof value === \"number\") {\n\t\t\tcssColors[key] = ansi256ToHex(value);\n\t\t} else if (value === \"\") {\n\t\t\t// Empty means default terminal color - use sensible fallback for HTML\n\t\t\tcssColors[key] = defaultText;\n\t\t} else {\n\t\t\tcssColors[key] = value;\n\t\t}\n\t}\n\treturn cssColors;\n}\n\n/**\n * Check if a theme is a \"light\" theme (for CSS that needs light/dark variants).\n */\nexport function isLightTheme(themeName?: string): boolean {\n\t// Currently just check the name - could be extended to analyze colors\n\treturn themeName === \"light\";\n}\n\n/**\n * Get explicit export colors from theme JSON, if specified.\n * Returns undefined for each color that isn't explicitly set.\n */\nexport function getThemeExportColors(themeName?: string): {\n\tpageBg?: string;\n\tcardBg?: string;\n\tinfoBg?: string;\n} {\n\tconst name = themeName ?? currentThemeName ?? getDefaultTheme();\n\ttry {\n\t\tconst themeJson = loadThemeJson(name);\n\t\tconst exportSection = themeJson.export;\n\t\tif (!exportSection) return {};\n\n\t\tconst vars = themeJson.vars ?? {};\n\t\tconst resolve = (value: ColorValue | undefined): string | undefined => {\n\t\t\tif (value === undefined) return undefined;\n\t\t\tconst resolved = resolveVarRefs(value, vars);\n\t\t\tif (typeof resolved === \"number\") return ansi256ToHex(resolved);\n\t\t\tif (resolved === \"\") return undefined;\n\t\t\treturn resolved;\n\t\t};\n\n\t\treturn {\n\t\t\tpageBg: resolve(exportSection.pageBg),\n\t\t\tcardBg: resolve(exportSection.cardBg),\n\t\t\tinfoBg: resolve(exportSection.infoBg),\n\t\t};\n\t} catch {\n\t\treturn {};\n\t}\n}\n\n// ============================================================================\n// TUI Helpers\n// ============================================================================\n\ntype CliHighlightTheme = Record<string, (s: string) => string>;\n\nlet cachedHighlightThemeFor: Theme | undefined;\nlet cachedCliHighlightTheme: CliHighlightTheme | undefined;\n\nfunction buildCliHighlightTheme(t: Theme): CliHighlightTheme {\n\treturn {\n\t\tkeyword: (s: string) => t.fg(\"syntaxKeyword\", s),\n\t\tbuilt_in: (s: string) => t.fg(\"syntaxType\", s),\n\t\tliteral: (s: string) => t.fg(\"syntaxNumber\", s),\n\t\tnumber: (s: string) => t.fg(\"syntaxNumber\", s),\n\t\tstring: (s: string) => t.fg(\"syntaxString\", s),\n\t\tcomment: (s: string) => t.fg(\"syntaxComment\", s),\n\t\tfunction: (s: string) => t.fg(\"syntaxFunction\", s),\n\t\ttitle: (s: string) => t.fg(\"syntaxFunction\", s),\n\t\tclass: (s: string) => t.fg(\"syntaxType\", s),\n\t\ttype: (s: string) => t.fg(\"syntaxType\", s),\n\t\tattr: (s: string) => t.fg(\"syntaxVariable\", s),\n\t\tvariable: (s: string) => t.fg(\"syntaxVariable\", s),\n\t\tparams: (s: string) => t.fg(\"syntaxVariable\", s),\n\t\toperator: (s: string) => t.fg(\"syntaxOperator\", s),\n\t\tpunctuation: (s: string) => t.fg(\"syntaxPunctuation\", s),\n\t};\n}\n\nfunction getCliHighlightTheme(t: Theme): CliHighlightTheme {\n\tif (cachedHighlightThemeFor !== t || !cachedCliHighlightTheme) {\n\t\tcachedHighlightThemeFor = t;\n\t\tcachedCliHighlightTheme = buildCliHighlightTheme(t);\n\t}\n\treturn cachedCliHighlightTheme;\n}\n\n/**\n * Highlight code with syntax coloring based on file extension or language.\n * Returns array of highlighted lines.\n */\nexport function highlightCode(code: string, lang?: string): string[] {\n\t// Validate language before highlighting to avoid stderr spam from cli-highlight\n\tconst validLang = lang && supportsLanguage(lang) ? lang : undefined;\n\t// Skip highlighting when no valid language is specified. cli-highlight's\n\t// auto-detection is unreliable and can misidentify prose as AppleScript,\n\t// LiveCodeServer, etc., coloring random English words as keywords.\n\tif (!validLang) {\n\t\treturn code.split(\"\\n\").map((line) => theme.fg(\"mdCodeBlock\", line));\n\t}\n\tconst opts = {\n\t\tlanguage: validLang,\n\t\tignoreIllegals: true,\n\t\ttheme: getCliHighlightTheme(theme),\n\t};\n\ttry {\n\t\treturn highlight(code, opts).split(\"\\n\");\n\t} catch {\n\t\treturn code.split(\"\\n\");\n\t}\n}\n\n/**\n * Get language identifier from file path extension.\n */\nexport function getLanguageFromPath(filePath: string): string | undefined {\n\tconst ext = filePath.split(\".\").pop()?.toLowerCase();\n\tif (!ext) return undefined;\n\n\tconst extToLang: Record<string, string> = {\n\t\tts: \"typescript\",\n\t\ttsx: \"typescript\",\n\t\tjs: \"javascript\",\n\t\tjsx: \"javascript\",\n\t\tmjs: \"javascript\",\n\t\tcjs: \"javascript\",\n\t\tpy: \"python\",\n\t\trb: \"ruby\",\n\t\trs: \"rust\",\n\t\tgo: \"go\",\n\t\tjava: \"java\",\n\t\tkt: \"kotlin\",\n\t\tswift: \"swift\",\n\t\tc: \"c\",\n\t\th: \"c\",\n\t\tcpp: \"cpp\",\n\t\tcc: \"cpp\",\n\t\tcxx: \"cpp\",\n\t\thpp: \"cpp\",\n\t\tcs: \"csharp\",\n\t\tphp: \"php\",\n\t\tsh: \"bash\",\n\t\tbash: \"bash\",\n\t\tzsh: \"bash\",\n\t\tfish: \"fish\",\n\t\tps1: \"powershell\",\n\t\tsql: \"sql\",\n\t\thtml: \"html\",\n\t\thtm: \"html\",\n\t\tcss: \"css\",\n\t\tscss: \"scss\",\n\t\tsass: \"sass\",\n\t\tless: \"less\",\n\t\tjson: \"json\",\n\t\tyaml: \"yaml\",\n\t\tyml: \"yaml\",\n\t\ttoml: \"toml\",\n\t\txml: \"xml\",\n\t\tmd: \"markdown\",\n\t\tmarkdown: \"markdown\",\n\t\tdockerfile: \"dockerfile\",\n\t\tmakefile: \"makefile\",\n\t\tcmake: \"cmake\",\n\t\tlua: \"lua\",\n\t\tperl: \"perl\",\n\t\tr: \"r\",\n\t\tscala: \"scala\",\n\t\tclj: \"clojure\",\n\t\tex: \"elixir\",\n\t\texs: \"elixir\",\n\t\terl: \"erlang\",\n\t\ths: \"haskell\",\n\t\tml: \"ocaml\",\n\t\tvim: \"vim\",\n\t\tgraphql: \"graphql\",\n\t\tproto: \"protobuf\",\n\t\ttf: \"hcl\",\n\t\thcl: \"hcl\",\n\t};\n\n\treturn extToLang[ext];\n}\n\nexport function getMarkdownTheme(): MarkdownTheme {\n\treturn {\n\t\theading: (text: string) => theme.fg(\"mdHeading\", text),\n\t\tlink: (text: string) => theme.fg(\"mdLink\", text),\n\t\tlinkUrl: (text: string) => theme.fg(\"mdLinkUrl\", text),\n\t\tcode: (text: string) => theme.fg(\"mdCode\", text),\n\t\tcodeBlock: (text: string) => theme.fg(\"mdCodeBlock\", text),\n\t\tcodeBlockBorder: (text: string) => theme.fg(\"mdCodeBlockBorder\", text),\n\t\tquote: (text: string) => theme.fg(\"mdQuote\", text),\n\t\tquoteBorder: (text: string) => theme.fg(\"mdQuoteBorder\", text),\n\t\thr: (text: string) => theme.fg(\"mdHr\", text),\n\t\tlistBullet: (text: string) => theme.fg(\"mdListBullet\", text),\n\t\tbold: (text: string) => theme.bold(text),\n\t\titalic: (text: string) => theme.italic(text),\n\t\tunderline: (text: string) => theme.underline(text),\n\t\tstrikethrough: (text: string) => chalk.strikethrough(text),\n\t\thighlightCode: (code: string, lang?: string): string[] => {\n\t\t\t// Validate language before highlighting to avoid stderr spam from cli-highlight\n\t\t\tconst validLang = lang && supportsLanguage(lang) ? lang : undefined;\n\t\t\t// Skip highlighting when no valid language is specified. cli-highlight's\n\t\t\t// auto-detection is unreliable and can misidentify prose as AppleScript,\n\t\t\t// LiveCodeServer, etc., coloring random English words as keywords.\n\t\t\tif (!validLang) {\n\t\t\t\treturn code.split(\"\\n\").map((line) => theme.fg(\"mdCodeBlock\", line));\n\t\t\t}\n\t\t\tconst opts = {\n\t\t\t\tlanguage: validLang,\n\t\t\t\tignoreIllegals: true,\n\t\t\t\ttheme: getCliHighlightTheme(theme),\n\t\t\t};\n\t\t\ttry {\n\t\t\t\treturn highlight(code, opts).split(\"\\n\");\n\t\t\t} catch {\n\t\t\t\treturn code.split(\"\\n\").map((line) => theme.fg(\"mdCodeBlock\", line));\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport function getSelectListTheme(): SelectListTheme {\n\treturn {\n\t\tselectedPrefix: (text: string) => theme.fg(\"accent\", text),\n\t\tselectedText: (text: string) => theme.fg(\"accent\", text),\n\t\tdescription: (text: string) => theme.fg(\"muted\", text),\n\t\tscrollInfo: (text: string) => theme.fg(\"muted\", text),\n\t\tnoMatch: (text: string) => theme.fg(\"muted\", text),\n\t};\n}\n\nexport function getEditorTheme(): EditorTheme {\n\treturn {\n\t\tborderColor: (text: string) => theme.fg(\"borderMuted\", text),\n\t\tselectList: getSelectListTheme(),\n\t};\n}\n\nexport function getSettingsListTheme(): import(\"@kolisachint/hoocode-tui\").SettingsListTheme {\n\treturn {\n\t\tlabel: (text: string, selected: boolean) => (selected ? theme.fg(\"accent\", text) : text),\n\t\tvalue: (text: string, selected: boolean) => (selected ? theme.fg(\"accent\", text) : theme.fg(\"muted\", text)),\n\t\tdescription: (text: string) => theme.fg(\"dim\", text),\n\t\tcursor: theme.fg(\"accent\", \"→ \"),\n\t\thint: (text: string) => theme.fg(\"dim\", text),\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/theme/theme.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { EditorTheme, MarkdownTheme, SelectListTheme } from \"@kolisachint/hoocode-tui\";\nimport chalk from \"chalk\";\nimport { highlight, supportsLanguage } from \"cli-highlight\";\nimport { type Static, Type } from \"typebox\";\nimport { Compile } from \"typebox/compile\";\nimport { getCustomThemesDir, getThemesDir } from \"../../../config.js\";\nimport type { SourceInfo } from \"../../../core/source-info.js\";\nimport { closeWatcher, watchWithErrorHandler } from \"../../../utils/fs-watch.js\";\n\n// ============================================================================\n// Types & Schema\n// ============================================================================\n\nconst ColorValueSchema = Type.Union([\n\tType.String(), // hex \"#ff0000\", var ref \"primary\", or empty \"\"\n\tType.Integer({ minimum: 0, maximum: 255 }), // 256-color index\n]);\n\ntype ColorValue = Static<typeof ColorValueSchema>;\n\nconst ThemeJsonSchema = Type.Object({\n\t$schema: Type.Optional(Type.String()),\n\tname: Type.String(),\n\tvars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),\n\tcolors: Type.Object({\n\t\t// Core UI (10 colors)\n\t\taccent: ColorValueSchema,\n\t\tborder: ColorValueSchema,\n\t\tborderAccent: ColorValueSchema,\n\t\tborderMuted: ColorValueSchema,\n\t\tsuccess: ColorValueSchema,\n\t\terror: ColorValueSchema,\n\t\twarning: ColorValueSchema,\n\t\tmuted: ColorValueSchema,\n\t\tdim: ColorValueSchema,\n\t\ttext: ColorValueSchema,\n\t\tthinkingText: ColorValueSchema,\n\t\t// Backgrounds & Content Text (11 colors)\n\t\tselectedBg: ColorValueSchema,\n\t\tuserMessageBg: ColorValueSchema,\n\t\tuserMessageText: ColorValueSchema,\n\t\tcustomMessageBg: ColorValueSchema,\n\t\tcustomMessageText: ColorValueSchema,\n\t\tcustomMessageLabel: ColorValueSchema,\n\t\ttoolPendingBg: ColorValueSchema,\n\t\ttoolSuccessBg: ColorValueSchema,\n\t\ttoolErrorBg: ColorValueSchema,\n\t\ttoolTitle: ColorValueSchema,\n\t\ttoolOutput: ColorValueSchema,\n\t\t// Markdown (10 colors)\n\t\tmdHeading: ColorValueSchema,\n\t\tmdLink: ColorValueSchema,\n\t\tmdLinkUrl: ColorValueSchema,\n\t\tmdCode: ColorValueSchema,\n\t\tmdCodeBlock: ColorValueSchema,\n\t\tmdCodeBlockBorder: ColorValueSchema,\n\t\tmdQuote: ColorValueSchema,\n\t\tmdQuoteBorder: ColorValueSchema,\n\t\tmdHr: ColorValueSchema,\n\t\tmdListBullet: ColorValueSchema,\n\t\t// Tool Diffs (3 colors)\n\t\ttoolDiffAdded: ColorValueSchema,\n\t\ttoolDiffRemoved: ColorValueSchema,\n\t\ttoolDiffContext: ColorValueSchema,\n\t\t// Syntax Highlighting (9 colors)\n\t\tsyntaxComment: ColorValueSchema,\n\t\tsyntaxKeyword: ColorValueSchema,\n\t\tsyntaxFunction: ColorValueSchema,\n\t\tsyntaxVariable: ColorValueSchema,\n\t\tsyntaxString: ColorValueSchema,\n\t\tsyntaxNumber: ColorValueSchema,\n\t\tsyntaxType: ColorValueSchema,\n\t\tsyntaxOperator: ColorValueSchema,\n\t\tsyntaxPunctuation: ColorValueSchema,\n\t\t// Thinking Level Borders (6 colors)\n\t\tthinkingOff: ColorValueSchema,\n\t\tthinkingMinimal: ColorValueSchema,\n\t\tthinkingLow: ColorValueSchema,\n\t\tthinkingMedium: ColorValueSchema,\n\t\tthinkingHigh: ColorValueSchema,\n\t\tthinkingXhigh: ColorValueSchema,\n\t\t// Bash Mode (1 color)\n\t\tbashMode: ColorValueSchema,\n\t\t// Agent identity palette (6 colors). OPTIONAL so existing custom themes\n\t\t// keep validating: any missing entry falls back to `accent` (see\n\t\t// createTheme). Subagent types hash into this palette so the same agent\n\t\t// reads as the same color across the chat, task panel, and TaskOutput.\n\t\tagent1: Type.Optional(ColorValueSchema),\n\t\tagent2: Type.Optional(ColorValueSchema),\n\t\tagent3: Type.Optional(ColorValueSchema),\n\t\tagent4: Type.Optional(ColorValueSchema),\n\t\tagent5: Type.Optional(ColorValueSchema),\n\t\tagent6: Type.Optional(ColorValueSchema),\n\t}),\n\texport: Type.Optional(\n\t\tType.Object({\n\t\t\tpageBg: Type.Optional(ColorValueSchema),\n\t\t\tcardBg: Type.Optional(ColorValueSchema),\n\t\t\tinfoBg: Type.Optional(ColorValueSchema),\n\t\t}),\n\t),\n});\n\ntype ThemeJson = Static<typeof ThemeJsonSchema>;\n\nconst validateThemeJson = Compile(ThemeJsonSchema);\n\nexport type ThemeColor =\n\t| \"accent\"\n\t| \"border\"\n\t| \"borderAccent\"\n\t| \"borderMuted\"\n\t| \"success\"\n\t| \"error\"\n\t| \"warning\"\n\t| \"muted\"\n\t| \"dim\"\n\t| \"text\"\n\t| \"thinkingText\"\n\t| \"userMessageText\"\n\t| \"customMessageText\"\n\t| \"customMessageLabel\"\n\t| \"toolTitle\"\n\t| \"toolOutput\"\n\t| \"mdHeading\"\n\t| \"mdLink\"\n\t| \"mdLinkUrl\"\n\t| \"mdCode\"\n\t| \"mdCodeBlock\"\n\t| \"mdCodeBlockBorder\"\n\t| \"mdQuote\"\n\t| \"mdQuoteBorder\"\n\t| \"mdHr\"\n\t| \"mdListBullet\"\n\t| \"toolDiffAdded\"\n\t| \"toolDiffRemoved\"\n\t| \"toolDiffContext\"\n\t| \"syntaxComment\"\n\t| \"syntaxKeyword\"\n\t| \"syntaxFunction\"\n\t| \"syntaxVariable\"\n\t| \"syntaxString\"\n\t| \"syntaxNumber\"\n\t| \"syntaxType\"\n\t| \"syntaxOperator\"\n\t| \"syntaxPunctuation\"\n\t| \"thinkingOff\"\n\t| \"thinkingMinimal\"\n\t| \"thinkingLow\"\n\t| \"thinkingMedium\"\n\t| \"thinkingHigh\"\n\t| \"thinkingXhigh\"\n\t| \"bashMode\"\n\t| \"agent1\"\n\t| \"agent2\"\n\t| \"agent3\"\n\t| \"agent4\"\n\t| \"agent5\"\n\t| \"agent6\";\n\n/** The agent identity palette, in hash order. */\nexport const AGENT_COLOR_TOKENS = [\n\t\"agent1\",\n\t\"agent2\",\n\t\"agent3\",\n\t\"agent4\",\n\t\"agent5\",\n\t\"agent6\",\n] as const satisfies readonly ThemeColor[];\n\n/**\n * Stable color token for an agent type (\"explore\", \"plan\", a custom agent…).\n * The name hashes into the agent palette, so the same type renders in the same\n * color everywhere it appears — chat tool line, task panel rows/roster, and\n * TaskOutput — for the whole session and across sessions. Distinct types spread\n * over 6 hues; collisions are possible but the `[name]`/`#N` labels stay the\n * primary identifier, color is the at-a-glance cue.\n */\nexport function agentColorFor(agentType: string): ThemeColor {\n\t// djb2: tiny, stable, good spread for short identifier-like strings.\n\tlet hash = 5381;\n\tfor (let i = 0; i < agentType.length; i++) {\n\t\thash = ((hash << 5) + hash + agentType.charCodeAt(i)) >>> 0;\n\t}\n\treturn AGENT_COLOR_TOKENS[hash % AGENT_COLOR_TOKENS.length] as ThemeColor;\n}\n\nexport type ThemeBg =\n\t| \"selectedBg\"\n\t| \"userMessageBg\"\n\t| \"customMessageBg\"\n\t| \"toolPendingBg\"\n\t| \"toolSuccessBg\"\n\t| \"toolErrorBg\";\n\ntype ColorMode = \"truecolor\" | \"256color\";\n\n// ============================================================================\n// Color Utilities\n// ============================================================================\n\nfunction detectColorMode(): ColorMode {\n\tconst colorterm = process.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\treturn \"truecolor\";\n\t}\n\t// Windows Terminal supports truecolor\n\tif (process.env.WT_SESSION) {\n\t\treturn \"truecolor\";\n\t}\n\tconst term = process.env.TERM || \"\";\n\t// Fall back to 256color for truly limited terminals\n\tif (term === \"dumb\" || term === \"\" || term === \"linux\") {\n\t\treturn \"256color\";\n\t}\n\t// Terminal.app also doesn't support truecolor\n\tif (process.env.TERM_PROGRAM === \"Apple_Terminal\") {\n\t\treturn \"256color\";\n\t}\n\t// GNU screen doesn't support truecolor unless explicitly opted in via COLORTERM=truecolor.\n\t// TERM under screen is typically \"screen\", \"screen-256color\", or \"screen.xterm-256color\".\n\tif (term === \"screen\" || term.startsWith(\"screen-\") || term.startsWith(\"screen.\")) {\n\t\treturn \"256color\";\n\t}\n\t// Assume truecolor for everything else - virtually all modern terminals support it\n\treturn \"truecolor\";\n}\n\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } {\n\tconst cleaned = hex.replace(\"#\", \"\");\n\tif (cleaned.length !== 6) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\tconst r = parseInt(cleaned.substring(0, 2), 16);\n\tconst g = parseInt(cleaned.substring(2, 4), 16);\n\tconst b = parseInt(cleaned.substring(4, 6), 16);\n\tif (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\treturn { r, g, b };\n}\n\n// The 6x6x6 color cube channel values (indices 0-5)\nconst CUBE_VALUES = [0, 95, 135, 175, 215, 255];\n\n// Grayscale ramp values (indices 232-255, 24 grays from 8 to 238)\nconst GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10);\n\nfunction findClosestCubeIndex(value: number): number {\n\tlet minDist = Infinity;\n\tlet minIdx = 0;\n\tfor (let i = 0; i < CUBE_VALUES.length; i++) {\n\t\tconst dist = Math.abs(value - CUBE_VALUES[i]);\n\t\tif (dist < minDist) {\n\t\t\tminDist = dist;\n\t\t\tminIdx = i;\n\t\t}\n\t}\n\treturn minIdx;\n}\n\nfunction findClosestGrayIndex(gray: number): number {\n\tlet minDist = Infinity;\n\tlet minIdx = 0;\n\tfor (let i = 0; i < GRAY_VALUES.length; i++) {\n\t\tconst dist = Math.abs(gray - GRAY_VALUES[i]);\n\t\tif (dist < minDist) {\n\t\t\tminDist = dist;\n\t\t\tminIdx = i;\n\t\t}\n\t}\n\treturn minIdx;\n}\n\nfunction colorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {\n\t// Weighted Euclidean distance (human eye is more sensitive to green)\n\tconst dr = r1 - r2;\n\tconst dg = g1 - g2;\n\tconst db = b1 - b2;\n\treturn dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;\n}\n\nfunction rgbTo256(r: number, g: number, b: number): number {\n\t// Find closest color in the 6x6x6 cube\n\tconst rIdx = findClosestCubeIndex(r);\n\tconst gIdx = findClosestCubeIndex(g);\n\tconst bIdx = findClosestCubeIndex(b);\n\tconst cubeR = CUBE_VALUES[rIdx];\n\tconst cubeG = CUBE_VALUES[gIdx];\n\tconst cubeB = CUBE_VALUES[bIdx];\n\tconst cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx;\n\tconst cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB);\n\n\t// Find closest grayscale\n\tconst gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);\n\tconst grayIdx = findClosestGrayIndex(gray);\n\tconst grayValue = GRAY_VALUES[grayIdx];\n\tconst grayIndex = 232 + grayIdx;\n\tconst grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue);\n\n\t// Check if color has noticeable saturation (hue matters)\n\t// If max-min spread is significant, prefer cube to preserve tint\n\tconst maxC = Math.max(r, g, b);\n\tconst minC = Math.min(r, g, b);\n\tconst spread = maxC - minC;\n\n\t// Only consider grayscale if color is nearly neutral (spread < 10)\n\t// AND grayscale is actually closer\n\tif (spread < 10 && grayDist < cubeDist) {\n\t\treturn grayIndex;\n\t}\n\n\treturn cubeIndex;\n}\n\nfunction hexTo256(hex: string): number {\n\tconst { r, g, b } = hexToRgb(hex);\n\treturn rgbTo256(r, g, b);\n}\n\nfunction fgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[39m\";\n\tif (typeof color === \"number\") return `\\x1b[38;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[38;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[38;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction bgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[49m\";\n\tif (typeof color === \"number\") return `\\x1b[48;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[48;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[48;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction resolveVarRefs(\n\tvalue: ColorValue,\n\tvars: Record<string, ColorValue>,\n\tvisited = new Set<string>(),\n): string | number {\n\tif (typeof value === \"number\" || value === \"\" || value.startsWith(\"#\")) {\n\t\treturn value;\n\t}\n\tif (visited.has(value)) {\n\t\tthrow new Error(`Circular variable reference detected: ${value}`);\n\t}\n\tif (!(value in vars)) {\n\t\tthrow new Error(`Variable reference not found: ${value}`);\n\t}\n\tvisited.add(value);\n\treturn resolveVarRefs(vars[value], vars, visited);\n}\n\nfunction resolveThemeColors<T extends Record<string, ColorValue>>(\n\tcolors: T,\n\tvars: Record<string, ColorValue> = {},\n): Record<keyof T, string | number> {\n\tconst resolved: Record<string, string | number> = {};\n\tfor (const [key, value] of Object.entries(colors)) {\n\t\tresolved[key] = resolveVarRefs(value, vars);\n\t}\n\treturn resolved as Record<keyof T, string | number>;\n}\n\n// ============================================================================\n// Theme Class\n// ============================================================================\n\nexport class Theme {\n\treadonly name?: string;\n\treadonly sourcePath?: string;\n\tsourceInfo?: SourceInfo;\n\tprivate fgColors: Map<ThemeColor, string>;\n\tprivate bgColors: Map<ThemeBg, string>;\n\tprivate mode: ColorMode;\n\n\tconstructor(\n\t\tfgColors: Record<ThemeColor, string | number>,\n\t\tbgColors: Record<ThemeBg, string | number>,\n\t\tmode: ColorMode,\n\t\toptions: { name?: string; sourcePath?: string; sourceInfo?: SourceInfo } = {},\n\t) {\n\t\tthis.name = options.name;\n\t\tthis.sourcePath = options.sourcePath;\n\t\tthis.sourceInfo = options.sourceInfo;\n\t\tthis.mode = mode;\n\t\tthis.fgColors = new Map();\n\t\tfor (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {\n\t\t\tthis.fgColors.set(key, fgAnsi(value, mode));\n\t\t}\n\t\t// Agent palette fallback for themes built before it existed (including\n\t\t// Theme instances constructed directly by extensions): default to accent.\n\t\tconst accentAnsi = this.fgColors.get(\"accent\");\n\t\tif (accentAnsi !== undefined) {\n\t\t\tfor (const token of AGENT_COLOR_TOKENS) {\n\t\t\t\tif (!this.fgColors.has(token)) {\n\t\t\t\t\tthis.fgColors.set(token, accentAnsi);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.bgColors = new Map();\n\t\tfor (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {\n\t\t\tthis.bgColors.set(key, bgAnsi(value, mode));\n\t\t}\n\t}\n\n\tfg(color: ThemeColor, text: string): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[39m`; // Reset only foreground color\n\t}\n\n\tbg(color: ThemeBg, text: string): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[49m`; // Reset only background color\n\t}\n\n\tbold(text: string): string {\n\t\treturn chalk.bold(text);\n\t}\n\n\tblink(text: string): string {\n\t\t// Terminal-native blink (SGR 5). Zero overhead; terminals that support\n\t\t// blink will animate, others render it as a steady glyph.\n\t\treturn `\\x1b[5m${text}\\x1b[25m`;\n\t}\n\n\titalic(text: string): string {\n\t\treturn chalk.italic(text);\n\t}\n\n\tunderline(text: string): string {\n\t\treturn chalk.underline(text);\n\t}\n\n\tinverse(text: string): string {\n\t\treturn chalk.inverse(text);\n\t}\n\n\tstrikethrough(text: string): string {\n\t\treturn chalk.strikethrough(text);\n\t}\n\n\tgetFgAnsi(color: ThemeColor): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetBgAnsi(color: ThemeBg): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetColorMode(): ColorMode {\n\t\treturn this.mode;\n\t}\n\n\tgetThinkingBorderColor(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"): (str: string) => string {\n\t\t// Map thinking levels to dedicated theme colors\n\t\tswitch (level) {\n\t\t\tcase \"off\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingOff\", str);\n\t\t\tcase \"minimal\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingMinimal\", str);\n\t\t\tcase \"low\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingLow\", str);\n\t\t\tcase \"medium\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingMedium\", str);\n\t\t\tcase \"high\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingHigh\", str);\n\t\t\tcase \"xhigh\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingXhigh\", str);\n\t\t\tdefault:\n\t\t\t\treturn (str: string) => this.fg(\"thinkingOff\", str);\n\t\t}\n\t}\n\n\tgetBashModeBorderColor(): (str: string) => string {\n\t\treturn (str: string) => this.fg(\"bashMode\", str);\n\t}\n}\n\n// ============================================================================\n// Theme Loading\n// ============================================================================\n\nlet BUILTIN_THEMES: Record<string, ThemeJson> | undefined;\n\nfunction getBuiltinThemes(): Record<string, ThemeJson> {\n\tif (!BUILTIN_THEMES) {\n\t\tconst themesDir = getThemesDir();\n\t\tconst darkPath = path.join(themesDir, \"dark.json\");\n\t\tconst lightPath = path.join(themesDir, \"light.json\");\n\t\tBUILTIN_THEMES = {\n\t\t\tdark: JSON.parse(fs.readFileSync(darkPath, \"utf-8\")) as ThemeJson,\n\t\t\tlight: JSON.parse(fs.readFileSync(lightPath, \"utf-8\")) as ThemeJson,\n\t\t};\n\t}\n\treturn BUILTIN_THEMES;\n}\n\nexport function getAvailableThemes(): string[] {\n\tconst themes = new Set<string>(Object.keys(getBuiltinThemes()));\n\tconst customThemesDir = getCustomThemesDir();\n\tif (fs.existsSync(customThemesDir)) {\n\t\tconst files = fs.readdirSync(customThemesDir);\n\t\tfor (const file of files) {\n\t\t\tif (file.endsWith(\".json\")) {\n\t\t\t\tthemes.add(file.slice(0, -5));\n\t\t\t}\n\t\t}\n\t}\n\tfor (const name of registeredThemes.keys()) {\n\t\tthemes.add(name);\n\t}\n\treturn Array.from(themes).sort();\n}\n\nexport interface ThemeInfo {\n\tname: string;\n\tpath: string | undefined;\n}\n\nexport function getAvailableThemesWithPaths(): ThemeInfo[] {\n\tconst themesDir = getThemesDir();\n\tconst customThemesDir = getCustomThemesDir();\n\tconst result: ThemeInfo[] = [];\n\n\t// Built-in themes\n\tfor (const name of Object.keys(getBuiltinThemes())) {\n\t\tresult.push({ name, path: path.join(themesDir, `${name}.json`) });\n\t}\n\n\t// Custom themes\n\tif (fs.existsSync(customThemesDir)) {\n\t\tfor (const file of fs.readdirSync(customThemesDir)) {\n\t\t\tif (file.endsWith(\".json\")) {\n\t\t\t\tconst name = file.slice(0, -5);\n\t\t\t\tif (!result.some((t) => t.name === name)) {\n\t\t\t\t\tresult.push({ name, path: path.join(customThemesDir, file) });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const [name, theme] of registeredThemes.entries()) {\n\t\tif (!result.some((t) => t.name === name)) {\n\t\t\tresult.push({ name, path: theme.sourcePath });\n\t\t}\n\t}\n\n\treturn result.sort((a, b) => a.name.localeCompare(b.name));\n}\n\nfunction parseThemeJson(label: string, json: unknown): ThemeJson {\n\tif (!validateThemeJson.Check(json)) {\n\t\tconst errors = Array.from(validateThemeJson.Errors(json));\n\t\tconst missingColors = new Set<string>();\n\t\tconst otherErrors: string[] = [];\n\n\t\tfor (const error of errors) {\n\t\t\tif (error.keyword === \"required\" && error.instancePath === \"/colors\") {\n\t\t\t\tconst requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;\n\t\t\t\tfor (const requiredProperty of requiredProperties ?? []) {\n\t\t\t\t\tmissingColors.add(requiredProperty);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst path = error.instancePath || \"/\";\n\t\t\totherErrors.push(` - ${path}: ${error.message}`);\n\t\t}\n\n\t\tlet errorMessage = `Invalid theme \"${label}\":\\n`;\n\t\tif (missingColors.size > 0) {\n\t\t\terrorMessage += \"\\nMissing required color tokens:\\n\";\n\t\t\terrorMessage += Array.from(missingColors)\n\t\t\t\t.sort()\n\t\t\t\t.map((color) => ` - ${color}`)\n\t\t\t\t.join(\"\\n\");\n\t\t\terrorMessage += '\\n\\nPlease add these colors to your theme\\'s \"colors\" object.';\n\t\t\terrorMessage += \"\\nSee the built-in themes (dark.json, light.json) for reference values.\";\n\t\t}\n\t\tif (otherErrors.length > 0) {\n\t\t\terrorMessage += `\\n\\nOther errors:\\n${otherErrors.join(\"\\n\")}`;\n\t\t}\n\n\t\tthrow new Error(errorMessage);\n\t}\n\n\treturn json as ThemeJson;\n}\n\nfunction parseThemeJsonContent(label: string, content: string): ThemeJson {\n\tlet json: unknown;\n\ttry {\n\t\tjson = JSON.parse(content);\n\t} catch (error) {\n\t\tthrow new Error(`Failed to parse theme ${label}: ${error}`);\n\t}\n\treturn parseThemeJson(label, json);\n}\n\nfunction loadThemeJson(name: string): ThemeJson {\n\tconst builtinThemes = getBuiltinThemes();\n\tif (name in builtinThemes) {\n\t\treturn builtinThemes[name];\n\t}\n\tconst registeredTheme = registeredThemes.get(name);\n\tif (registeredTheme?.sourcePath) {\n\t\tconst content = fs.readFileSync(registeredTheme.sourcePath, \"utf-8\");\n\t\treturn parseThemeJsonContent(registeredTheme.sourcePath, content);\n\t}\n\tif (registeredTheme) {\n\t\tthrow new Error(`Theme \"${name}\" does not have a source path for export`);\n\t}\n\tconst customThemesDir = getCustomThemesDir();\n\tconst themePath = path.join(customThemesDir, `${name}.json`);\n\tif (!fs.existsSync(themePath)) {\n\t\tthrow new Error(`Theme not found: ${name}`);\n\t}\n\tconst content = fs.readFileSync(themePath, \"utf-8\");\n\treturn parseThemeJsonContent(name, content);\n}\n\nfunction createTheme(themeJson: ThemeJson, mode?: ColorMode, sourcePath?: string): Theme {\n\tconst colorMode = mode ?? detectColorMode();\n\tconst resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);\n\tconst fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>;\n\tconst bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;\n\tconst bgColorKeys: Set<string> = new Set([\n\t\t\"selectedBg\",\n\t\t\"userMessageBg\",\n\t\t\"customMessageBg\",\n\t\t\"toolPendingBg\",\n\t\t\"toolSuccessBg\",\n\t\t\"toolErrorBg\",\n\t]);\n\tfor (const [key, value] of Object.entries(resolvedColors)) {\n\t\tif (bgColorKeys.has(key)) {\n\t\t\tbgColors[key as ThemeBg] = value;\n\t\t} else {\n\t\t\tfgColors[key as ThemeColor] = value;\n\t\t}\n\t}\n\t// The agent palette is optional in theme JSON (older custom themes predate\n\t// it); missing entries fall back to accent so theme.fg never throws.\n\tfor (const token of AGENT_COLOR_TOKENS) {\n\t\tif (fgColors[token] === undefined) {\n\t\t\tfgColors[token] = fgColors.accent;\n\t\t}\n\t}\n\treturn new Theme(fgColors, bgColors, colorMode, {\n\t\tname: themeJson.name,\n\t\tsourcePath,\n\t});\n}\n\nexport function loadThemeFromPath(themePath: string, mode?: ColorMode): Theme {\n\tconst content = fs.readFileSync(themePath, \"utf-8\");\n\tconst themeJson = parseThemeJsonContent(themePath, content);\n\treturn createTheme(themeJson, mode, themePath);\n}\n\nfunction loadTheme(name: string, mode?: ColorMode): Theme {\n\tconst registeredTheme = registeredThemes.get(name);\n\tif (registeredTheme) {\n\t\treturn registeredTheme;\n\t}\n\tconst themeJson = loadThemeJson(name);\n\treturn createTheme(themeJson, mode);\n}\n\nexport function getThemeByName(name: string): Theme | undefined {\n\ttry {\n\t\treturn loadTheme(name);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = process.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\tconst result = bg < 8 ? \"dark\" : \"light\";\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\treturn \"dark\";\n}\n\nfunction getDefaultTheme(): string {\n\treturn detectTerminalBackground();\n}\n\n// ============================================================================\n// Global Theme Instance\n// ============================================================================\n\n// Use globalThis to share theme across module loaders (tsx + jiti in dev mode)\nconst THEME_KEY = Symbol.for(\"@kolisachint/hoocode-agent:theme\");\n\n// Export theme as a getter that reads from globalThis\n// This ensures all module instances (tsx, jiti) see the same theme\nexport const theme: Theme = new Proxy({} as Theme, {\n\tget(_target, prop) {\n\t\tconst t = (globalThis as Record<symbol, Theme>)[THEME_KEY];\n\t\tif (!t) throw new Error(\"Theme not initialized. Call initTheme() first.\");\n\t\treturn (t as unknown as Record<string | symbol, unknown>)[prop];\n\t},\n});\n\nfunction setGlobalTheme(t: Theme): void {\n\t(globalThis as Record<symbol, Theme>)[THEME_KEY] = t;\n}\n\nlet currentThemeName: string | undefined;\nlet themeWatcher: fs.FSWatcher | undefined;\nlet themeReloadTimer: NodeJS.Timeout | undefined;\nlet onThemeChangeCallback: (() => void) | undefined;\nconst registeredThemes = new Map<string, Theme>();\n\nexport function setRegisteredThemes(themes: Theme[]): void {\n\tregisteredThemes.clear();\n\tfor (const theme of themes) {\n\t\tif (theme.name) {\n\t\t\tregisteredThemes.set(theme.name, theme);\n\t\t}\n\t}\n}\n\nexport function initTheme(themeName?: string, enableWatcher: boolean = false): void {\n\tconst name = themeName ?? getDefaultTheme();\n\tcurrentThemeName = name;\n\ttry {\n\t\tsetGlobalTheme(loadTheme(name));\n\t\tif (enableWatcher) {\n\t\t\tstartThemeWatcher();\n\t\t}\n\t} catch (_error) {\n\t\t// Theme is invalid - fall back to dark theme silently\n\t\tcurrentThemeName = \"dark\";\n\t\tsetGlobalTheme(loadTheme(\"dark\"));\n\t\t// Don't start watcher for fallback theme\n\t}\n}\n\nexport function setTheme(name: string, enableWatcher: boolean = false): { success: boolean; error?: string } {\n\tcurrentThemeName = name;\n\ttry {\n\t\tsetGlobalTheme(loadTheme(name));\n\t\tif (enableWatcher) {\n\t\t\tstartThemeWatcher();\n\t\t}\n\t\tif (onThemeChangeCallback) {\n\t\t\tonThemeChangeCallback();\n\t\t}\n\t\treturn { success: true };\n\t} catch (error) {\n\t\t// Theme is invalid - fall back to dark theme\n\t\tcurrentThemeName = \"dark\";\n\t\tsetGlobalTheme(loadTheme(\"dark\"));\n\t\t// Don't start watcher for fallback theme\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t};\n\t}\n}\n\nexport function setThemeInstance(themeInstance: Theme): void {\n\tsetGlobalTheme(themeInstance);\n\tcurrentThemeName = \"<in-memory>\";\n\tstopThemeWatcher(); // Can't watch a direct instance\n\tif (onThemeChangeCallback) {\n\t\tonThemeChangeCallback();\n\t}\n}\n\nexport function onThemeChange(callback: () => void): void {\n\tonThemeChangeCallback = callback;\n}\n\nfunction startThemeWatcher(): void {\n\tstopThemeWatcher();\n\n\t// Only watch if it's a custom theme (not built-in)\n\tif (!currentThemeName || currentThemeName === \"dark\" || currentThemeName === \"light\") {\n\t\treturn;\n\t}\n\n\tconst customThemesDir = getCustomThemesDir();\n\tconst watchedThemeName = currentThemeName;\n\tconst watchedFileName = `${watchedThemeName}.json`;\n\tconst themeFile = path.join(customThemesDir, watchedFileName);\n\n\t// Only watch if the file exists\n\tif (!fs.existsSync(themeFile)) {\n\t\treturn;\n\t}\n\n\tconst scheduleReload = () => {\n\t\tif (themeReloadTimer) {\n\t\t\tclearTimeout(themeReloadTimer);\n\t\t}\n\t\tthemeReloadTimer = setTimeout(() => {\n\t\t\tthemeReloadTimer = undefined;\n\n\t\t\t// Ignore stale timers after switching themes or stopping the watcher\n\t\t\tif (currentThemeName !== watchedThemeName) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Keep the last successfully loaded theme active if the file is temporarily missing\n\t\t\tif (!fs.existsSync(themeFile)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Reload the theme from disk and refresh the registry cache\n\t\t\t\tconst reloadedTheme = loadThemeFromPath(themeFile);\n\t\t\t\tregisteredThemes.set(watchedThemeName, reloadedTheme);\n\t\t\t\tsetGlobalTheme(reloadedTheme);\n\t\t\t\t// Notify callback (to invalidate UI)\n\t\t\t\tif (onThemeChangeCallback) {\n\t\t\t\t\tonThemeChangeCallback();\n\t\t\t\t}\n\t\t\t} catch (_error) {\n\t\t\t\t// Ignore errors (file might be in invalid state while being edited)\n\t\t\t}\n\t\t}, 100);\n\t};\n\n\tthemeWatcher =\n\t\twatchWithErrorHandler(\n\t\t\tcustomThemesDir,\n\t\t\t(_eventType, filename) => {\n\t\t\t\tif (currentThemeName !== watchedThemeName) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!filename) {\n\t\t\t\t\tscheduleReload();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (filename !== watchedFileName) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tscheduleReload();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tcloseWatcher(themeWatcher);\n\t\t\t\tthemeWatcher = undefined;\n\t\t\t},\n\t\t) ?? undefined;\n}\n\nexport function stopThemeWatcher(): void {\n\tif (themeReloadTimer) {\n\t\tclearTimeout(themeReloadTimer);\n\t\tthemeReloadTimer = undefined;\n\t}\n\tcloseWatcher(themeWatcher);\n\tthemeWatcher = undefined;\n}\n\n// ============================================================================\n// HTML Export Helpers\n// ============================================================================\n\n/**\n * Convert a 256-color index to hex string.\n * Indices 0-15: basic colors (approximate)\n * Indices 16-231: 6x6x6 color cube\n * Indices 232-255: grayscale ramp\n */\nfunction ansi256ToHex(index: number): string {\n\t// Basic colors (0-15) - approximate common terminal values\n\tconst basicColors = [\n\t\t\"#000000\",\n\t\t\"#800000\",\n\t\t\"#008000\",\n\t\t\"#808000\",\n\t\t\"#000080\",\n\t\t\"#800080\",\n\t\t\"#008080\",\n\t\t\"#c0c0c0\",\n\t\t\"#808080\",\n\t\t\"#ff0000\",\n\t\t\"#00ff00\",\n\t\t\"#ffff00\",\n\t\t\"#0000ff\",\n\t\t\"#ff00ff\",\n\t\t\"#00ffff\",\n\t\t\"#ffffff\",\n\t];\n\tif (index < 16) {\n\t\treturn basicColors[index];\n\t}\n\n\t// Color cube (16-231): 6x6x6 = 216 colors\n\tif (index < 232) {\n\t\tconst cubeIndex = index - 16;\n\t\tconst r = Math.floor(cubeIndex / 36);\n\t\tconst g = Math.floor((cubeIndex % 36) / 6);\n\t\tconst b = cubeIndex % 6;\n\t\tconst toHex = (n: number) => (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, \"0\");\n\t\treturn `#${toHex(r)}${toHex(g)}${toHex(b)}`;\n\t}\n\n\t// Grayscale (232-255): 24 shades\n\tconst gray = 8 + (index - 232) * 10;\n\tconst grayHex = gray.toString(16).padStart(2, \"0\");\n\treturn `#${grayHex}${grayHex}${grayHex}`;\n}\n\n/**\n * Get resolved theme colors as CSS-compatible hex strings.\n * Used by HTML export to generate CSS custom properties.\n */\nexport function getResolvedThemeColors(themeName?: string): Record<string, string> {\n\tconst name = themeName ?? currentThemeName ?? getDefaultTheme();\n\tconst isLight = name === \"light\";\n\tconst themeJson = loadThemeJson(name);\n\tconst resolved = resolveThemeColors(themeJson.colors, themeJson.vars);\n\n\t// Default text color for empty values (terminal uses default fg color)\n\tconst defaultText = isLight ? \"#000000\" : \"#e5e5e7\";\n\n\tconst cssColors: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(resolved)) {\n\t\tif (typeof value === \"number\") {\n\t\t\tcssColors[key] = ansi256ToHex(value);\n\t\t} else if (value === \"\") {\n\t\t\t// Empty means default terminal color - use sensible fallback for HTML\n\t\t\tcssColors[key] = defaultText;\n\t\t} else {\n\t\t\tcssColors[key] = value;\n\t\t}\n\t}\n\treturn cssColors;\n}\n\n/**\n * Check if a theme is a \"light\" theme (for CSS that needs light/dark variants).\n */\nexport function isLightTheme(themeName?: string): boolean {\n\t// Currently just check the name - could be extended to analyze colors\n\treturn themeName === \"light\";\n}\n\n/**\n * Get explicit export colors from theme JSON, if specified.\n * Returns undefined for each color that isn't explicitly set.\n */\nexport function getThemeExportColors(themeName?: string): {\n\tpageBg?: string;\n\tcardBg?: string;\n\tinfoBg?: string;\n} {\n\tconst name = themeName ?? currentThemeName ?? getDefaultTheme();\n\ttry {\n\t\tconst themeJson = loadThemeJson(name);\n\t\tconst exportSection = themeJson.export;\n\t\tif (!exportSection) return {};\n\n\t\tconst vars = themeJson.vars ?? {};\n\t\tconst resolve = (value: ColorValue | undefined): string | undefined => {\n\t\t\tif (value === undefined) return undefined;\n\t\t\tconst resolved = resolveVarRefs(value, vars);\n\t\t\tif (typeof resolved === \"number\") return ansi256ToHex(resolved);\n\t\t\tif (resolved === \"\") return undefined;\n\t\t\treturn resolved;\n\t\t};\n\n\t\treturn {\n\t\t\tpageBg: resolve(exportSection.pageBg),\n\t\t\tcardBg: resolve(exportSection.cardBg),\n\t\t\tinfoBg: resolve(exportSection.infoBg),\n\t\t};\n\t} catch {\n\t\treturn {};\n\t}\n}\n\n// ============================================================================\n// TUI Helpers\n// ============================================================================\n\ntype CliHighlightTheme = Record<string, (s: string) => string>;\n\nlet cachedHighlightThemeFor: Theme | undefined;\nlet cachedCliHighlightTheme: CliHighlightTheme | undefined;\n\nfunction buildCliHighlightTheme(t: Theme): CliHighlightTheme {\n\treturn {\n\t\tkeyword: (s: string) => t.fg(\"syntaxKeyword\", s),\n\t\tbuilt_in: (s: string) => t.fg(\"syntaxType\", s),\n\t\tliteral: (s: string) => t.fg(\"syntaxNumber\", s),\n\t\tnumber: (s: string) => t.fg(\"syntaxNumber\", s),\n\t\tstring: (s: string) => t.fg(\"syntaxString\", s),\n\t\tcomment: (s: string) => t.fg(\"syntaxComment\", s),\n\t\tfunction: (s: string) => t.fg(\"syntaxFunction\", s),\n\t\ttitle: (s: string) => t.fg(\"syntaxFunction\", s),\n\t\tclass: (s: string) => t.fg(\"syntaxType\", s),\n\t\ttype: (s: string) => t.fg(\"syntaxType\", s),\n\t\tattr: (s: string) => t.fg(\"syntaxVariable\", s),\n\t\tvariable: (s: string) => t.fg(\"syntaxVariable\", s),\n\t\tparams: (s: string) => t.fg(\"syntaxVariable\", s),\n\t\toperator: (s: string) => t.fg(\"syntaxOperator\", s),\n\t\tpunctuation: (s: string) => t.fg(\"syntaxPunctuation\", s),\n\t};\n}\n\nfunction getCliHighlightTheme(t: Theme): CliHighlightTheme {\n\tif (cachedHighlightThemeFor !== t || !cachedCliHighlightTheme) {\n\t\tcachedHighlightThemeFor = t;\n\t\tcachedCliHighlightTheme = buildCliHighlightTheme(t);\n\t}\n\treturn cachedCliHighlightTheme;\n}\n\n/**\n * Highlight code with syntax coloring based on file extension or language.\n * Returns array of highlighted lines.\n */\nexport function highlightCode(code: string, lang?: string): string[] {\n\t// Validate language before highlighting to avoid stderr spam from cli-highlight\n\tconst validLang = lang && supportsLanguage(lang) ? lang : undefined;\n\t// Skip highlighting when no valid language is specified. cli-highlight's\n\t// auto-detection is unreliable and can misidentify prose as AppleScript,\n\t// LiveCodeServer, etc., coloring random English words as keywords.\n\tif (!validLang) {\n\t\treturn code.split(\"\\n\").map((line) => theme.fg(\"mdCodeBlock\", line));\n\t}\n\tconst opts = {\n\t\tlanguage: validLang,\n\t\tignoreIllegals: true,\n\t\ttheme: getCliHighlightTheme(theme),\n\t};\n\ttry {\n\t\treturn highlight(code, opts).split(\"\\n\");\n\t} catch {\n\t\treturn code.split(\"\\n\");\n\t}\n}\n\n/**\n * Get language identifier from file path extension.\n */\nexport function getLanguageFromPath(filePath: string): string | undefined {\n\tconst ext = filePath.split(\".\").pop()?.toLowerCase();\n\tif (!ext) return undefined;\n\n\tconst extToLang: Record<string, string> = {\n\t\tts: \"typescript\",\n\t\ttsx: \"typescript\",\n\t\tjs: \"javascript\",\n\t\tjsx: \"javascript\",\n\t\tmjs: \"javascript\",\n\t\tcjs: \"javascript\",\n\t\tpy: \"python\",\n\t\trb: \"ruby\",\n\t\trs: \"rust\",\n\t\tgo: \"go\",\n\t\tjava: \"java\",\n\t\tkt: \"kotlin\",\n\t\tswift: \"swift\",\n\t\tc: \"c\",\n\t\th: \"c\",\n\t\tcpp: \"cpp\",\n\t\tcc: \"cpp\",\n\t\tcxx: \"cpp\",\n\t\thpp: \"cpp\",\n\t\tcs: \"csharp\",\n\t\tphp: \"php\",\n\t\tsh: \"bash\",\n\t\tbash: \"bash\",\n\t\tzsh: \"bash\",\n\t\tfish: \"fish\",\n\t\tps1: \"powershell\",\n\t\tsql: \"sql\",\n\t\thtml: \"html\",\n\t\thtm: \"html\",\n\t\tcss: \"css\",\n\t\tscss: \"scss\",\n\t\tsass: \"sass\",\n\t\tless: \"less\",\n\t\tjson: \"json\",\n\t\tyaml: \"yaml\",\n\t\tyml: \"yaml\",\n\t\ttoml: \"toml\",\n\t\txml: \"xml\",\n\t\tmd: \"markdown\",\n\t\tmarkdown: \"markdown\",\n\t\tdockerfile: \"dockerfile\",\n\t\tmakefile: \"makefile\",\n\t\tcmake: \"cmake\",\n\t\tlua: \"lua\",\n\t\tperl: \"perl\",\n\t\tr: \"r\",\n\t\tscala: \"scala\",\n\t\tclj: \"clojure\",\n\t\tex: \"elixir\",\n\t\texs: \"elixir\",\n\t\terl: \"erlang\",\n\t\ths: \"haskell\",\n\t\tml: \"ocaml\",\n\t\tvim: \"vim\",\n\t\tgraphql: \"graphql\",\n\t\tproto: \"protobuf\",\n\t\ttf: \"hcl\",\n\t\thcl: \"hcl\",\n\t};\n\n\treturn extToLang[ext];\n}\n\nexport function getMarkdownTheme(): MarkdownTheme {\n\treturn {\n\t\theading: (text: string) => theme.fg(\"mdHeading\", text),\n\t\tlink: (text: string) => theme.fg(\"mdLink\", text),\n\t\tlinkUrl: (text: string) => theme.fg(\"mdLinkUrl\", text),\n\t\tcode: (text: string) => theme.fg(\"mdCode\", text),\n\t\tcodeBlock: (text: string) => theme.fg(\"mdCodeBlock\", text),\n\t\tcodeBlockBorder: (text: string) => theme.fg(\"mdCodeBlockBorder\", text),\n\t\tquote: (text: string) => theme.fg(\"mdQuote\", text),\n\t\tquoteBorder: (text: string) => theme.fg(\"mdQuoteBorder\", text),\n\t\thr: (text: string) => theme.fg(\"mdHr\", text),\n\t\tlistBullet: (text: string) => theme.fg(\"mdListBullet\", text),\n\t\tbold: (text: string) => theme.bold(text),\n\t\titalic: (text: string) => theme.italic(text),\n\t\tunderline: (text: string) => theme.underline(text),\n\t\tstrikethrough: (text: string) => chalk.strikethrough(text),\n\t\thighlightCode: (code: string, lang?: string): string[] => {\n\t\t\t// Validate language before highlighting to avoid stderr spam from cli-highlight\n\t\t\tconst validLang = lang && supportsLanguage(lang) ? lang : undefined;\n\t\t\t// Skip highlighting when no valid language is specified. cli-highlight's\n\t\t\t// auto-detection is unreliable and can misidentify prose as AppleScript,\n\t\t\t// LiveCodeServer, etc., coloring random English words as keywords.\n\t\t\tif (!validLang) {\n\t\t\t\treturn code.split(\"\\n\").map((line) => theme.fg(\"mdCodeBlock\", line));\n\t\t\t}\n\t\t\tconst opts = {\n\t\t\t\tlanguage: validLang,\n\t\t\t\tignoreIllegals: true,\n\t\t\t\ttheme: getCliHighlightTheme(theme),\n\t\t\t};\n\t\t\ttry {\n\t\t\t\treturn highlight(code, opts).split(\"\\n\");\n\t\t\t} catch {\n\t\t\t\treturn code.split(\"\\n\").map((line) => theme.fg(\"mdCodeBlock\", line));\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport function getSelectListTheme(): SelectListTheme {\n\treturn {\n\t\tselectedPrefix: (text: string) => theme.fg(\"accent\", text),\n\t\tselectedText: (text: string) => theme.fg(\"accent\", text),\n\t\tdescription: (text: string) => theme.fg(\"muted\", text),\n\t\tscrollInfo: (text: string) => theme.fg(\"muted\", text),\n\t\tnoMatch: (text: string) => theme.fg(\"muted\", text),\n\t};\n}\n\nexport function getEditorTheme(): EditorTheme {\n\treturn {\n\t\tborderColor: (text: string) => theme.fg(\"borderMuted\", text),\n\t\tselectList: getSelectListTheme(),\n\t};\n}\n\nexport function getSettingsListTheme(): import(\"@kolisachint/hoocode-tui\").SettingsListTheme {\n\treturn {\n\t\tlabel: (text: string, selected: boolean) => (selected ? theme.fg(\"accent\", text) : text),\n\t\tvalue: (text: string, selected: boolean) => (selected ? theme.fg(\"accent\", text) : theme.fg(\"muted\", text)),\n\t\tdescription: (text: string) => theme.fg(\"dim\", text),\n\t\tcursor: theme.fg(\"accent\", \"→ \"),\n\t\thint: (text: string) => theme.fg(\"dim\", text),\n\t};\n}\n"]}
|
|
@@ -76,6 +76,16 @@ const ThemeJsonSchema = Type.Object({
|
|
|
76
76
|
thinkingXhigh: ColorValueSchema,
|
|
77
77
|
// Bash Mode (1 color)
|
|
78
78
|
bashMode: ColorValueSchema,
|
|
79
|
+
// Agent identity palette (6 colors). OPTIONAL so existing custom themes
|
|
80
|
+
// keep validating: any missing entry falls back to `accent` (see
|
|
81
|
+
// createTheme). Subagent types hash into this palette so the same agent
|
|
82
|
+
// reads as the same color across the chat, task panel, and TaskOutput.
|
|
83
|
+
agent1: Type.Optional(ColorValueSchema),
|
|
84
|
+
agent2: Type.Optional(ColorValueSchema),
|
|
85
|
+
agent3: Type.Optional(ColorValueSchema),
|
|
86
|
+
agent4: Type.Optional(ColorValueSchema),
|
|
87
|
+
agent5: Type.Optional(ColorValueSchema),
|
|
88
|
+
agent6: Type.Optional(ColorValueSchema),
|
|
79
89
|
}),
|
|
80
90
|
export: Type.Optional(Type.Object({
|
|
81
91
|
pageBg: Type.Optional(ColorValueSchema),
|
|
@@ -84,6 +94,31 @@ const ThemeJsonSchema = Type.Object({
|
|
|
84
94
|
})),
|
|
85
95
|
});
|
|
86
96
|
const validateThemeJson = Compile(ThemeJsonSchema);
|
|
97
|
+
/** The agent identity palette, in hash order. */
|
|
98
|
+
export const AGENT_COLOR_TOKENS = [
|
|
99
|
+
"agent1",
|
|
100
|
+
"agent2",
|
|
101
|
+
"agent3",
|
|
102
|
+
"agent4",
|
|
103
|
+
"agent5",
|
|
104
|
+
"agent6",
|
|
105
|
+
];
|
|
106
|
+
/**
|
|
107
|
+
* Stable color token for an agent type ("explore", "plan", a custom agent…).
|
|
108
|
+
* The name hashes into the agent palette, so the same type renders in the same
|
|
109
|
+
* color everywhere it appears — chat tool line, task panel rows/roster, and
|
|
110
|
+
* TaskOutput — for the whole session and across sessions. Distinct types spread
|
|
111
|
+
* over 6 hues; collisions are possible but the `[name]`/`#N` labels stay the
|
|
112
|
+
* primary identifier, color is the at-a-glance cue.
|
|
113
|
+
*/
|
|
114
|
+
export function agentColorFor(agentType) {
|
|
115
|
+
// djb2: tiny, stable, good spread for short identifier-like strings.
|
|
116
|
+
let hash = 5381;
|
|
117
|
+
for (let i = 0; i < agentType.length; i++) {
|
|
118
|
+
hash = ((hash << 5) + hash + agentType.charCodeAt(i)) >>> 0;
|
|
119
|
+
}
|
|
120
|
+
return AGENT_COLOR_TOKENS[hash % AGENT_COLOR_TOKENS.length];
|
|
121
|
+
}
|
|
87
122
|
// ============================================================================
|
|
88
123
|
// Color Utilities
|
|
89
124
|
// ============================================================================
|
|
@@ -266,6 +301,16 @@ export class Theme {
|
|
|
266
301
|
for (const [key, value] of Object.entries(fgColors)) {
|
|
267
302
|
this.fgColors.set(key, fgAnsi(value, mode));
|
|
268
303
|
}
|
|
304
|
+
// Agent palette fallback for themes built before it existed (including
|
|
305
|
+
// Theme instances constructed directly by extensions): default to accent.
|
|
306
|
+
const accentAnsi = this.fgColors.get("accent");
|
|
307
|
+
if (accentAnsi !== undefined) {
|
|
308
|
+
for (const token of AGENT_COLOR_TOKENS) {
|
|
309
|
+
if (!this.fgColors.has(token)) {
|
|
310
|
+
this.fgColors.set(token, accentAnsi);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
269
314
|
this.bgColors = new Map();
|
|
270
315
|
for (const [key, value] of Object.entries(bgColors)) {
|
|
271
316
|
this.bgColors.set(key, bgAnsi(value, mode));
|
|
@@ -484,6 +529,13 @@ function createTheme(themeJson, mode, sourcePath) {
|
|
|
484
529
|
fgColors[key] = value;
|
|
485
530
|
}
|
|
486
531
|
}
|
|
532
|
+
// The agent palette is optional in theme JSON (older custom themes predate
|
|
533
|
+
// it); missing entries fall back to accent so theme.fg never throws.
|
|
534
|
+
for (const token of AGENT_COLOR_TOKENS) {
|
|
535
|
+
if (fgColors[token] === undefined) {
|
|
536
|
+
fgColors[token] = fgColors.accent;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
487
539
|
return new Theme(fgColors, bgColors, colorMode, {
|
|
488
540
|
name: themeJson.name,
|
|
489
541
|
sourcePath,
|