@oh-my-pi/pi-coding-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,2211 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
4
+ import { type Static, Type } from "@sinclair/typebox";
5
+ import { TypeCompiler } from "@sinclair/typebox/compiler";
6
+ import chalk from "chalk";
7
+ import { highlight, supportsLanguage } from "cli-highlight";
8
+ import { getCustomThemesDir } from "../../../config";
9
+ import { logger } from "../../../core/logger";
10
+ // Embed theme JSON files at build time
11
+ import darkThemeJson from "./dark.json" with { type: "json" };
12
+ import { defaultThemes } from "./defaults";
13
+ import lightThemeJson from "./light.json" with { type: "json" };
14
+
15
+ // ============================================================================
16
+ // Symbol Presets
17
+ // ============================================================================
18
+
19
+ export type SymbolPreset = "unicode" | "nerd" | "ascii";
20
+
21
+ /**
22
+ * All available symbol keys organized by category.
23
+ */
24
+ export type SymbolKey =
25
+ // Status Indicators
26
+ | "status.success"
27
+ | "status.error"
28
+ | "status.warning"
29
+ | "status.info"
30
+ | "status.pending"
31
+ | "status.disabled"
32
+ | "status.enabled"
33
+ | "status.running"
34
+ | "status.shadowed"
35
+ | "status.aborted"
36
+ // Navigation
37
+ | "nav.cursor"
38
+ | "nav.selected"
39
+ | "nav.expand"
40
+ | "nav.collapse"
41
+ | "nav.back"
42
+ // Tree Connectors
43
+ | "tree.branch"
44
+ | "tree.last"
45
+ | "tree.vertical"
46
+ | "tree.horizontal"
47
+ | "tree.hook"
48
+ // Box Drawing - Rounded
49
+ | "boxRound.topLeft"
50
+ | "boxRound.topRight"
51
+ | "boxRound.bottomLeft"
52
+ | "boxRound.bottomRight"
53
+ | "boxRound.horizontal"
54
+ | "boxRound.vertical"
55
+ // Box Drawing - Sharp
56
+ | "boxSharp.topLeft"
57
+ | "boxSharp.topRight"
58
+ | "boxSharp.bottomLeft"
59
+ | "boxSharp.bottomRight"
60
+ | "boxSharp.horizontal"
61
+ | "boxSharp.vertical"
62
+ | "boxSharp.cross"
63
+ | "boxSharp.teeDown"
64
+ | "boxSharp.teeUp"
65
+ | "boxSharp.teeRight"
66
+ | "boxSharp.teeLeft"
67
+ // Separators
68
+ | "sep.powerline"
69
+ | "sep.powerlineThin"
70
+ | "sep.powerlineLeft"
71
+ | "sep.powerlineRight"
72
+ | "sep.powerlineThinLeft"
73
+ | "sep.powerlineThinRight"
74
+ | "sep.block"
75
+ | "sep.space"
76
+ | "sep.asciiLeft"
77
+ | "sep.asciiRight"
78
+ | "sep.dot"
79
+ | "sep.slash"
80
+ | "sep.pipe"
81
+ // Icons
82
+ | "icon.model"
83
+ | "icon.folder"
84
+ | "icon.file"
85
+ | "icon.git"
86
+ | "icon.branch"
87
+ | "icon.tokens"
88
+ | "icon.context"
89
+ | "icon.cost"
90
+ | "icon.time"
91
+ | "icon.pi"
92
+ | "icon.agents"
93
+ | "icon.cache"
94
+ | "icon.input"
95
+ | "icon.output"
96
+ | "icon.host"
97
+ | "icon.session"
98
+ | "icon.package"
99
+ | "icon.warning"
100
+ | "icon.rewind"
101
+ | "icon.auto"
102
+ | "icon.extensionSkill"
103
+ | "icon.extensionTool"
104
+ | "icon.extensionSlashCommand"
105
+ | "icon.extensionMcp"
106
+ | "icon.extensionRule"
107
+ | "icon.extensionHook"
108
+ | "icon.extensionPrompt"
109
+ | "icon.extensionContextFile"
110
+ | "icon.extensionInstruction"
111
+ // Thinking Levels
112
+ | "thinking.minimal"
113
+ | "thinking.low"
114
+ | "thinking.medium"
115
+ | "thinking.high"
116
+ | "thinking.xhigh"
117
+ // Checkboxes
118
+ | "checkbox.checked"
119
+ | "checkbox.unchecked"
120
+ // Text Formatting
121
+ | "format.ellipsis"
122
+ | "format.bullet"
123
+ | "format.dash"
124
+ | "format.bracketLeft"
125
+ | "format.bracketRight"
126
+ // Markdown-specific
127
+ | "md.quoteBorder"
128
+ | "md.hrChar"
129
+ | "md.bullet"
130
+ // Language/file type icons
131
+ | "lang.default"
132
+ | "lang.typescript"
133
+ | "lang.javascript"
134
+ | "lang.python"
135
+ | "lang.rust"
136
+ | "lang.go"
137
+ | "lang.java"
138
+ | "lang.c"
139
+ | "lang.cpp"
140
+ | "lang.csharp"
141
+ | "lang.ruby"
142
+ | "lang.php"
143
+ | "lang.swift"
144
+ | "lang.kotlin"
145
+ | "lang.shell"
146
+ | "lang.html"
147
+ | "lang.css"
148
+ | "lang.json"
149
+ | "lang.yaml"
150
+ | "lang.markdown"
151
+ | "lang.sql"
152
+ | "lang.docker"
153
+ | "lang.lua"
154
+ | "lang.text"
155
+ | "lang.env"
156
+ | "lang.toml"
157
+ | "lang.xml"
158
+ | "lang.ini"
159
+ | "lang.conf"
160
+ | "lang.log"
161
+ | "lang.csv"
162
+ | "lang.tsv"
163
+ | "lang.image"
164
+ | "lang.pdf"
165
+ | "lang.archive"
166
+ | "lang.binary";
167
+
168
+ type SymbolMap = Record<SymbolKey, string>;
169
+
170
+ const UNICODE_SYMBOLS: SymbolMap = {
171
+ // Status Indicators
172
+ // pick: ✓ | alt: ✔ ✅ ☑ ✚
173
+ "status.success": "✓",
174
+ // pick: ✗ | alt: ✘ ✖ ❌ ⨯
175
+ "status.error": "✗",
176
+ // pick: ⚠ | alt: ‼ ⁉ ▲ △
177
+ "status.warning": "⚠",
178
+ // pick: ℹ | alt: ⓘ 🛈 ⅈ
179
+ "status.info": "ℹ",
180
+ // pick: ◔ | alt: ● ◐ ◑ ◒ ◓ ⏳ …
181
+ "status.pending": "◔",
182
+ // pick: ○ | alt: ◌ ◯ ⃠
183
+ "status.disabled": "○",
184
+ // pick: ● | alt: ◉ ◎ ⬤
185
+ "status.enabled": "●",
186
+ // pick: ↻ | alt: ↺ ⟳ ⟲ ◐ ▶
187
+ "status.running": "↻",
188
+ // pick: ◐ | alt: ◑ ◒ ◓ ◔
189
+ "status.shadowed": "◐",
190
+ // pick: ⊗ | alt: ⊘ ⛔ ⏹ ⨂
191
+ "status.aborted": "⊗",
192
+ // Navigation
193
+ // pick: ❯ | alt: › ▸ ▹
194
+ "nav.cursor": "❯",
195
+ // pick: ➜ | alt: → ➤ ➔ ⇒
196
+ "nav.selected": "➜",
197
+ // pick: ▸ | alt: ▶ ▹ ⯈
198
+ "nav.expand": "▸",
199
+ // pick: ▾ | alt: ▼ ▽ ⯆
200
+ "nav.collapse": "▾",
201
+ // pick: ← | alt: ↩ ↫ ⇦
202
+ "nav.back": "←",
203
+ // Tree Connectors
204
+ // pick: ├─ | alt: ├╴ ├╌ ├┄ ╠═
205
+ "tree.branch": "├─",
206
+ // pick: └─ | alt: └╴ └╌ └┄ ╚═
207
+ "tree.last": "└─",
208
+ // pick: │ | alt: ┃ ║ ▏ ▕
209
+ "tree.vertical": "│",
210
+ // pick: ─ | alt: ━ ═ ╌ ┄
211
+ "tree.horizontal": "─",
212
+ // pick: ⎿ | alt: └ ╰ ↳
213
+ "tree.hook": "⎿",
214
+ // Box Drawing - Rounded
215
+ // pick: ╭ | alt: ┌ ┏ ╔
216
+ "boxRound.topLeft": "╭",
217
+ // pick: ╮ | alt: ┐ ┓ ╗
218
+ "boxRound.topRight": "╮",
219
+ // pick: ╰ | alt: └ ┗ ╚
220
+ "boxRound.bottomLeft": "╰",
221
+ // pick: ╯ | alt: ┘ ┛ ╝
222
+ "boxRound.bottomRight": "╯",
223
+ // pick: ─ | alt: ━ ═ ╌
224
+ "boxRound.horizontal": "─",
225
+ // pick: │ | alt: ┃ ║ ▏
226
+ "boxRound.vertical": "│",
227
+ // Box Drawing - Sharp
228
+ // pick: ┌ | alt: ┏ ╭ ╔
229
+ "boxSharp.topLeft": "┌",
230
+ // pick: ┐ | alt: ┓ ╮ ╗
231
+ "boxSharp.topRight": "┐",
232
+ // pick: └ | alt: ┗ ╰ ╚
233
+ "boxSharp.bottomLeft": "└",
234
+ // pick: ┘ | alt: ┛ ╯ ╝
235
+ "boxSharp.bottomRight": "┘",
236
+ // pick: ─ | alt: ━ ═ ╌
237
+ "boxSharp.horizontal": "─",
238
+ // pick: │ | alt: ┃ ║ ▏
239
+ "boxSharp.vertical": "│",
240
+ // pick: ┼ | alt: ╋ ╬ ┿
241
+ "boxSharp.cross": "┼",
242
+ // pick: ┬ | alt: ╦ ┯ ┳
243
+ "boxSharp.teeDown": "┬",
244
+ // pick: ┴ | alt: ╩ ┷ ┻
245
+ "boxSharp.teeUp": "┴",
246
+ // pick: ├ | alt: ╠ ┝ ┣
247
+ "boxSharp.teeRight": "├",
248
+ // pick: ┤ | alt: ╣ ┥ ┫
249
+ "boxSharp.teeLeft": "┤",
250
+ // Separators
251
+ // pick: │ | alt: ┃ ║ ▏
252
+ "sep.powerline": "│",
253
+ // pick: │ | alt: ┆ ┊
254
+ "sep.powerlineThin": "│",
255
+ // pick: > | alt: › ▸ »
256
+ "sep.powerlineLeft": ">",
257
+ // pick: < | alt: ‹ ◂ «
258
+ "sep.powerlineRight": "<",
259
+ // pick: > | alt: › ▸
260
+ "sep.powerlineThinLeft": ">",
261
+ // pick: < | alt: ‹ ◂
262
+ "sep.powerlineThinRight": "<",
263
+ // pick: █ | alt: ▓ ▒ ░ ▉ ▌
264
+ "sep.block": "█",
265
+ // pick: space | alt: ␠ ·
266
+ "sep.space": " ",
267
+ // pick: > | alt: › » ▸
268
+ "sep.asciiLeft": ">",
269
+ // pick: < | alt: ‹ « ◂
270
+ "sep.asciiRight": "<",
271
+ // pick: · | alt: • ⋅
272
+ "sep.dot": " · ",
273
+ // pick: / | alt: / ∕ ⁄
274
+ "sep.slash": " / ",
275
+ // pick: | | alt: │ ┃ ║
276
+ "sep.pipe": " | ",
277
+ // Icons
278
+ // pick: ◈ | alt: ◆ ⬢ ◇
279
+ "icon.model": "◈",
280
+ // pick: 📁 | alt: 📂 🗂 🗃
281
+ "icon.folder": "📁",
282
+ // pick: 📄 | alt: 📃 📝
283
+ "icon.file": "📄",
284
+ // pick: ⎇ | alt: 🔀 ⑂
285
+ "icon.git": "⎇",
286
+ // pick: ⎇ | alt: 🌿 ⑂
287
+ "icon.branch": "⎇",
288
+ // pick: ⊛ | alt: ◎ ◍ ⊙
289
+ "icon.tokens": "⊛",
290
+ // pick: ◫ | alt: ◧ ▣ ▦
291
+ "icon.context": "◫",
292
+ // pick: $ | alt: 💲 💰
293
+ "icon.cost": "$",
294
+ // pick: ◷ | alt: ⏱ ⏲ ⌛
295
+ "icon.time": "◷",
296
+ // pick: π | alt: ∏ ∑
297
+ "icon.pi": "π",
298
+ // pick: AG | alt: 👥 👤
299
+ "icon.agents": "AG",
300
+ // pick: cache | alt: 💾 🗄
301
+ "icon.cache": "cache",
302
+ // pick: in: | alt: ⤵ ↲
303
+ "icon.input": "in:",
304
+ // pick: out: | alt: ⤴ ↱
305
+ "icon.output": "out:",
306
+ // pick: host | alt: 🖥 💻
307
+ "icon.host": "host",
308
+ // pick: id | alt: 🧭 🧩
309
+ "icon.session": "id",
310
+ // pick: 📦 | alt: 🧰
311
+ "icon.package": "📦",
312
+ // pick: ⚠ | alt: ❗
313
+ "icon.warning": "⚠",
314
+ // pick: ↩ | alt: ↺ ⟲
315
+ "icon.rewind": "↩",
316
+ // pick: ⚡ | alt: ✨ ✦
317
+ "icon.auto": "⚡",
318
+ // pick: SK | alt: 🧠 🎓
319
+ "icon.extensionSkill": "SK",
320
+ // pick: TL | alt: 🛠 ⚙
321
+ "icon.extensionTool": "TL",
322
+ // pick: / | alt: ⌘ ⌥
323
+ "icon.extensionSlashCommand": "/",
324
+ // pick: MCP | alt: 🔌 🧩
325
+ "icon.extensionMcp": "MCP",
326
+ // pick: RL | alt: ⚖ 📏
327
+ "icon.extensionRule": "RL",
328
+ // pick: HK | alt: 🪝 ⚓
329
+ "icon.extensionHook": "HK",
330
+ // pick: PR | alt: 💬 ✎
331
+ "icon.extensionPrompt": "PR",
332
+ // pick: CF | alt: 📄 📎
333
+ "icon.extensionContextFile": "CF",
334
+ // pick: IN | alt: 📘 ℹ
335
+ "icon.extensionInstruction": "IN",
336
+ // Thinking Levels
337
+ // pick: [min] | alt: · ◔ min
338
+ "thinking.minimal": "[min]",
339
+ // pick: [low] | alt: ◑ low ▪ low
340
+ "thinking.low": "[low]",
341
+ // pick: [med] | alt: ◒ med ▪ med
342
+ "thinking.medium": "[med]",
343
+ // pick: [high] | alt: ◕ high ▪ high
344
+ "thinking.high": "[high]",
345
+ // pick: [xhi] | alt: ◉ xhi ▪ xhi
346
+ "thinking.xhigh": "[xhi]",
347
+ // Checkboxes
348
+ // pick: ☑ | alt: ✓ ✔ ✅
349
+ "checkbox.checked": "☑",
350
+ // pick: ☐ | alt: □ ▢
351
+ "checkbox.unchecked": "☐",
352
+ // Text Formatting
353
+ // pick: … | alt: ⋯ ...
354
+ "format.ellipsis": "…",
355
+ // pick: • | alt: · ▪ ◦
356
+ "format.bullet": "•",
357
+ // pick: – | alt: — ― -
358
+ "format.dash": "–",
359
+ // pick: [ | alt: ⟦ ⟨
360
+ "format.bracketLeft": "[",
361
+ // pick: ] | alt: ⟧ ⟩
362
+ "format.bracketRight": "]",
363
+ // Markdown-specific
364
+ // pick: │ | alt: ┃ ║
365
+ "md.quoteBorder": "│",
366
+ // pick: ─ | alt: ━ ═
367
+ "md.hrChar": "─",
368
+ // pick: • | alt: · ▪ ◦
369
+ "md.bullet": "•",
370
+ // Language icons (unicode uses code symbol prefix)
371
+ "lang.default": "❖",
372
+ "lang.typescript": "❖ ts",
373
+ "lang.javascript": "❖ js",
374
+ "lang.python": "❖ py",
375
+ "lang.rust": "❖ rs",
376
+ "lang.go": "❖ go",
377
+ "lang.java": "❖ java",
378
+ "lang.c": "❖ c",
379
+ "lang.cpp": "❖ c++",
380
+ "lang.csharp": "❖ c#",
381
+ "lang.ruby": "❖ rb",
382
+ "lang.php": "❖ php",
383
+ "lang.swift": "❖ swift",
384
+ "lang.kotlin": "❖ kt",
385
+ "lang.shell": "❖ sh",
386
+ "lang.html": "❖ html",
387
+ "lang.css": "❖ css",
388
+ "lang.json": "❖ json",
389
+ "lang.yaml": "❖ yaml",
390
+ "lang.markdown": "❖ md",
391
+ "lang.sql": "❖ sql",
392
+ "lang.docker": "❖ docker",
393
+ "lang.lua": "❖ lua",
394
+ "lang.text": "❖ txt",
395
+ "lang.env": "❖ env",
396
+ "lang.toml": "❖ toml",
397
+ "lang.xml": "❖ xml",
398
+ "lang.ini": "❖ ini",
399
+ "lang.conf": "❖ conf",
400
+ "lang.log": "❖ log",
401
+ "lang.csv": "❖ csv",
402
+ "lang.tsv": "❖ tsv",
403
+ "lang.image": "❖ img",
404
+ "lang.pdf": "❖ pdf",
405
+ "lang.archive": "❖ zip",
406
+ "lang.binary": "❖ bin",
407
+ };
408
+
409
+ const NERD_SYMBOLS: SymbolMap = {
410
+ // Status Indicators
411
+ // pick:  | alt:   
412
+ "status.success": "\uf00c",
413
+ // pick:  | alt:   
414
+ "status.error": "\uf00d",
415
+ // pick:  | alt:  
416
+ "status.warning": "\uf12a",
417
+ // pick:  | alt: 
418
+ "status.info": "\uf129",
419
+ // pick:  | alt:   
420
+ "status.pending": "\uf254",
421
+ // pick:  | alt:  
422
+ "status.disabled": "\uf05e",
423
+ // pick:  | alt:  
424
+ "status.enabled": "\uf111",
425
+ // pick:  | alt:   
426
+ "status.running": "\uf110",
427
+ // pick: ◐ | alt: ◑ ◒ ◓ ◔
428
+ "status.shadowed": "◐",
429
+ // pick:  | alt:  
430
+ "status.aborted": "\uf04d",
431
+ // Navigation
432
+ // pick:  | alt:  
433
+ "nav.cursor": "\uf054",
434
+ // pick:  | alt:  
435
+ "nav.selected": "\uf178",
436
+ // pick:  | alt:  
437
+ "nav.expand": "\uf0da",
438
+ // pick:  | alt:  
439
+ "nav.collapse": "\uf0d7",
440
+ // pick:  | alt:  
441
+ "nav.back": "\uf060",
442
+ // Tree Connectors (same as unicode)
443
+ // pick: ├─ | alt: ├╴ ├╌ ╠═ ┣━
444
+ "tree.branch": "\u251c\u2500",
445
+ // pick: └─ | alt: └╴ └╌ ╚═ ┗━
446
+ "tree.last": "\u2514\u2500",
447
+ // pick: │ | alt: ┃ ║ ▏ ▕
448
+ "tree.vertical": "\u2502",
449
+ // pick: ─ | alt: ━ ═ ╌ ┄
450
+ "tree.horizontal": "\u2500",
451
+ // pick: └ | alt: ╰ ⎿ ↳
452
+ "tree.hook": "\u2514",
453
+ // Box Drawing - Rounded (same as unicode)
454
+ // pick: ╭ | alt: ┌ ┏ ╔
455
+ "boxRound.topLeft": "\u256d",
456
+ // pick: ╮ | alt: ┐ ┓ ╗
457
+ "boxRound.topRight": "\u256e",
458
+ // pick: ╰ | alt: └ ┗ ╚
459
+ "boxRound.bottomLeft": "\u2570",
460
+ // pick: ╯ | alt: ┘ ┛ ╝
461
+ "boxRound.bottomRight": "\u256f",
462
+ // pick: ─ | alt: ━ ═ ╌
463
+ "boxRound.horizontal": "\u2500",
464
+ // pick: │ | alt: ┃ ║ ▏
465
+ "boxRound.vertical": "\u2502",
466
+ // Box Drawing - Sharp (same as unicode)
467
+ // pick: ┌ | alt: ┏ ╭ ╔
468
+ "boxSharp.topLeft": "\u250c",
469
+ // pick: ┐ | alt: ┓ ╮ ╗
470
+ "boxSharp.topRight": "\u2510",
471
+ // pick: └ | alt: ┗ ╰ ╚
472
+ "boxSharp.bottomLeft": "\u2514",
473
+ // pick: ┘ | alt: ┛ ╯ ╝
474
+ "boxSharp.bottomRight": "\u2518",
475
+ // pick: ─ | alt: ━ ═ ╌
476
+ "boxSharp.horizontal": "\u2500",
477
+ // pick: │ | alt: ┃ ║ ▏
478
+ "boxSharp.vertical": "\u2502",
479
+ // pick: ┼ | alt: ╋ ╬ ┿
480
+ "boxSharp.cross": "\u253c",
481
+ // pick: ┬ | alt: ╦ ┯ ┳
482
+ "boxSharp.teeDown": "\u252c",
483
+ // pick: ┴ | alt: ╩ ┷ ┻
484
+ "boxSharp.teeUp": "\u2534",
485
+ // pick: ├ | alt: ╠ ┝ ┣
486
+ "boxSharp.teeRight": "\u251c",
487
+ // pick: ┤ | alt: ╣ ┥ ┫
488
+ "boxSharp.teeLeft": "\u2524",
489
+ // Separators - Nerd Font specific
490
+ // pick:  | alt:   
491
+ "sep.powerline": "\ue0b0",
492
+ // pick:  | alt:  
493
+ "sep.powerlineThin": "\ue0b1",
494
+ // pick:  | alt:  
495
+ "sep.powerlineLeft": "\ue0b0",
496
+ // pick:  | alt:  
497
+ "sep.powerlineRight": "\ue0b2",
498
+ // pick:  | alt: 
499
+ "sep.powerlineThinLeft": "\ue0b1",
500
+ // pick:  | alt: 
501
+ "sep.powerlineThinRight": "\ue0b3",
502
+ // pick: █ | alt: ▓ ▒ ░ ▉ ▌
503
+ "sep.block": "\u2588",
504
+ // pick: space | alt: ␠ ·
505
+ "sep.space": " ",
506
+ // pick: > | alt: › » ▸
507
+ "sep.asciiLeft": ">",
508
+ // pick: < | alt: ‹ « ◂
509
+ "sep.asciiRight": "<",
510
+ // pick: · | alt: • ⋅
511
+ "sep.dot": " \u00b7 ",
512
+ // pick:  | alt: / ∕ ⁄
513
+ "sep.slash": "\ue0bb",
514
+ // pick:  | alt: │ ┃ |
515
+ "sep.pipe": "\ue0b3",
516
+ // Icons - Nerd Font specific
517
+ // pick:  | alt:   ◆
518
+ "icon.model": "\uec19",
519
+ // pick:  | alt:  
520
+ "icon.folder": "\uf115",
521
+ // pick:  | alt:  
522
+ "icon.file": "\uf15b",
523
+ // pick:  | alt:  ⎇
524
+ "icon.git": "\uf1d3",
525
+ // pick:  | alt:  ⎇
526
+ "icon.branch": "\uf126",
527
+ // pick:  | alt: ⊛ ◍ 
528
+ "icon.tokens": "\ue26b",
529
+ // pick:  | alt: ◫ ▦
530
+ "icon.context": "\ue70f",
531
+ // pick:  | alt: $ ¢
532
+ "icon.cost": "\uf155",
533
+ // pick:  | alt: ◷ ◴
534
+ "icon.time": "\uf017",
535
+ // pick:  | alt: π ∏ ∑
536
+ "icon.pi": "\ue22c",
537
+ // pick:  | alt: 
538
+ "icon.agents": "\uf0c0",
539
+ // pick:  | alt:  
540
+ "icon.cache": "\uf1c0",
541
+ // pick:  | alt:  →
542
+ "icon.input": "\uf090",
543
+ // pick:  | alt:  →
544
+ "icon.output": "\uf08b",
545
+ // pick:  | alt:  
546
+ "icon.host": "\uf109",
547
+ // pick:  | alt:  
548
+ "icon.session": "\uf550",
549
+ // pick:  | alt: 
550
+ "icon.package": "\uf487",
551
+ // pick:  | alt:  
552
+ "icon.warning": "\uf071",
553
+ // pick:  | alt:  ↺
554
+ "icon.rewind": "\uf0e2",
555
+ // pick: 󰁨 | alt:   
556
+ "icon.auto": "\u{f0068}",
557
+ // pick:  | alt:  
558
+ "icon.extensionSkill": "\uf0eb",
559
+ // pick:  | alt:  
560
+ "icon.extensionTool": "\uf0ad",
561
+ // pick:  | alt: 
562
+ "icon.extensionSlashCommand": "\uf120",
563
+ // pick:  | alt:  
564
+ "icon.extensionMcp": "\uf1e6",
565
+ // pick:  | alt:  
566
+ "icon.extensionRule": "\uf0e3",
567
+ // pick:  | alt: 
568
+ "icon.extensionHook": "\uf0c1",
569
+ // pick:  | alt:  
570
+ "icon.extensionPrompt": "\uf075",
571
+ // pick:  | alt:  
572
+ "icon.extensionContextFile": "\uf0f6",
573
+ // pick:  | alt:  
574
+ "icon.extensionInstruction": "\uf02d",
575
+ // Thinking Levels - emoji labels
576
+ // pick: 🤨 min | alt:  min  min
577
+ "thinking.minimal": "🤨 min",
578
+ // pick: 🤔 low | alt:  low  low
579
+ "thinking.low": "🤔 low",
580
+ // pick: 🤓 med | alt:  med  med
581
+ "thinking.medium": "🤓 med",
582
+ // pick: 🤯 high | alt:  high  high
583
+ "thinking.high": "🤯 high",
584
+ // pick: 🧠 xhi | alt:  xhi  xhi
585
+ "thinking.xhigh": "🧠 xhi",
586
+ // Checkboxes
587
+ // pick:  | alt:  
588
+ "checkbox.checked": "\uf14a",
589
+ // pick:  | alt: 
590
+ "checkbox.unchecked": "\uf096",
591
+ // Text Formatting
592
+ // pick: … | alt: ⋯ ...
593
+ "format.ellipsis": "\u2026",
594
+ // pick:  | alt:   •
595
+ "format.bullet": "\uf111",
596
+ // pick: – | alt: — ― -
597
+ "format.dash": "\u2013",
598
+ // pick: [ | alt: ⟦ ⟨
599
+ "format.bracketLeft": "[",
600
+ // pick: ] | alt: ⟧ ⟩
601
+ "format.bracketRight": "]",
602
+ // Markdown-specific
603
+ // pick: │ | alt: ┃ ║
604
+ "md.quoteBorder": "\u2502",
605
+ // pick: ─ | alt: ━ ═
606
+ "md.hrChar": "\u2500",
607
+ // pick:  | alt:  •
608
+ "md.bullet": "\uf111",
609
+ // Language icons (nerd font devicons)
610
+ "lang.default": "",
611
+ "lang.typescript": "",
612
+ "lang.javascript": "",
613
+ "lang.python": "",
614
+ "lang.rust": "",
615
+ "lang.go": "",
616
+ "lang.java": "",
617
+ "lang.c": "",
618
+ "lang.cpp": "",
619
+ "lang.csharp": "",
620
+ "lang.ruby": "",
621
+ "lang.php": "",
622
+ "lang.swift": "",
623
+ "lang.kotlin": "",
624
+ "lang.shell": "",
625
+ "lang.html": "",
626
+ "lang.css": "",
627
+ "lang.json": "",
628
+ "lang.yaml": "",
629
+ "lang.markdown": "",
630
+ "lang.sql": "",
631
+ "lang.docker": "",
632
+ "lang.lua": "",
633
+ "lang.text": "",
634
+ "lang.env": "",
635
+ "lang.toml": "",
636
+ "lang.xml": "󰗀",
637
+ "lang.ini": "",
638
+ "lang.conf": "",
639
+ "lang.log": "󰌱",
640
+ "lang.csv": "󰈛",
641
+ "lang.tsv": "󰈛",
642
+ "lang.image": "󰈟",
643
+ "lang.pdf": "󰈦",
644
+ "lang.archive": "",
645
+ "lang.binary": "󰆚",
646
+ };
647
+
648
+ const ASCII_SYMBOLS: SymbolMap = {
649
+ // Status Indicators
650
+ "status.success": "[ok]",
651
+ "status.error": "[!!]",
652
+ "status.warning": "[!]",
653
+ "status.info": "[i]",
654
+ "status.pending": "[*]",
655
+ "status.disabled": "[ ]",
656
+ "status.enabled": "[x]",
657
+ "status.running": "[~]",
658
+ "status.shadowed": "[/]",
659
+ "status.aborted": "[-]",
660
+ // Navigation
661
+ "nav.cursor": ">",
662
+ "nav.selected": "->",
663
+ "nav.expand": "+",
664
+ "nav.collapse": "-",
665
+ "nav.back": "<-",
666
+ // Tree Connectors
667
+ "tree.branch": "|--",
668
+ "tree.last": "'--",
669
+ "tree.vertical": "|",
670
+ "tree.horizontal": "-",
671
+ "tree.hook": "`-",
672
+ // Box Drawing - Rounded (ASCII fallback)
673
+ "boxRound.topLeft": "+",
674
+ "boxRound.topRight": "+",
675
+ "boxRound.bottomLeft": "+",
676
+ "boxRound.bottomRight": "+",
677
+ "boxRound.horizontal": "-",
678
+ "boxRound.vertical": "|",
679
+ // Box Drawing - Sharp (ASCII fallback)
680
+ "boxSharp.topLeft": "+",
681
+ "boxSharp.topRight": "+",
682
+ "boxSharp.bottomLeft": "+",
683
+ "boxSharp.bottomRight": "+",
684
+ "boxSharp.horizontal": "-",
685
+ "boxSharp.vertical": "|",
686
+ "boxSharp.cross": "+",
687
+ "boxSharp.teeDown": "+",
688
+ "boxSharp.teeUp": "+",
689
+ "boxSharp.teeRight": "+",
690
+ "boxSharp.teeLeft": "+",
691
+ // Separators
692
+ "sep.powerline": ">",
693
+ "sep.powerlineThin": ">",
694
+ "sep.powerlineLeft": ">",
695
+ "sep.powerlineRight": "<",
696
+ "sep.powerlineThinLeft": ">",
697
+ "sep.powerlineThinRight": "<",
698
+ "sep.block": "#",
699
+ "sep.space": " ",
700
+ "sep.asciiLeft": ">",
701
+ "sep.asciiRight": "<",
702
+ "sep.dot": " - ",
703
+ "sep.slash": " / ",
704
+ "sep.pipe": " | ",
705
+ // Icons
706
+ "icon.model": "[M]",
707
+ "icon.folder": "[D]",
708
+ "icon.file": "[F]",
709
+ "icon.git": "git:",
710
+ "icon.branch": "@",
711
+ "icon.tokens": "tok:",
712
+ "icon.context": "ctx:",
713
+ "icon.cost": "$",
714
+ "icon.time": "t:",
715
+ "icon.pi": "pi",
716
+ "icon.agents": "AG",
717
+ "icon.cache": "cache",
718
+ "icon.input": "in:",
719
+ "icon.output": "out:",
720
+ "icon.host": "host",
721
+ "icon.session": "id",
722
+ "icon.package": "[P]",
723
+ "icon.warning": "[!]",
724
+ "icon.rewind": "<-",
725
+ "icon.auto": "[A]",
726
+ "icon.extensionSkill": "SK",
727
+ "icon.extensionTool": "TL",
728
+ "icon.extensionSlashCommand": "/",
729
+ "icon.extensionMcp": "MCP",
730
+ "icon.extensionRule": "RL",
731
+ "icon.extensionHook": "HK",
732
+ "icon.extensionPrompt": "PR",
733
+ "icon.extensionContextFile": "CF",
734
+ "icon.extensionInstruction": "IN",
735
+ // Thinking Levels
736
+ "thinking.minimal": "[min]",
737
+ "thinking.low": "[low]",
738
+ "thinking.medium": "[med]",
739
+ "thinking.high": "[high]",
740
+ "thinking.xhigh": "[xhi]",
741
+ // Checkboxes
742
+ "checkbox.checked": "[x]",
743
+ "checkbox.unchecked": "[ ]",
744
+ // Text Formatting
745
+ "format.ellipsis": "...",
746
+ "format.bullet": "*",
747
+ "format.dash": "-",
748
+ "format.bracketLeft": "[",
749
+ "format.bracketRight": "]",
750
+ // Markdown-specific
751
+ "md.quoteBorder": "|",
752
+ "md.hrChar": "-",
753
+ "md.bullet": "*",
754
+ // Language icons (ASCII uses abbreviations)
755
+ "lang.default": "code",
756
+ "lang.typescript": "ts",
757
+ "lang.javascript": "js",
758
+ "lang.python": "py",
759
+ "lang.rust": "rs",
760
+ "lang.go": "go",
761
+ "lang.java": "java",
762
+ "lang.c": "c",
763
+ "lang.cpp": "cpp",
764
+ "lang.csharp": "cs",
765
+ "lang.ruby": "rb",
766
+ "lang.php": "php",
767
+ "lang.swift": "swift",
768
+ "lang.kotlin": "kt",
769
+ "lang.shell": "sh",
770
+ "lang.html": "html",
771
+ "lang.css": "css",
772
+ "lang.json": "json",
773
+ "lang.yaml": "yaml",
774
+ "lang.markdown": "md",
775
+ "lang.sql": "sql",
776
+ "lang.docker": "docker",
777
+ "lang.lua": "lua",
778
+ "lang.text": "txt",
779
+ "lang.env": "env",
780
+ "lang.toml": "toml",
781
+ "lang.xml": "xml",
782
+ "lang.ini": "ini",
783
+ "lang.conf": "conf",
784
+ "lang.log": "log",
785
+ "lang.csv": "csv",
786
+ "lang.tsv": "tsv",
787
+ "lang.image": "img",
788
+ "lang.pdf": "pdf",
789
+ "lang.archive": "zip",
790
+ "lang.binary": "bin",
791
+ };
792
+
793
+ const SYMBOL_PRESETS: Record<SymbolPreset, SymbolMap> = {
794
+ unicode: UNICODE_SYMBOLS,
795
+ nerd: NERD_SYMBOLS,
796
+ ascii: ASCII_SYMBOLS,
797
+ };
798
+
799
+ export type SpinnerType = "status" | "activity";
800
+
801
+ const SPINNER_FRAMES: Record<SymbolPreset, Record<SpinnerType, string[]>> = {
802
+ unicode: {
803
+ status: ["·", "•", "●", "•"],
804
+ activity: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
805
+ },
806
+ nerd: {
807
+ status: ["󰪥", "󰪤", "󰪣", "󰪢", "󰪡", "󰪠", "󰪟", "󰪞", "󰪥"],
808
+ activity: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
809
+ },
810
+ ascii: {
811
+ status: ["|", "/", "-", "\\"],
812
+ activity: ["-", "\\", "|", "/"],
813
+ },
814
+ };
815
+
816
+ // ============================================================================
817
+ // Types & Schema
818
+ // ============================================================================
819
+
820
+ const ColorValueSchema = Type.Union([
821
+ Type.String(), // hex "#ff0000", var ref "primary", or empty ""
822
+ Type.Integer({ minimum: 0, maximum: 255 }), // 256-color index
823
+ ]);
824
+
825
+ type ColorValue = Static<typeof ColorValueSchema>;
826
+
827
+ const SymbolPresetSchema = Type.Union([Type.Literal("unicode"), Type.Literal("nerd"), Type.Literal("ascii")]);
828
+
829
+ const SymbolsSchema = Type.Optional(
830
+ Type.Object({
831
+ preset: Type.Optional(SymbolPresetSchema),
832
+ overrides: Type.Optional(Type.Record(Type.String(), Type.String())),
833
+ }),
834
+ );
835
+
836
+ const ThemeJsonSchema = Type.Object({
837
+ $schema: Type.Optional(Type.String()),
838
+ name: Type.String(),
839
+ vars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),
840
+ colors: Type.Object({
841
+ // Core UI (10 colors)
842
+ accent: ColorValueSchema,
843
+ border: ColorValueSchema,
844
+ borderAccent: ColorValueSchema,
845
+ borderMuted: ColorValueSchema,
846
+ success: ColorValueSchema,
847
+ error: ColorValueSchema,
848
+ warning: ColorValueSchema,
849
+ muted: ColorValueSchema,
850
+ dim: ColorValueSchema,
851
+ text: ColorValueSchema,
852
+ thinkingText: ColorValueSchema,
853
+ // Backgrounds & Content Text (11 colors)
854
+ selectedBg: ColorValueSchema,
855
+ userMessageBg: ColorValueSchema,
856
+ userMessageText: ColorValueSchema,
857
+ customMessageBg: ColorValueSchema,
858
+ customMessageText: ColorValueSchema,
859
+ customMessageLabel: ColorValueSchema,
860
+ toolPendingBg: ColorValueSchema,
861
+ toolSuccessBg: ColorValueSchema,
862
+ toolErrorBg: ColorValueSchema,
863
+ toolTitle: ColorValueSchema,
864
+ toolOutput: ColorValueSchema,
865
+ // Markdown (10 colors)
866
+ mdHeading: ColorValueSchema,
867
+ mdLink: ColorValueSchema,
868
+ mdLinkUrl: ColorValueSchema,
869
+ mdCode: ColorValueSchema,
870
+ mdCodeBlock: ColorValueSchema,
871
+ mdCodeBlockBorder: ColorValueSchema,
872
+ mdQuote: ColorValueSchema,
873
+ mdQuoteBorder: ColorValueSchema,
874
+ mdHr: ColorValueSchema,
875
+ mdListBullet: ColorValueSchema,
876
+ // Tool Diffs (3 colors)
877
+ toolDiffAdded: ColorValueSchema,
878
+ toolDiffRemoved: ColorValueSchema,
879
+ toolDiffContext: ColorValueSchema,
880
+ // Syntax Highlighting (9 colors)
881
+ syntaxComment: ColorValueSchema,
882
+ syntaxKeyword: ColorValueSchema,
883
+ syntaxFunction: ColorValueSchema,
884
+ syntaxVariable: ColorValueSchema,
885
+ syntaxString: ColorValueSchema,
886
+ syntaxNumber: ColorValueSchema,
887
+ syntaxType: ColorValueSchema,
888
+ syntaxOperator: ColorValueSchema,
889
+ syntaxPunctuation: ColorValueSchema,
890
+ // Thinking Level Borders (6 colors)
891
+ thinkingOff: ColorValueSchema,
892
+ thinkingMinimal: ColorValueSchema,
893
+ thinkingLow: ColorValueSchema,
894
+ thinkingMedium: ColorValueSchema,
895
+ thinkingHigh: ColorValueSchema,
896
+ thinkingXhigh: ColorValueSchema,
897
+ // Bash Mode (1 color)
898
+ bashMode: ColorValueSchema,
899
+ // Footer Status Line
900
+ statusLineBg: ColorValueSchema,
901
+ statusLineSep: ColorValueSchema,
902
+ statusLineModel: ColorValueSchema,
903
+ statusLinePath: ColorValueSchema,
904
+ statusLineGitClean: ColorValueSchema,
905
+ statusLineGitDirty: ColorValueSchema,
906
+ statusLineContext: ColorValueSchema,
907
+ statusLineSpend: ColorValueSchema,
908
+ statusLineStaged: ColorValueSchema,
909
+ statusLineDirty: ColorValueSchema,
910
+ statusLineUntracked: ColorValueSchema,
911
+ statusLineOutput: ColorValueSchema,
912
+ statusLineCost: ColorValueSchema,
913
+ statusLineSubagents: ColorValueSchema,
914
+ }),
915
+ export: Type.Optional(
916
+ Type.Object({
917
+ pageBg: Type.Optional(ColorValueSchema),
918
+ cardBg: Type.Optional(ColorValueSchema),
919
+ infoBg: Type.Optional(ColorValueSchema),
920
+ }),
921
+ ),
922
+ symbols: SymbolsSchema,
923
+ });
924
+
925
+ type ThemeJson = Static<typeof ThemeJsonSchema>;
926
+
927
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TypeBox CJS/ESM type mismatch
928
+ const validateThemeJson = TypeCompiler.Compile(ThemeJsonSchema as any);
929
+
930
+ export type ThemeColor =
931
+ | "accent"
932
+ | "border"
933
+ | "borderAccent"
934
+ | "borderMuted"
935
+ | "success"
936
+ | "error"
937
+ | "warning"
938
+ | "muted"
939
+ | "dim"
940
+ | "text"
941
+ | "thinkingText"
942
+ | "userMessageText"
943
+ | "customMessageText"
944
+ | "customMessageLabel"
945
+ | "toolTitle"
946
+ | "toolOutput"
947
+ | "mdHeading"
948
+ | "mdLink"
949
+ | "mdLinkUrl"
950
+ | "mdCode"
951
+ | "mdCodeBlock"
952
+ | "mdCodeBlockBorder"
953
+ | "mdQuote"
954
+ | "mdQuoteBorder"
955
+ | "mdHr"
956
+ | "mdListBullet"
957
+ | "toolDiffAdded"
958
+ | "toolDiffRemoved"
959
+ | "toolDiffContext"
960
+ | "syntaxComment"
961
+ | "syntaxKeyword"
962
+ | "syntaxFunction"
963
+ | "syntaxVariable"
964
+ | "syntaxString"
965
+ | "syntaxNumber"
966
+ | "syntaxType"
967
+ | "syntaxOperator"
968
+ | "syntaxPunctuation"
969
+ | "thinkingOff"
970
+ | "thinkingMinimal"
971
+ | "thinkingLow"
972
+ | "thinkingMedium"
973
+ | "thinkingHigh"
974
+ | "thinkingXhigh"
975
+ | "bashMode"
976
+ | "statusLineSep"
977
+ | "statusLineModel"
978
+ | "statusLinePath"
979
+ | "statusLineGitClean"
980
+ | "statusLineGitDirty"
981
+ | "statusLineContext"
982
+ | "statusLineSpend"
983
+ | "statusLineStaged"
984
+ | "statusLineDirty"
985
+ | "statusLineUntracked"
986
+ | "statusLineOutput"
987
+ | "statusLineCost"
988
+ | "statusLineSubagents";
989
+
990
+ export type ThemeBg =
991
+ | "selectedBg"
992
+ | "userMessageBg"
993
+ | "customMessageBg"
994
+ | "toolPendingBg"
995
+ | "toolSuccessBg"
996
+ | "toolErrorBg"
997
+ | "statusLineBg";
998
+
999
+ type ColorMode = "truecolor" | "256color";
1000
+
1001
+ // ============================================================================
1002
+ // Color Utilities
1003
+ // ============================================================================
1004
+
1005
+ function detectColorMode(): ColorMode {
1006
+ const colorterm = process.env.COLORTERM;
1007
+ if (colorterm === "truecolor" || colorterm === "24bit") {
1008
+ return "truecolor";
1009
+ }
1010
+ // Windows Terminal supports truecolor
1011
+ if (process.env.WT_SESSION) {
1012
+ return "truecolor";
1013
+ }
1014
+ const term = process.env.TERM || "";
1015
+ if (term.includes("256color")) {
1016
+ return "256color";
1017
+ }
1018
+ return "256color";
1019
+ }
1020
+
1021
+ function hexToRgb(hex: string): { r: number; g: number; b: number } {
1022
+ const cleaned = hex.replace("#", "");
1023
+ if (cleaned.length !== 6) {
1024
+ throw new Error(`Invalid hex color: ${hex}`);
1025
+ }
1026
+ const r = parseInt(cleaned.substring(0, 2), 16);
1027
+ const g = parseInt(cleaned.substring(2, 4), 16);
1028
+ const b = parseInt(cleaned.substring(4, 6), 16);
1029
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
1030
+ throw new Error(`Invalid hex color: ${hex}`);
1031
+ }
1032
+ return { r, g, b };
1033
+ }
1034
+
1035
+ // The 6x6x6 color cube channel values (indices 0-5)
1036
+ const CUBE_VALUES = [0, 95, 135, 175, 215, 255];
1037
+
1038
+ // Grayscale ramp values (indices 232-255, 24 grays from 8 to 238)
1039
+ const GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10);
1040
+
1041
+ function findClosestCubeIndex(value: number): number {
1042
+ let minDist = Infinity;
1043
+ let minIdx = 0;
1044
+ for (let i = 0; i < CUBE_VALUES.length; i++) {
1045
+ const dist = Math.abs(value - CUBE_VALUES[i]);
1046
+ if (dist < minDist) {
1047
+ minDist = dist;
1048
+ minIdx = i;
1049
+ }
1050
+ }
1051
+ return minIdx;
1052
+ }
1053
+
1054
+ function findClosestGrayIndex(gray: number): number {
1055
+ let minDist = Infinity;
1056
+ let minIdx = 0;
1057
+ for (let i = 0; i < GRAY_VALUES.length; i++) {
1058
+ const dist = Math.abs(gray - GRAY_VALUES[i]);
1059
+ if (dist < minDist) {
1060
+ minDist = dist;
1061
+ minIdx = i;
1062
+ }
1063
+ }
1064
+ return minIdx;
1065
+ }
1066
+
1067
+ function colorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {
1068
+ // Weighted Euclidean distance (human eye is more sensitive to green)
1069
+ const dr = r1 - r2;
1070
+ const dg = g1 - g2;
1071
+ const db = b1 - b2;
1072
+ return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;
1073
+ }
1074
+
1075
+ function rgbTo256(r: number, g: number, b: number): number {
1076
+ // Find closest color in the 6x6x6 cube
1077
+ const rIdx = findClosestCubeIndex(r);
1078
+ const gIdx = findClosestCubeIndex(g);
1079
+ const bIdx = findClosestCubeIndex(b);
1080
+ const cubeR = CUBE_VALUES[rIdx];
1081
+ const cubeG = CUBE_VALUES[gIdx];
1082
+ const cubeB = CUBE_VALUES[bIdx];
1083
+ const cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx;
1084
+ const cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB);
1085
+
1086
+ // Find closest grayscale
1087
+ const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
1088
+ const grayIdx = findClosestGrayIndex(gray);
1089
+ const grayValue = GRAY_VALUES[grayIdx];
1090
+ const grayIndex = 232 + grayIdx;
1091
+ const grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue);
1092
+
1093
+ // Check if color has noticeable saturation (hue matters)
1094
+ // If max-min spread is significant, prefer cube to preserve tint
1095
+ const maxC = Math.max(r, g, b);
1096
+ const minC = Math.min(r, g, b);
1097
+ const spread = maxC - minC;
1098
+
1099
+ // Only consider grayscale if color is nearly neutral (spread < 10)
1100
+ // AND grayscale is actually closer
1101
+ if (spread < 10 && grayDist < cubeDist) {
1102
+ return grayIndex;
1103
+ }
1104
+
1105
+ return cubeIndex;
1106
+ }
1107
+
1108
+ function hexTo256(hex: string): number {
1109
+ const { r, g, b } = hexToRgb(hex);
1110
+ return rgbTo256(r, g, b);
1111
+ }
1112
+
1113
+ function fgAnsi(color: string | number, mode: ColorMode): string {
1114
+ if (color === "") return "\x1b[39m";
1115
+ if (typeof color === "number") return `\x1b[38;5;${color}m`;
1116
+ if (color.startsWith("#")) {
1117
+ if (mode === "truecolor") {
1118
+ const { r, g, b } = hexToRgb(color);
1119
+ return `\x1b[38;2;${r};${g};${b}m`;
1120
+ } else {
1121
+ const index = hexTo256(color);
1122
+ return `\x1b[38;5;${index}m`;
1123
+ }
1124
+ }
1125
+ throw new Error(`Invalid color value: ${color}`);
1126
+ }
1127
+
1128
+ function bgAnsi(color: string | number, mode: ColorMode): string {
1129
+ if (color === "") return "\x1b[49m";
1130
+ if (typeof color === "number") return `\x1b[48;5;${color}m`;
1131
+ if (color.startsWith("#")) {
1132
+ if (mode === "truecolor") {
1133
+ const { r, g, b } = hexToRgb(color);
1134
+ return `\x1b[48;2;${r};${g};${b}m`;
1135
+ } else {
1136
+ const index = hexTo256(color);
1137
+ return `\x1b[48;5;${index}m`;
1138
+ }
1139
+ }
1140
+ throw new Error(`Invalid color value: ${color}`);
1141
+ }
1142
+
1143
+ function resolveVarRefs(
1144
+ value: ColorValue,
1145
+ vars: Record<string, ColorValue>,
1146
+ visited = new Set<string>(),
1147
+ ): string | number {
1148
+ if (typeof value === "number" || value === "" || value.startsWith("#")) {
1149
+ return value;
1150
+ }
1151
+ if (visited.has(value)) {
1152
+ throw new Error(`Circular variable reference detected: ${value}`);
1153
+ }
1154
+ if (!(value in vars)) {
1155
+ throw new Error(`Variable reference not found: ${value}`);
1156
+ }
1157
+ visited.add(value);
1158
+ return resolveVarRefs(vars[value], vars, visited);
1159
+ }
1160
+
1161
+ function resolveThemeColors<T extends Record<string, ColorValue>>(
1162
+ colors: T,
1163
+ vars: Record<string, ColorValue> = {},
1164
+ ): Record<keyof T, string | number> {
1165
+ const resolved: Record<string, string | number> = {};
1166
+ for (const [key, value] of Object.entries(colors)) {
1167
+ resolved[key] = resolveVarRefs(value, vars);
1168
+ }
1169
+ return resolved as Record<keyof T, string | number>;
1170
+ }
1171
+
1172
+ // ============================================================================
1173
+ // Theme Class
1174
+ // ============================================================================
1175
+
1176
+ const langMap: Record<string, SymbolKey> = {
1177
+ typescript: "lang.typescript",
1178
+ ts: "lang.typescript",
1179
+ tsx: "lang.typescript",
1180
+ javascript: "lang.javascript",
1181
+ js: "lang.javascript",
1182
+ jsx: "lang.javascript",
1183
+ mjs: "lang.javascript",
1184
+ cjs: "lang.javascript",
1185
+ python: "lang.python",
1186
+ py: "lang.python",
1187
+ rust: "lang.rust",
1188
+ rs: "lang.rust",
1189
+ go: "lang.go",
1190
+ java: "lang.java",
1191
+ c: "lang.c",
1192
+ cpp: "lang.cpp",
1193
+ "c++": "lang.cpp",
1194
+ cc: "lang.cpp",
1195
+ cxx: "lang.cpp",
1196
+ csharp: "lang.csharp",
1197
+ cs: "lang.csharp",
1198
+ ruby: "lang.ruby",
1199
+ rb: "lang.ruby",
1200
+ php: "lang.php",
1201
+ swift: "lang.swift",
1202
+ kotlin: "lang.kotlin",
1203
+ kt: "lang.kotlin",
1204
+ bash: "lang.shell",
1205
+ sh: "lang.shell",
1206
+ zsh: "lang.shell",
1207
+ fish: "lang.shell",
1208
+ shell: "lang.shell",
1209
+ html: "lang.html",
1210
+ htm: "lang.html",
1211
+ css: "lang.css",
1212
+ scss: "lang.css",
1213
+ sass: "lang.css",
1214
+ less: "lang.css",
1215
+ json: "lang.json",
1216
+ yaml: "lang.yaml",
1217
+ yml: "lang.yaml",
1218
+ markdown: "lang.markdown",
1219
+ md: "lang.markdown",
1220
+ sql: "lang.sql",
1221
+ dockerfile: "lang.docker",
1222
+ docker: "lang.docker",
1223
+ lua: "lang.lua",
1224
+ text: "lang.text",
1225
+ txt: "lang.text",
1226
+ plain: "lang.text",
1227
+ log: "lang.log",
1228
+ env: "lang.env",
1229
+ dotenv: "lang.env",
1230
+ toml: "lang.toml",
1231
+ xml: "lang.xml",
1232
+ ini: "lang.ini",
1233
+ conf: "lang.conf",
1234
+ cfg: "lang.conf",
1235
+ config: "lang.conf",
1236
+ properties: "lang.conf",
1237
+ csv: "lang.csv",
1238
+ tsv: "lang.tsv",
1239
+ image: "lang.image",
1240
+ img: "lang.image",
1241
+ png: "lang.image",
1242
+ jpg: "lang.image",
1243
+ jpeg: "lang.image",
1244
+ gif: "lang.image",
1245
+ webp: "lang.image",
1246
+ svg: "lang.image",
1247
+ ico: "lang.image",
1248
+ bmp: "lang.image",
1249
+ tiff: "lang.image",
1250
+ pdf: "lang.pdf",
1251
+ zip: "lang.archive",
1252
+ tar: "lang.archive",
1253
+ gz: "lang.archive",
1254
+ tgz: "lang.archive",
1255
+ bz2: "lang.archive",
1256
+ xz: "lang.archive",
1257
+ "7z": "lang.archive",
1258
+ exe: "lang.binary",
1259
+ dll: "lang.binary",
1260
+ so: "lang.binary",
1261
+ dylib: "lang.binary",
1262
+ wasm: "lang.binary",
1263
+ bin: "lang.binary",
1264
+ };
1265
+
1266
+ export class Theme {
1267
+ private fgColors: Map<ThemeColor, string>;
1268
+ private bgColors: Map<ThemeBg, string>;
1269
+ private mode: ColorMode;
1270
+ private symbols: SymbolMap;
1271
+ private symbolPreset: SymbolPreset;
1272
+
1273
+ constructor(
1274
+ fgColors: Record<ThemeColor, string | number>,
1275
+ bgColors: Record<ThemeBg, string | number>,
1276
+ mode: ColorMode,
1277
+ symbolPreset: SymbolPreset = "unicode",
1278
+ symbolOverrides: Record<string, string> = {},
1279
+ ) {
1280
+ this.mode = mode;
1281
+ this.symbolPreset = symbolPreset;
1282
+ this.fgColors = new Map();
1283
+ for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
1284
+ this.fgColors.set(key, fgAnsi(value, mode));
1285
+ }
1286
+ this.bgColors = new Map();
1287
+ for (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {
1288
+ this.bgColors.set(key, bgAnsi(value, mode));
1289
+ }
1290
+ // Build symbol map from preset + overrides
1291
+ const baseSymbols = SYMBOL_PRESETS[symbolPreset];
1292
+ this.symbols = { ...baseSymbols };
1293
+ for (const [key, value] of Object.entries(symbolOverrides)) {
1294
+ if (key in this.symbols) {
1295
+ this.symbols[key as SymbolKey] = value;
1296
+ } else {
1297
+ logger.debug("Invalid symbol key in override", { key, availableKeys: Object.keys(this.symbols) });
1298
+ }
1299
+ }
1300
+ }
1301
+
1302
+ fg(color: ThemeColor, text: string): string {
1303
+ const ansi = this.fgColors.get(color);
1304
+ if (!ansi) throw new Error(`Unknown theme color: ${color}`);
1305
+ return `${ansi}${text}\x1b[39m`; // Reset only foreground color
1306
+ }
1307
+
1308
+ bg(color: ThemeBg, text: string): string {
1309
+ const ansi = this.bgColors.get(color);
1310
+ if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
1311
+ return `${ansi}${text}\x1b[49m`; // Reset only background color
1312
+ }
1313
+
1314
+ bold(text: string): string {
1315
+ return chalk.bold(text);
1316
+ }
1317
+
1318
+ italic(text: string): string {
1319
+ return chalk.italic(text);
1320
+ }
1321
+
1322
+ underline(text: string): string {
1323
+ return chalk.underline(text);
1324
+ }
1325
+
1326
+ inverse(text: string): string {
1327
+ return chalk.inverse(text);
1328
+ }
1329
+
1330
+ getFgAnsi(color: ThemeColor): string {
1331
+ const ansi = this.fgColors.get(color);
1332
+ if (!ansi) throw new Error(`Unknown theme color: ${color}`);
1333
+ return ansi;
1334
+ }
1335
+
1336
+ getBgAnsi(color: ThemeBg): string {
1337
+ const ansi = this.bgColors.get(color);
1338
+ if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
1339
+ return ansi;
1340
+ }
1341
+
1342
+ getColorMode(): ColorMode {
1343
+ return this.mode;
1344
+ }
1345
+
1346
+ getThinkingBorderColor(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): (str: string) => string {
1347
+ // Map thinking levels to dedicated theme colors
1348
+ switch (level) {
1349
+ case "off":
1350
+ return (str: string) => this.fg("thinkingOff", str);
1351
+ case "minimal":
1352
+ return (str: string) => this.fg("thinkingMinimal", str);
1353
+ case "low":
1354
+ return (str: string) => this.fg("thinkingLow", str);
1355
+ case "medium":
1356
+ return (str: string) => this.fg("thinkingMedium", str);
1357
+ case "high":
1358
+ return (str: string) => this.fg("thinkingHigh", str);
1359
+ case "xhigh":
1360
+ return (str: string) => this.fg("thinkingXhigh", str);
1361
+ default:
1362
+ return (str: string) => this.fg("thinkingOff", str);
1363
+ }
1364
+ }
1365
+
1366
+ getBashModeBorderColor(): (str: string) => string {
1367
+ return (str: string) => this.fg("bashMode", str);
1368
+ }
1369
+
1370
+ // -------------------------------------------------------------------------
1371
+ // Symbol Methods
1372
+ // -------------------------------------------------------------------------
1373
+
1374
+ /**
1375
+ * Get a symbol by key.
1376
+ */
1377
+ symbol(key: SymbolKey): string {
1378
+ return this.symbols[key];
1379
+ }
1380
+
1381
+ /**
1382
+ * Get a symbol styled with a color.
1383
+ */
1384
+ styledSymbol(key: SymbolKey, color: ThemeColor): string {
1385
+ return this.fg(color, this.symbols[key]);
1386
+ }
1387
+
1388
+ /**
1389
+ * Get the current symbol preset.
1390
+ */
1391
+ getSymbolPreset(): SymbolPreset {
1392
+ return this.symbolPreset;
1393
+ }
1394
+
1395
+ // -------------------------------------------------------------------------
1396
+ // Symbol Category Accessors
1397
+ // -------------------------------------------------------------------------
1398
+
1399
+ get status() {
1400
+ return {
1401
+ success: this.symbols["status.success"],
1402
+ error: this.symbols["status.error"],
1403
+ warning: this.symbols["status.warning"],
1404
+ info: this.symbols["status.info"],
1405
+ pending: this.symbols["status.pending"],
1406
+ disabled: this.symbols["status.disabled"],
1407
+ enabled: this.symbols["status.enabled"],
1408
+ running: this.symbols["status.running"],
1409
+ shadowed: this.symbols["status.shadowed"],
1410
+ aborted: this.symbols["status.aborted"],
1411
+ };
1412
+ }
1413
+
1414
+ get nav() {
1415
+ return {
1416
+ cursor: this.symbols["nav.cursor"],
1417
+ selected: this.symbols["nav.selected"],
1418
+ expand: this.symbols["nav.expand"],
1419
+ collapse: this.symbols["nav.collapse"],
1420
+ back: this.symbols["nav.back"],
1421
+ };
1422
+ }
1423
+
1424
+ get tree() {
1425
+ return {
1426
+ branch: this.symbols["tree.branch"],
1427
+ last: this.symbols["tree.last"],
1428
+ vertical: this.symbols["tree.vertical"],
1429
+ horizontal: this.symbols["tree.horizontal"],
1430
+ hook: this.symbols["tree.hook"],
1431
+ };
1432
+ }
1433
+
1434
+ get boxRound() {
1435
+ return {
1436
+ topLeft: this.symbols["boxRound.topLeft"],
1437
+ topRight: this.symbols["boxRound.topRight"],
1438
+ bottomLeft: this.symbols["boxRound.bottomLeft"],
1439
+ bottomRight: this.symbols["boxRound.bottomRight"],
1440
+ horizontal: this.symbols["boxRound.horizontal"],
1441
+ vertical: this.symbols["boxRound.vertical"],
1442
+ };
1443
+ }
1444
+
1445
+ get boxSharp() {
1446
+ return {
1447
+ topLeft: this.symbols["boxSharp.topLeft"],
1448
+ topRight: this.symbols["boxSharp.topRight"],
1449
+ bottomLeft: this.symbols["boxSharp.bottomLeft"],
1450
+ bottomRight: this.symbols["boxSharp.bottomRight"],
1451
+ horizontal: this.symbols["boxSharp.horizontal"],
1452
+ vertical: this.symbols["boxSharp.vertical"],
1453
+ cross: this.symbols["boxSharp.cross"],
1454
+ teeDown: this.symbols["boxSharp.teeDown"],
1455
+ teeUp: this.symbols["boxSharp.teeUp"],
1456
+ teeRight: this.symbols["boxSharp.teeRight"],
1457
+ teeLeft: this.symbols["boxSharp.teeLeft"],
1458
+ };
1459
+ }
1460
+
1461
+ get sep() {
1462
+ return {
1463
+ powerline: this.symbols["sep.powerline"],
1464
+ powerlineThin: this.symbols["sep.powerlineThin"],
1465
+ powerlineLeft: this.symbols["sep.powerlineLeft"],
1466
+ powerlineRight: this.symbols["sep.powerlineRight"],
1467
+ powerlineThinLeft: this.symbols["sep.powerlineThinLeft"],
1468
+ powerlineThinRight: this.symbols["sep.powerlineThinRight"],
1469
+ block: this.symbols["sep.block"],
1470
+ space: this.symbols["sep.space"],
1471
+ asciiLeft: this.symbols["sep.asciiLeft"],
1472
+ asciiRight: this.symbols["sep.asciiRight"],
1473
+ dot: this.symbols["sep.dot"],
1474
+ slash: this.symbols["sep.slash"],
1475
+ pipe: this.symbols["sep.pipe"],
1476
+ };
1477
+ }
1478
+
1479
+ get icon() {
1480
+ return {
1481
+ model: this.symbols["icon.model"],
1482
+ folder: this.symbols["icon.folder"],
1483
+ file: this.symbols["icon.file"],
1484
+ git: this.symbols["icon.git"],
1485
+ branch: this.symbols["icon.branch"],
1486
+ tokens: this.symbols["icon.tokens"],
1487
+ context: this.symbols["icon.context"],
1488
+ cost: this.symbols["icon.cost"],
1489
+ time: this.symbols["icon.time"],
1490
+ pi: this.symbols["icon.pi"],
1491
+ agents: this.symbols["icon.agents"],
1492
+ cache: this.symbols["icon.cache"],
1493
+ input: this.symbols["icon.input"],
1494
+ output: this.symbols["icon.output"],
1495
+ host: this.symbols["icon.host"],
1496
+ session: this.symbols["icon.session"],
1497
+ package: this.symbols["icon.package"],
1498
+ warning: this.symbols["icon.warning"],
1499
+ rewind: this.symbols["icon.rewind"],
1500
+ auto: this.symbols["icon.auto"],
1501
+ extensionSkill: this.symbols["icon.extensionSkill"],
1502
+ extensionTool: this.symbols["icon.extensionTool"],
1503
+ extensionSlashCommand: this.symbols["icon.extensionSlashCommand"],
1504
+ extensionMcp: this.symbols["icon.extensionMcp"],
1505
+ extensionRule: this.symbols["icon.extensionRule"],
1506
+ extensionHook: this.symbols["icon.extensionHook"],
1507
+ extensionPrompt: this.symbols["icon.extensionPrompt"],
1508
+ extensionContextFile: this.symbols["icon.extensionContextFile"],
1509
+ extensionInstruction: this.symbols["icon.extensionInstruction"],
1510
+ };
1511
+ }
1512
+
1513
+ get thinking() {
1514
+ return {
1515
+ minimal: this.symbols["thinking.minimal"],
1516
+ low: this.symbols["thinking.low"],
1517
+ medium: this.symbols["thinking.medium"],
1518
+ high: this.symbols["thinking.high"],
1519
+ xhigh: this.symbols["thinking.xhigh"],
1520
+ };
1521
+ }
1522
+
1523
+ get checkbox() {
1524
+ return {
1525
+ checked: this.symbols["checkbox.checked"],
1526
+ unchecked: this.symbols["checkbox.unchecked"],
1527
+ };
1528
+ }
1529
+
1530
+ get format() {
1531
+ return {
1532
+ ellipsis: this.symbols["format.ellipsis"],
1533
+ bullet: this.symbols["format.bullet"],
1534
+ dash: this.symbols["format.dash"],
1535
+ bracketLeft: this.symbols["format.bracketLeft"],
1536
+ bracketRight: this.symbols["format.bracketRight"],
1537
+ };
1538
+ }
1539
+
1540
+ get md() {
1541
+ return {
1542
+ quoteBorder: this.symbols["md.quoteBorder"],
1543
+ hrChar: this.symbols["md.hrChar"],
1544
+ bullet: this.symbols["md.bullet"],
1545
+ };
1546
+ }
1547
+
1548
+ /**
1549
+ * Default spinner frames (status spinner).
1550
+ */
1551
+ get spinnerFrames(): string[] {
1552
+ return this.getSpinnerFrames();
1553
+ }
1554
+
1555
+ /**
1556
+ * Get spinner frames by type.
1557
+ */
1558
+ getSpinnerFrames(type: SpinnerType = "status"): string[] {
1559
+ return SPINNER_FRAMES[this.symbolPreset][type];
1560
+ }
1561
+
1562
+ /**
1563
+ * Get language icon for a language name.
1564
+ * Maps common language names to their corresponding symbol keys.
1565
+ */
1566
+ getLangIcon(lang: string | undefined): string {
1567
+ if (!lang) return this.symbols["lang.default"];
1568
+ const normalized = lang.toLowerCase();
1569
+ const key = langMap[normalized];
1570
+ return key ? this.symbols[key] : this.symbols["lang.default"];
1571
+ }
1572
+ }
1573
+
1574
+ // ============================================================================
1575
+ // Theme Loading
1576
+ // ============================================================================
1577
+
1578
+ const BUILTIN_THEMES: Record<string, ThemeJson> = {
1579
+ dark: darkThemeJson as ThemeJson,
1580
+ light: lightThemeJson as ThemeJson,
1581
+ ...(defaultThemes as Record<string, ThemeJson>),
1582
+ };
1583
+
1584
+ function getBuiltinThemes(): Record<string, ThemeJson> {
1585
+ return BUILTIN_THEMES;
1586
+ }
1587
+
1588
+ export function getAvailableThemes(): string[] {
1589
+ const themes = new Set<string>(Object.keys(getBuiltinThemes()));
1590
+ const customThemesDir = getCustomThemesDir();
1591
+ if (fs.existsSync(customThemesDir)) {
1592
+ const files = fs.readdirSync(customThemesDir);
1593
+ for (const file of files) {
1594
+ if (file.endsWith(".json")) {
1595
+ themes.add(file.slice(0, -5));
1596
+ }
1597
+ }
1598
+ }
1599
+ return Array.from(themes).sort();
1600
+ }
1601
+
1602
+ function loadThemeJson(name: string): ThemeJson {
1603
+ const builtinThemes = getBuiltinThemes();
1604
+ if (name in builtinThemes) {
1605
+ return builtinThemes[name];
1606
+ }
1607
+ const customThemesDir = getCustomThemesDir();
1608
+ const themePath = path.join(customThemesDir, `${name}.json`);
1609
+ if (!fs.existsSync(themePath)) {
1610
+ throw new Error(`Theme not found: ${name}`);
1611
+ }
1612
+ const content = fs.readFileSync(themePath, "utf-8");
1613
+ let json: unknown;
1614
+ try {
1615
+ json = JSON.parse(content);
1616
+ } catch (error) {
1617
+ throw new Error(`Failed to parse theme ${name}: ${error}`);
1618
+ }
1619
+ if (!validateThemeJson.Check(json)) {
1620
+ const errors = Array.from(validateThemeJson.Errors(json));
1621
+ const missingColors: string[] = [];
1622
+ const otherErrors: string[] = [];
1623
+
1624
+ for (const e of errors) {
1625
+ // Check for missing required color properties
1626
+ const match = e.path.match(/^\/colors\/(\w+)$/);
1627
+ if (match && e.message.includes("Required")) {
1628
+ missingColors.push(match[1]);
1629
+ } else {
1630
+ otherErrors.push(` - ${e.path}: ${e.message}`);
1631
+ }
1632
+ }
1633
+
1634
+ let errorMessage = `Invalid theme "${name}":\n`;
1635
+ if (missingColors.length > 0) {
1636
+ errorMessage += `\nMissing required color tokens:\n`;
1637
+ errorMessage += missingColors.map((c) => ` - ${c}`).join("\n");
1638
+ errorMessage += `\n\nPlease add these colors to your theme's "colors" object.`;
1639
+ errorMessage += `\nSee the built-in themes (dark.json, light.json) for reference values.`;
1640
+ }
1641
+ if (otherErrors.length > 0) {
1642
+ errorMessage += `\n\nOther errors:\n${otherErrors.join("\n")}`;
1643
+ }
1644
+
1645
+ throw new Error(errorMessage);
1646
+ }
1647
+ return json as ThemeJson;
1648
+ }
1649
+
1650
+ function createTheme(themeJson: ThemeJson, mode?: ColorMode, symbolPresetOverride?: SymbolPreset): Theme {
1651
+ const colorMode = mode ?? detectColorMode();
1652
+ const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);
1653
+ const fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>;
1654
+ const bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;
1655
+ const bgColorKeys: Set<string> = new Set([
1656
+ "selectedBg",
1657
+ "userMessageBg",
1658
+ "customMessageBg",
1659
+ "toolPendingBg",
1660
+ "toolSuccessBg",
1661
+ "toolErrorBg",
1662
+ "statusLineBg",
1663
+ ]);
1664
+ for (const [key, value] of Object.entries(resolvedColors)) {
1665
+ if (bgColorKeys.has(key)) {
1666
+ bgColors[key as ThemeBg] = value;
1667
+ } else {
1668
+ fgColors[key as ThemeColor] = value;
1669
+ }
1670
+ }
1671
+ // Extract symbol configuration - settings override takes precedence over theme
1672
+ const symbolPreset: SymbolPreset = symbolPresetOverride ?? themeJson.symbols?.preset ?? "unicode";
1673
+ const symbolOverrides = themeJson.symbols?.overrides ?? {};
1674
+ return new Theme(fgColors, bgColors, colorMode, symbolPreset, symbolOverrides);
1675
+ }
1676
+
1677
+ function loadTheme(name: string, mode?: ColorMode, symbolPresetOverride?: SymbolPreset): Theme {
1678
+ const themeJson = loadThemeJson(name);
1679
+ return createTheme(themeJson, mode, symbolPresetOverride);
1680
+ }
1681
+
1682
+ function detectTerminalBackground(): "dark" | "light" {
1683
+ const colorfgbg = process.env.COLORFGBG || "";
1684
+ if (colorfgbg) {
1685
+ const parts = colorfgbg.split(";");
1686
+ if (parts.length >= 2) {
1687
+ const bg = parseInt(parts[1], 10);
1688
+ if (!Number.isNaN(bg)) {
1689
+ const result = bg < 8 ? "dark" : "light";
1690
+ return result;
1691
+ }
1692
+ }
1693
+ }
1694
+ return "dark";
1695
+ }
1696
+
1697
+ function getDefaultTheme(): string {
1698
+ return detectTerminalBackground();
1699
+ }
1700
+
1701
+ // ============================================================================
1702
+ // Global Theme Instance
1703
+ // ============================================================================
1704
+
1705
+ export let theme: Theme;
1706
+ let currentThemeName: string | undefined;
1707
+ let currentSymbolPresetOverride: SymbolPreset | undefined;
1708
+ let themeWatcher: fs.FSWatcher | undefined;
1709
+ let onThemeChangeCallback: (() => void) | undefined;
1710
+
1711
+ export function initTheme(themeName?: string, enableWatcher: boolean = false, symbolPreset?: SymbolPreset): void {
1712
+ const name = themeName ?? getDefaultTheme();
1713
+ currentThemeName = name;
1714
+ currentSymbolPresetOverride = symbolPreset;
1715
+ try {
1716
+ theme = loadTheme(name, undefined, symbolPreset);
1717
+ if (enableWatcher) {
1718
+ startThemeWatcher();
1719
+ }
1720
+ } catch (err) {
1721
+ logger.debug("Theme loading failed, falling back to dark theme", { error: String(err) });
1722
+ currentThemeName = "dark";
1723
+ theme = loadTheme("dark", undefined, symbolPreset);
1724
+ // Don't start watcher for fallback theme
1725
+ }
1726
+ }
1727
+
1728
+ export function setTheme(name: string, enableWatcher: boolean = false): { success: boolean; error?: string } {
1729
+ currentThemeName = name;
1730
+ try {
1731
+ theme = loadTheme(name, undefined, currentSymbolPresetOverride);
1732
+ if (enableWatcher) {
1733
+ startThemeWatcher();
1734
+ }
1735
+ if (onThemeChangeCallback) {
1736
+ onThemeChangeCallback();
1737
+ }
1738
+ return { success: true };
1739
+ } catch (error) {
1740
+ // Theme is invalid - fall back to dark theme
1741
+ currentThemeName = "dark";
1742
+ theme = loadTheme("dark", undefined, currentSymbolPresetOverride);
1743
+ // Don't start watcher for fallback theme
1744
+ return {
1745
+ success: false,
1746
+ error: error instanceof Error ? error.message : String(error),
1747
+ };
1748
+ }
1749
+ }
1750
+
1751
+ /**
1752
+ * Set the symbol preset override, recreating the theme with the new preset.
1753
+ */
1754
+ export function setSymbolPreset(preset: SymbolPreset): void {
1755
+ currentSymbolPresetOverride = preset;
1756
+ if (currentThemeName) {
1757
+ try {
1758
+ theme = loadTheme(currentThemeName, undefined, preset);
1759
+ } catch {
1760
+ // Fall back to dark theme with new preset
1761
+ theme = loadTheme("dark", undefined, preset);
1762
+ }
1763
+ if (onThemeChangeCallback) {
1764
+ onThemeChangeCallback();
1765
+ }
1766
+ }
1767
+ }
1768
+
1769
+ /**
1770
+ * Get the current symbol preset override.
1771
+ */
1772
+ export function getSymbolPresetOverride(): SymbolPreset | undefined {
1773
+ return currentSymbolPresetOverride;
1774
+ }
1775
+
1776
+ export function onThemeChange(callback: () => void): void {
1777
+ onThemeChangeCallback = callback;
1778
+ }
1779
+
1780
+ /**
1781
+ * Get available symbol presets.
1782
+ */
1783
+ export function getAvailableSymbolPresets(): SymbolPreset[] {
1784
+ return ["unicode", "nerd", "ascii"];
1785
+ }
1786
+
1787
+ /**
1788
+ * Check if a string is a valid symbol preset.
1789
+ */
1790
+ export function isValidSymbolPreset(preset: string): preset is SymbolPreset {
1791
+ return preset === "unicode" || preset === "nerd" || preset === "ascii";
1792
+ }
1793
+
1794
+ function startThemeWatcher(): void {
1795
+ // Stop existing watcher if any
1796
+ if (themeWatcher) {
1797
+ themeWatcher.close();
1798
+ themeWatcher = undefined;
1799
+ }
1800
+
1801
+ // Only watch if it's a custom theme (not built-in)
1802
+ if (!currentThemeName || currentThemeName === "dark" || currentThemeName === "light") {
1803
+ return;
1804
+ }
1805
+
1806
+ const customThemesDir = getCustomThemesDir();
1807
+ const themeFile = path.join(customThemesDir, `${currentThemeName}.json`);
1808
+
1809
+ // Only watch if the file exists
1810
+ if (!fs.existsSync(themeFile)) {
1811
+ return;
1812
+ }
1813
+
1814
+ try {
1815
+ themeWatcher = fs.watch(themeFile, (eventType) => {
1816
+ if (eventType === "change") {
1817
+ // Debounce rapid changes
1818
+ setTimeout(() => {
1819
+ try {
1820
+ // Reload the theme with current symbol preset override
1821
+ theme = loadTheme(currentThemeName!, undefined, currentSymbolPresetOverride);
1822
+ // Notify callback (to invalidate UI)
1823
+ if (onThemeChangeCallback) {
1824
+ onThemeChangeCallback();
1825
+ }
1826
+ } catch (err) {
1827
+ logger.debug("Theme reload error during file change", { error: String(err) });
1828
+ }
1829
+ }, 100);
1830
+ } else if (eventType === "rename") {
1831
+ // File was deleted or renamed - fall back to default theme
1832
+ setTimeout(() => {
1833
+ if (!fs.existsSync(themeFile)) {
1834
+ currentThemeName = "dark";
1835
+ theme = loadTheme("dark");
1836
+ if (themeWatcher) {
1837
+ themeWatcher.close();
1838
+ themeWatcher = undefined;
1839
+ }
1840
+ if (onThemeChangeCallback) {
1841
+ onThemeChangeCallback();
1842
+ }
1843
+ }
1844
+ }, 100);
1845
+ }
1846
+ });
1847
+ } catch (err) {
1848
+ logger.debug("Failed to start theme watcher", { error: String(err) });
1849
+ }
1850
+ }
1851
+
1852
+ export function stopThemeWatcher(): void {
1853
+ if (themeWatcher) {
1854
+ themeWatcher.close();
1855
+ themeWatcher = undefined;
1856
+ }
1857
+ }
1858
+
1859
+ // ============================================================================
1860
+ // HTML Export Helpers
1861
+ // ============================================================================
1862
+
1863
+ /**
1864
+ * Convert a 256-color index to hex string.
1865
+ * Indices 0-15: basic colors (approximate)
1866
+ * Indices 16-231: 6x6x6 color cube
1867
+ * Indices 232-255: grayscale ramp
1868
+ */
1869
+ function ansi256ToHex(index: number): string {
1870
+ // Basic colors (0-15) - approximate common terminal values
1871
+ const basicColors = [
1872
+ "#000000",
1873
+ "#800000",
1874
+ "#008000",
1875
+ "#808000",
1876
+ "#000080",
1877
+ "#800080",
1878
+ "#008080",
1879
+ "#c0c0c0",
1880
+ "#808080",
1881
+ "#ff0000",
1882
+ "#00ff00",
1883
+ "#ffff00",
1884
+ "#0000ff",
1885
+ "#ff00ff",
1886
+ "#00ffff",
1887
+ "#ffffff",
1888
+ ];
1889
+ if (index < 16) {
1890
+ return basicColors[index];
1891
+ }
1892
+
1893
+ // Color cube (16-231): 6x6x6 = 216 colors
1894
+ if (index < 232) {
1895
+ const cubeIndex = index - 16;
1896
+ const r = Math.floor(cubeIndex / 36);
1897
+ const g = Math.floor((cubeIndex % 36) / 6);
1898
+ const b = cubeIndex % 6;
1899
+ const toHex = (n: number) => (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, "0");
1900
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
1901
+ }
1902
+
1903
+ // Grayscale (232-255): 24 shades
1904
+ const gray = 8 + (index - 232) * 10;
1905
+ const grayHex = gray.toString(16).padStart(2, "0");
1906
+ return `#${grayHex}${grayHex}${grayHex}`;
1907
+ }
1908
+
1909
+ /**
1910
+ * Get resolved theme colors as CSS-compatible hex strings.
1911
+ * Used by HTML export to generate CSS custom properties.
1912
+ */
1913
+ export function getResolvedThemeColors(themeName?: string): Record<string, string> {
1914
+ const name = themeName ?? getDefaultTheme();
1915
+ const isLight = name === "light";
1916
+ const themeJson = loadThemeJson(name);
1917
+ const resolved = resolveThemeColors(themeJson.colors, themeJson.vars);
1918
+
1919
+ // Default text color for empty values (terminal uses default fg color)
1920
+ const defaultText = isLight ? "#000000" : "#e5e5e7";
1921
+
1922
+ const cssColors: Record<string, string> = {};
1923
+ for (const [key, value] of Object.entries(resolved)) {
1924
+ if (typeof value === "number") {
1925
+ cssColors[key] = ansi256ToHex(value);
1926
+ } else if (value === "") {
1927
+ // Empty means default terminal color - use sensible fallback for HTML
1928
+ cssColors[key] = defaultText;
1929
+ } else {
1930
+ cssColors[key] = value;
1931
+ }
1932
+ }
1933
+ return cssColors;
1934
+ }
1935
+
1936
+ /**
1937
+ * Check if a theme is a "light" theme (for CSS that needs light/dark variants).
1938
+ */
1939
+ export function isLightTheme(themeName?: string): boolean {
1940
+ // Currently just check the name - could be extended to analyze colors
1941
+ return themeName === "light";
1942
+ }
1943
+
1944
+ /**
1945
+ * Get explicit export colors from theme JSON, if specified.
1946
+ * Returns undefined for each color that isn't explicitly set.
1947
+ */
1948
+ export function getThemeExportColors(themeName?: string): {
1949
+ pageBg?: string;
1950
+ cardBg?: string;
1951
+ infoBg?: string;
1952
+ } {
1953
+ const name = themeName ?? getDefaultTheme();
1954
+ try {
1955
+ const themeJson = loadThemeJson(name);
1956
+ const exportSection = themeJson.export;
1957
+ if (!exportSection) return {};
1958
+
1959
+ const vars = themeJson.vars ?? {};
1960
+ const resolve = (value: string | number | undefined): string | undefined => {
1961
+ if (value === undefined) return undefined;
1962
+ if (typeof value === "number") return ansi256ToHex(value);
1963
+ if (value.startsWith("$")) {
1964
+ const resolved = vars[value];
1965
+ if (resolved === undefined) return undefined;
1966
+ if (typeof resolved === "number") return ansi256ToHex(resolved);
1967
+ return resolved;
1968
+ }
1969
+ return value;
1970
+ };
1971
+
1972
+ return {
1973
+ pageBg: resolve(exportSection.pageBg),
1974
+ cardBg: resolve(exportSection.cardBg),
1975
+ infoBg: resolve(exportSection.infoBg),
1976
+ };
1977
+ } catch {
1978
+ return {};
1979
+ }
1980
+ }
1981
+
1982
+ // ============================================================================
1983
+ // TUI Helpers
1984
+ // ============================================================================
1985
+
1986
+ type CliHighlightTheme = Record<string, (s: string) => string>;
1987
+
1988
+ let cachedHighlightThemeFor: Theme | undefined;
1989
+ let cachedCliHighlightTheme: CliHighlightTheme | undefined;
1990
+
1991
+ function buildCliHighlightTheme(t: Theme): CliHighlightTheme {
1992
+ return {
1993
+ keyword: (s: string) => t.fg("syntaxKeyword", s),
1994
+ built_in: (s: string) => t.fg("syntaxType", s),
1995
+ literal: (s: string) => t.fg("syntaxNumber", s),
1996
+ number: (s: string) => t.fg("syntaxNumber", s),
1997
+ string: (s: string) => t.fg("syntaxString", s),
1998
+ comment: (s: string) => t.fg("syntaxComment", s),
1999
+ function: (s: string) => t.fg("syntaxFunction", s),
2000
+ title: (s: string) => t.fg("syntaxFunction", s),
2001
+ class: (s: string) => t.fg("syntaxType", s),
2002
+ type: (s: string) => t.fg("syntaxType", s),
2003
+ attr: (s: string) => t.fg("syntaxVariable", s),
2004
+ variable: (s: string) => t.fg("syntaxVariable", s),
2005
+ params: (s: string) => t.fg("syntaxVariable", s),
2006
+ operator: (s: string) => t.fg("syntaxOperator", s),
2007
+ punctuation: (s: string) => t.fg("syntaxPunctuation", s),
2008
+ };
2009
+ }
2010
+
2011
+ function getCliHighlightTheme(t: Theme): CliHighlightTheme {
2012
+ if (cachedHighlightThemeFor !== t || !cachedCliHighlightTheme) {
2013
+ cachedHighlightThemeFor = t;
2014
+ cachedCliHighlightTheme = buildCliHighlightTheme(t);
2015
+ }
2016
+ return cachedCliHighlightTheme;
2017
+ }
2018
+
2019
+ /**
2020
+ * Highlight code with syntax coloring based on file extension or language.
2021
+ * Returns array of highlighted lines.
2022
+ */
2023
+ export function highlightCode(code: string, lang?: string): string[] {
2024
+ // Validate language before highlighting to avoid stderr spam from cli-highlight
2025
+ const validLang = lang && supportsLanguage(lang) ? lang : undefined;
2026
+ const opts = {
2027
+ language: validLang,
2028
+ ignoreIllegals: true,
2029
+ theme: getCliHighlightTheme(theme),
2030
+ };
2031
+ try {
2032
+ return highlight(code, opts).split("\n");
2033
+ } catch {
2034
+ return code.split("\n");
2035
+ }
2036
+ }
2037
+
2038
+ /**
2039
+ * Get language identifier from file path extension.
2040
+ */
2041
+ export function getLanguageFromPath(filePath: string): string | undefined {
2042
+ const baseName = path.basename(filePath).toLowerCase();
2043
+ if (baseName === ".env" || baseName.startsWith(".env.")) return "env";
2044
+ if (
2045
+ baseName === ".gitignore" ||
2046
+ baseName === ".gitattributes" ||
2047
+ baseName === ".gitmodules" ||
2048
+ baseName === ".editorconfig" ||
2049
+ baseName === ".npmrc" ||
2050
+ baseName === ".prettierrc" ||
2051
+ baseName === ".eslintrc"
2052
+ ) {
2053
+ return "conf";
2054
+ }
2055
+
2056
+ const ext = filePath.split(".").pop()?.toLowerCase();
2057
+ if (!ext) return undefined;
2058
+
2059
+ const extToLang: Record<string, string> = {
2060
+ ts: "typescript",
2061
+ tsx: "typescript",
2062
+ js: "javascript",
2063
+ jsx: "javascript",
2064
+ mjs: "javascript",
2065
+ cjs: "javascript",
2066
+ py: "python",
2067
+ rb: "ruby",
2068
+ rs: "rust",
2069
+ go: "go",
2070
+ java: "java",
2071
+ kt: "kotlin",
2072
+ swift: "swift",
2073
+ c: "c",
2074
+ h: "c",
2075
+ cpp: "cpp",
2076
+ cc: "cpp",
2077
+ cxx: "cpp",
2078
+ hpp: "cpp",
2079
+ cs: "csharp",
2080
+ php: "php",
2081
+ sh: "bash",
2082
+ bash: "bash",
2083
+ zsh: "bash",
2084
+ fish: "fish",
2085
+ ps1: "powershell",
2086
+ sql: "sql",
2087
+ html: "html",
2088
+ htm: "html",
2089
+ css: "css",
2090
+ scss: "scss",
2091
+ sass: "sass",
2092
+ less: "less",
2093
+ json: "json",
2094
+ yaml: "yaml",
2095
+ yml: "yaml",
2096
+ toml: "toml",
2097
+ xml: "xml",
2098
+ md: "markdown",
2099
+ markdown: "markdown",
2100
+ dockerfile: "dockerfile",
2101
+ makefile: "makefile",
2102
+ cmake: "cmake",
2103
+ lua: "lua",
2104
+ perl: "perl",
2105
+ r: "r",
2106
+ scala: "scala",
2107
+ clj: "clojure",
2108
+ ex: "elixir",
2109
+ exs: "elixir",
2110
+ erl: "erlang",
2111
+ hs: "haskell",
2112
+ ml: "ocaml",
2113
+ vim: "vim",
2114
+ graphql: "graphql",
2115
+ proto: "protobuf",
2116
+ tf: "hcl",
2117
+ hcl: "hcl",
2118
+ txt: "text",
2119
+ text: "text",
2120
+ log: "log",
2121
+ csv: "csv",
2122
+ tsv: "tsv",
2123
+ ini: "ini",
2124
+ cfg: "conf",
2125
+ conf: "conf",
2126
+ config: "conf",
2127
+ properties: "conf",
2128
+ env: "env",
2129
+ };
2130
+
2131
+ return extToLang[ext];
2132
+ }
2133
+
2134
+ export function getSymbolTheme(): SymbolTheme {
2135
+ const preset = theme.getSymbolPreset();
2136
+
2137
+ return {
2138
+ cursor: theme.nav.cursor,
2139
+ inputCursor: preset === "ascii" ? "|" : "▏",
2140
+ ellipsis: theme.format.ellipsis,
2141
+ boxRound: theme.boxRound,
2142
+ boxSharp: theme.boxSharp,
2143
+ table: theme.boxSharp,
2144
+ quoteBorder: theme.md.quoteBorder,
2145
+ hrChar: theme.md.hrChar,
2146
+ spinnerFrames: theme.getSpinnerFrames("activity"),
2147
+ };
2148
+ }
2149
+
2150
+ export function getMarkdownTheme(): MarkdownTheme {
2151
+ return {
2152
+ heading: (text: string) => theme.fg("mdHeading", text),
2153
+ link: (text: string) => theme.fg("mdLink", text),
2154
+ linkUrl: (text: string) => theme.fg("mdLinkUrl", text),
2155
+ code: (text: string) => theme.fg("mdCode", text),
2156
+ codeBlock: (text: string) => theme.fg("mdCodeBlock", text),
2157
+ codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text),
2158
+ quote: (text: string) => theme.fg("mdQuote", text),
2159
+ quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text),
2160
+ hr: (text: string) => theme.fg("mdHr", text),
2161
+ listBullet: (text: string) => theme.fg("mdListBullet", text),
2162
+ bold: (text: string) => theme.bold(text),
2163
+ italic: (text: string) => theme.italic(text),
2164
+ underline: (text: string) => theme.underline(text),
2165
+ strikethrough: (text: string) => chalk.strikethrough(text),
2166
+ symbols: getSymbolTheme(),
2167
+ highlightCode: (code: string, lang?: string): string[] => {
2168
+ // Validate language before highlighting to avoid stderr spam from cli-highlight
2169
+ const validLang = lang && supportsLanguage(lang) ? lang : undefined;
2170
+ const opts = {
2171
+ language: validLang,
2172
+ ignoreIllegals: true,
2173
+ theme: getCliHighlightTheme(theme),
2174
+ };
2175
+ try {
2176
+ return highlight(code, opts).split("\n");
2177
+ } catch {
2178
+ return code.split("\n").map((line) => theme.fg("mdCodeBlock", line));
2179
+ }
2180
+ },
2181
+ };
2182
+ }
2183
+
2184
+ export function getSelectListTheme(): SelectListTheme {
2185
+ return {
2186
+ selectedPrefix: (text: string) => theme.fg("accent", text),
2187
+ selectedText: (text: string) => theme.fg("accent", text),
2188
+ description: (text: string) => theme.fg("muted", text),
2189
+ scrollInfo: (text: string) => theme.fg("muted", text),
2190
+ noMatch: (text: string) => theme.fg("muted", text),
2191
+ symbols: getSymbolTheme(),
2192
+ };
2193
+ }
2194
+
2195
+ export function getEditorTheme(): EditorTheme {
2196
+ return {
2197
+ borderColor: (text: string) => theme.fg("borderMuted", text),
2198
+ selectList: getSelectListTheme(),
2199
+ symbols: getSymbolTheme(),
2200
+ };
2201
+ }
2202
+
2203
+ export function getSettingsListTheme(): import("@oh-my-pi/pi-tui").SettingsListTheme {
2204
+ return {
2205
+ label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text),
2206
+ value: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : theme.fg("muted", text)),
2207
+ description: (text: string) => theme.fg("dim", text),
2208
+ cursor: theme.fg("accent", `${theme.nav.cursor} `),
2209
+ hint: (text: string) => theme.fg("dim", text),
2210
+ };
2211
+ }