@selesai/code 0.9.15 → 0.10.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 (267) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/package.json +3 -3
  193. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  194. package/dist/extensions/auto-model/classifier.ts +0 -23
  195. package/dist/extensions/auto-model/config.ts +0 -51
  196. package/dist/extensions/auto-model/index.ts +0 -22
  197. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  198. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  199. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  200. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  201. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  202. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  203. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  204. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  205. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  206. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  207. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  208. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  209. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  210. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  211. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  212. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  213. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  214. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  215. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  216. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  217. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  218. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  219. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  220. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  221. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  222. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  223. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  224. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  225. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  226. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  227. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  228. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  229. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  230. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  231. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  232. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  233. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  234. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  235. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  236. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  237. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  238. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  239. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  240. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  241. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  242. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  243. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  244. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  245. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  246. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  247. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  248. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  249. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  250. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  251. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  252. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  253. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  254. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  255. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  256. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  257. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  258. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  259. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  260. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  261. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  262. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  263. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  264. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  265. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  266. package/dist/extensions/preview-tools-disabled.ts +0 -11
  267. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,2100 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ fchmodSync,
6
+ fsyncSync,
7
+ lstatSync,
8
+ openSync,
9
+ readFileSync,
10
+ realpathSync,
11
+ renameSync,
12
+ statSync,
13
+ unlinkSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { basename, dirname, join } from "node:path";
17
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
18
+ import {
19
+ ICON_GLYPH_KEYS,
20
+ type IconGlyphs,
21
+ type IconMode,
22
+ NERD_DEFAULT_ICONS,
23
+ normalizeIconMode,
24
+ type ResolvedIcons,
25
+ resolveConfiguredIcons,
26
+ } from "./icons";
27
+ import { isSupportedColorSpec } from "./style";
28
+ import { normalizeWorkingLineMessages } from "./working-line";
29
+ import { PI_WORKING_LINE_MESSAGES } from "./working-line-messages";
30
+
31
+ export type ColorSpec = string;
32
+ export type ColorSource = "theme" | "terminal";
33
+ export type { IconMode } from "./icons";
34
+
35
+ export type ContextStyle = "text" | "gauge" | "text+gauge";
36
+ export type SeparatorStyle = "pipe" | "dot" | "chevron" | "none";
37
+ export type ModelLabelSource = "id" | "name";
38
+ export type EditorStyle = "opencode" | "opencode-copy-friendly" | "accent-rail" | "minimalist";
39
+ export type UserMessageStyle = "framed" | "framed-copy-friendly" | "compact" | "labeled";
40
+ export type SelectorBorderStyle = "zentui";
41
+ export type FooterStyle = "native" | "starship" | "hidden";
42
+ export type WorkingLineSpinner =
43
+ | "braille"
44
+ | "star-bloom"
45
+ | "pinwheel"
46
+ | "claude-inspired"
47
+ | "pulse";
48
+ export type WorkingLineTextAnimation = "classic" | "kitt" | "disabled";
49
+ export type ComponentStyleOwner = "editor" | "userMessages" | "selectorBorders" | "footer";
50
+ export type MinimalistPathDisplayMode = "compact" | "project" | "full";
51
+ export type MinimalistContextFormat = "percent" | "percent-total";
52
+ export type EditorBorderColorMode = "static" | "adaptive";
53
+ export type CompletionMenuStyle = "native" | "palette";
54
+ export type CompactFooterMaxLines = 1 | 2 | 3 | "unlimited";
55
+
56
+ export const DEFAULT_COMPACT_FOOTER_FORMAT =
57
+ "$cwd$wrap(in $session_name)$wrap(on $git_branch) $git_status$wrap$context$wrap_sep$tokens";
58
+
59
+ export type ContextThresholds = {
60
+ warning: number;
61
+ error: number;
62
+ };
63
+
64
+ export type PathDisplayMode = "basename" | "full" | "repository";
65
+
66
+ export type PathDisplayConfig = {
67
+ mode: PathDisplayMode;
68
+ /** Final components to show in full/repository mode. 0 = unlimited; clamped to 0..5. */
69
+ depth: number;
70
+ };
71
+
72
+ export type GitBranchMaxLength = "full" | number;
73
+
74
+ export type GitBranchConfig = {
75
+ maxLength: GitBranchMaxLength;
76
+ };
77
+
78
+ export type ColorSourcesConfig = {
79
+ starship: ColorSource;
80
+ editor: ColorSource;
81
+ userMessages: ColorSource;
82
+ };
83
+
84
+ export type UiFeaturesConfig = {
85
+ editor: boolean;
86
+ statusLine: boolean;
87
+ viewportIndicators: boolean;
88
+ };
89
+
90
+ export type FooterSegmentsConfig = {
91
+ cwd: boolean;
92
+ sessionName: boolean;
93
+ gitBranch: boolean;
94
+ gitStatus: boolean;
95
+ gitCounts: boolean;
96
+ gitCommit: boolean;
97
+ gitMetrics: boolean;
98
+ runtime: boolean;
99
+ modelInfo: boolean;
100
+ context: boolean;
101
+ tokens: boolean;
102
+ cost: boolean;
103
+ sessionDuration: boolean;
104
+ username: boolean;
105
+ time: boolean;
106
+ os: boolean;
107
+ packageVersion: boolean;
108
+ };
109
+
110
+ export type PolishedEditorStyleConfig = {
111
+ metadataFormat: string;
112
+ completionMenu: CompletionMenuStyle;
113
+ };
114
+
115
+ export type PolishedCopyFriendlyEditorStyleConfig = {
116
+ metadataFormat: string;
117
+ completionMenu: CompletionMenuStyle;
118
+ };
119
+
120
+ export type AccentRailEditorStyleConfig = {
121
+ rail: string;
122
+ asciiRail: string;
123
+ transparent: boolean;
124
+ };
125
+
126
+ export type MinimalistEditorStyleConfig = {
127
+ pathDisplay: MinimalistPathDisplayMode;
128
+ contextFormat: MinimalistContextFormat;
129
+ contextGauge: boolean;
130
+ showSessionName: boolean;
131
+ showTimer: boolean;
132
+ showCost: boolean;
133
+ showGit: boolean;
134
+ contextThresholds: ContextThresholds;
135
+ };
136
+
137
+ /** Temporary name retained for existing settings consumers. */
138
+ export type MinimalistConfig = MinimalistEditorStyleConfig;
139
+
140
+ export type EditorStylesConfig = {
141
+ minimalist: MinimalistEditorStyleConfig;
142
+ };
143
+
144
+ export type EditorComponentConfig = {
145
+ enabled: boolean;
146
+ style: EditorStyle;
147
+ colorSource: ColorSource;
148
+ borderColorMode: EditorBorderColorMode;
149
+ modelLabel: ModelLabelSource;
150
+ viewportIndicators: boolean;
151
+ styles: {
152
+ opencode: PolishedEditorStyleConfig;
153
+ "opencode-copy-friendly": PolishedCopyFriendlyEditorStyleConfig;
154
+ "accent-rail": AccentRailEditorStyleConfig;
155
+ minimalist: MinimalistEditorStyleConfig;
156
+ };
157
+ };
158
+
159
+ export type FramedUserMessageStyleConfig = Record<string, never>;
160
+ export type FramedCopyFriendlyUserMessageStyleConfig = Record<string, never>;
161
+ export type CompactUserMessageStyleConfig = Record<string, never>;
162
+ export type LabeledUserMessageStyleConfig = Record<string, never>;
163
+
164
+ export type UserMessagesComponentConfig = {
165
+ enabled: boolean;
166
+ style: UserMessageStyle;
167
+ colorSource: ColorSource;
168
+ styles: {
169
+ framed: FramedUserMessageStyleConfig;
170
+ "framed-copy-friendly": FramedCopyFriendlyUserMessageStyleConfig;
171
+ compact: CompactUserMessageStyleConfig;
172
+ labeled: LabeledUserMessageStyleConfig;
173
+ };
174
+ };
175
+
176
+ export type SelectorBordersComponentConfig = {
177
+ enabled: boolean;
178
+ style: SelectorBorderStyle;
179
+ colorSource: ColorSource;
180
+ };
181
+
182
+ export type StarshipFooterStyleConfig = {
183
+ format: string;
184
+ responsive: boolean;
185
+ compactFormat: string;
186
+ compactMaxLines: CompactFooterMaxLines;
187
+ separator: SeparatorStyle;
188
+ contextStyle: ContextStyle;
189
+ contextThresholds: ContextThresholds;
190
+ pathDisplay: PathDisplayConfig;
191
+ segments: FooterSegmentsConfig;
192
+ gitBranch: GitBranchConfig;
193
+ gitCommit: GitCommitConfig;
194
+ gitMetrics: GitMetricsConfig;
195
+ extensionStatuses: ExtensionStatusesConfig;
196
+ };
197
+
198
+ export type FooterComponentConfig = {
199
+ style: FooterStyle;
200
+ colorSource: ColorSource;
201
+ modelLabel: ModelLabelSource;
202
+ styles: {
203
+ starship: StarshipFooterStyleConfig;
204
+ };
205
+ };
206
+
207
+ export type WorkingLineMessagesConfig = {
208
+ custom: boolean;
209
+ values: string[];
210
+ };
211
+
212
+ export type WorkingLineSegmentsConfig = {
213
+ tool: boolean;
214
+ elapsed: boolean;
215
+ thought: boolean;
216
+ tokens: boolean;
217
+ };
218
+
219
+ export const DEFAULT_WORKING_LINE_SPINNER_INTERVAL_MS = 100;
220
+ export const DEFAULT_WORKING_LINE_TEXT_INTERVAL_MS = 60;
221
+ export const MIN_WORKING_LINE_INTERVAL_MS = 30;
222
+ export const MAX_WORKING_LINE_INTERVAL_MS = 1000;
223
+
224
+ export function isValidWorkingLineIntervalMs(value: unknown): value is number {
225
+ return (
226
+ typeof value === "number" &&
227
+ Number.isSafeInteger(value) &&
228
+ value >= MIN_WORKING_LINE_INTERVAL_MS &&
229
+ value <= MAX_WORKING_LINE_INTERVAL_MS
230
+ );
231
+ }
232
+
233
+ export type WorkingLineComponentConfig = {
234
+ enabled: boolean;
235
+ turnSummary: boolean;
236
+ spinner: WorkingLineSpinner;
237
+ spinnerIntervalMs: number;
238
+ animateSpinnerColor: boolean;
239
+ textIntervalMs: number;
240
+ textAnimation: WorkingLineTextAnimation;
241
+ colorSource: ColorSource;
242
+ messages: WorkingLineMessagesConfig;
243
+ segments: WorkingLineSegmentsConfig;
244
+ };
245
+
246
+ export type WorkingLineComponentPatch = Partial<
247
+ Omit<WorkingLineComponentConfig, "messages" | "segments">
248
+ > & {
249
+ messages?: Partial<WorkingLineMessagesConfig>;
250
+ segments?: Partial<WorkingLineSegmentsConfig>;
251
+ };
252
+
253
+ export type ComponentsConfig = {
254
+ editor: EditorComponentConfig;
255
+ userMessages: UserMessagesComponentConfig;
256
+ workingLine: WorkingLineComponentConfig;
257
+ selectorBorders: SelectorBordersComponentConfig;
258
+ footer: FooterComponentConfig;
259
+ };
260
+
261
+ export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
262
+ export type ExtensionStatusColorMode = "zentui" | "original";
263
+
264
+ /**
265
+ * Starship `git_commit`-style options.
266
+ * See https://starship.rs/config/#git-commit
267
+ */
268
+ export type GitCommitConfig = {
269
+ hashLength: number;
270
+ onlyDetached: boolean;
271
+ showTag: boolean;
272
+ };
273
+
274
+ /**
275
+ * Starship `git_metrics`-style options.
276
+ * See https://starship.rs/config/#git-metrics
277
+ */
278
+ export type GitMetricsConfig = {
279
+ onlyNonzero: boolean;
280
+ ignoreSubmodules: boolean;
281
+ };
282
+
283
+ const DEFAULT_EXTENSION_STATUS_PLACEMENT: ExtensionStatusPlacement = "right";
284
+ const DEFAULT_EXTENSION_STATUS_COLOR_MODE: ExtensionStatusColorMode = "zentui";
285
+
286
+ export type ExtensionStatusesConfig = {
287
+ defaultPlacement: ExtensionStatusPlacement;
288
+ placements: Record<string, ExtensionStatusPlacement>;
289
+ colorModes: Record<string, ExtensionStatusColorMode>;
290
+ };
291
+
292
+ const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
293
+ const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
294
+ export const DEFAULT_EDITOR_METADATA_FORMAT = "$model $provider( $thinking)";
295
+
296
+ export type ZentuiConfig = {
297
+ projectRefreshIntervalMs: number;
298
+ icons: ResolvedIcons;
299
+ colors: PolishedTuiColors;
300
+ components: ComponentsConfig;
301
+ };
302
+
303
+ export type PolishedTuiColors = {
304
+ cwd: ColorSpec;
305
+ sessionName: ColorSpec;
306
+ gitBranch: ColorSpec;
307
+ gitStatus: ColorSpec;
308
+ contextNormal: ColorSpec;
309
+ contextWarning: ColorSpec;
310
+ contextError: ColorSpec;
311
+ tokens: ColorSpec;
312
+ cost: ColorSpec;
313
+ separator: ColorSpec;
314
+ runtimePrefix: ColorSpec;
315
+ extensionStatus: ColorSpec;
316
+ sessionDuration: ColorSpec;
317
+ packageVersion: ColorSpec;
318
+ gitCommit: ColorSpec;
319
+ gitMetricsAdded: ColorSpec;
320
+ gitMetricsDeleted: ColorSpec;
321
+ username: ColorSpec;
322
+ time: ColorSpec;
323
+ os: ColorSpec;
324
+ editorAccent?: ColorSpec;
325
+ editorRail?: ColorSpec;
326
+ editorPrompt?: ColorSpec;
327
+ editorBorder?: ColorSpec;
328
+ editorGitBranch?: ColorSpec;
329
+ editorModel?: ColorSpec;
330
+ editorProvider?: ColorSpec;
331
+ editorThinking?: ColorSpec;
332
+ editorThinkingMinimal?: ColorSpec;
333
+ editorThinkingLow?: ColorSpec;
334
+ editorThinkingMedium?: ColorSpec;
335
+ editorThinkingHigh?: ColorSpec;
336
+ editorThinkingXhigh?: ColorSpec;
337
+ editorThinkingMax?: ColorSpec;
338
+ workingLineLow?: ColorSpec;
339
+ workingLineMid?: ColorSpec;
340
+ workingLineHigh?: ColorSpec;
341
+ };
342
+
343
+ /**
344
+ * Canonical configuration plus a temporary flat compatibility projection used
345
+ * by production consumers while they migrate to `components`.
346
+ */
347
+ export type PolishedTuiConfig = ZentuiConfig & {
348
+ footerFormat: string;
349
+ responsiveFooter: boolean;
350
+ compactFooterFormat: string;
351
+ compactFooterMaxLines: CompactFooterMaxLines;
352
+ editorMetadataFormat: string;
353
+ separator: SeparatorStyle;
354
+ contextStyle: ContextStyle;
355
+ editorModelLabel: ModelLabelSource;
356
+ editorStyle: EditorStyle;
357
+ editorStyles: EditorStylesConfig;
358
+ editorBorderColorMode: EditorBorderColorMode;
359
+ contextThresholds: ContextThresholds;
360
+ pathDisplay: PathDisplayConfig;
361
+ gitBranch: GitBranchConfig;
362
+ colorSources: ColorSourcesConfig;
363
+ features: UiFeaturesConfig;
364
+ footerSegments: FooterSegmentsConfig;
365
+ gitCommit: GitCommitConfig;
366
+ gitMetrics: GitMetricsConfig;
367
+ extensionStatuses: ExtensionStatusesConfig;
368
+ };
369
+
370
+ /**
371
+ * Canonical footer format variable names. In a `footerFormat` string these
372
+ * are written as `$name` or `${name}`.
373
+ */
374
+ export const FOOTER_FORMAT_VARIABLES = [
375
+ "cwd",
376
+ "session_name",
377
+ "git_branch",
378
+ "git_status",
379
+ "git_state",
380
+ "runtime",
381
+ "model",
382
+ "provider",
383
+ "session_duration",
384
+ "username",
385
+ "os",
386
+ "time",
387
+ "context",
388
+ "tokens",
389
+ "cache_read",
390
+ "cache_write",
391
+ "cost",
392
+ "subscription",
393
+ "auto_compaction",
394
+ "package",
395
+ "package_version",
396
+ "git_commit",
397
+ "git_tag",
398
+ "git_metrics",
399
+ "git_added",
400
+ "git_deleted",
401
+ "sep",
402
+ ] as const;
403
+
404
+ /**
405
+ * Alias → canonical variable name mapping for `footerFormat`.
406
+ * `$fill` is special (not a variable) and handled by the parser.
407
+ */
408
+ export const FOOTER_FORMAT_ALIASES: Record<string, string> = {
409
+ directory: "cwd",
410
+ branch: "git_branch",
411
+ status: "git_status",
412
+ state: "git_state",
413
+ commit: "git_commit",
414
+ tag: "git_tag",
415
+ duration: "session_duration",
416
+ separator: "sep",
417
+ };
418
+
419
+ export const configPath = join(getAgentDir(), "zentui.json");
420
+
421
+ const defaultFooterSegments: FooterSegmentsConfig = {
422
+ cwd: true,
423
+ sessionName: true,
424
+ gitBranch: true,
425
+ gitStatus: true,
426
+ gitCounts: false,
427
+ gitCommit: false,
428
+ gitMetrics: true,
429
+ runtime: true,
430
+ modelInfo: false,
431
+ context: false,
432
+ tokens: true,
433
+ cost: true,
434
+ sessionDuration: true,
435
+ username: false,
436
+ time: false,
437
+ os: false,
438
+ packageVersion: false,
439
+ };
440
+
441
+ const defaultAccentRailStyle: AccentRailEditorStyleConfig = {
442
+ rail: "▎",
443
+ asciiRail: "|",
444
+ transparent: false,
445
+ };
446
+
447
+ const defaultMinimalistStyle: MinimalistEditorStyleConfig = {
448
+ pathDisplay: "project",
449
+ contextFormat: "percent-total",
450
+ contextGauge: false,
451
+ showSessionName: true,
452
+ showTimer: false,
453
+ showCost: false,
454
+ showGit: false,
455
+ contextThresholds: { warning: 50, error: 80 },
456
+ };
457
+
458
+ const defaultStarshipStyle: StarshipFooterStyleConfig = {
459
+ format: "",
460
+ responsive: true,
461
+ compactFormat: DEFAULT_COMPACT_FOOTER_FORMAT,
462
+ compactMaxLines: "unlimited",
463
+ separator: "pipe",
464
+ contextStyle: "text+gauge",
465
+ contextThresholds: { warning: 70, error: 90 },
466
+ pathDisplay: { mode: "basename", depth: 2 },
467
+ segments: defaultFooterSegments,
468
+ gitBranch: { maxLength: "full" },
469
+ gitCommit: { hashLength: 7, onlyDetached: true, showTag: true },
470
+ gitMetrics: { onlyNonzero: true, ignoreSubmodules: false },
471
+ extensionStatuses: { defaultPlacement: "right", placements: {}, colorModes: {} },
472
+ };
473
+
474
+ const defaultComponents: ComponentsConfig = {
475
+ editor: {
476
+ enabled: true,
477
+ style: "minimalist",
478
+ colorSource: "theme",
479
+ borderColorMode: "adaptive",
480
+ modelLabel: "name",
481
+ viewportIndicators: false,
482
+ styles: {
483
+ opencode: {
484
+ metadataFormat: DEFAULT_EDITOR_METADATA_FORMAT,
485
+ completionMenu: "native",
486
+ },
487
+ "opencode-copy-friendly": {
488
+ metadataFormat: DEFAULT_EDITOR_METADATA_FORMAT,
489
+ completionMenu: "native",
490
+ },
491
+ "accent-rail": defaultAccentRailStyle,
492
+ minimalist: defaultMinimalistStyle,
493
+ },
494
+ },
495
+ userMessages: {
496
+ enabled: true,
497
+ style: "labeled",
498
+ colorSource: "theme",
499
+ styles: { framed: {}, "framed-copy-friendly": {}, compact: {}, labeled: {} },
500
+ },
501
+ workingLine: {
502
+ enabled: true,
503
+ turnSummary: true,
504
+ spinner: "claude-inspired",
505
+ spinnerIntervalMs: 160,
506
+ animateSpinnerColor: false,
507
+ textIntervalMs: DEFAULT_WORKING_LINE_TEXT_INTERVAL_MS,
508
+ textAnimation: "classic",
509
+ colorSource: "theme",
510
+ messages: { custom: true, values: [...PI_WORKING_LINE_MESSAGES] },
511
+ segments: { tool: true, elapsed: true, thought: true, tokens: true },
512
+ },
513
+ selectorBorders: { enabled: true, style: "zentui", colorSource: "theme" },
514
+ footer: {
515
+ style: "starship",
516
+ colorSource: "theme",
517
+ modelLabel: "name",
518
+ styles: { starship: defaultStarshipStyle },
519
+ },
520
+ };
521
+
522
+ export const defaultConfig: PolishedTuiConfig = {
523
+ projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
524
+ icons: { mode: "ascii", ...NERD_DEFAULT_ICONS },
525
+ colors: {
526
+ cwd: "bold cyan",
527
+ sessionName: "bold green",
528
+ gitBranch: "bold purple",
529
+ gitStatus: "bold red",
530
+ contextNormal: "bright-black",
531
+ contextWarning: "bold yellow",
532
+ contextError: "bold red",
533
+ tokens: "bright-black",
534
+ cost: "bold green",
535
+ separator: "bright-black",
536
+ runtimePrefix: "",
537
+ extensionStatus: "bright-black",
538
+ sessionDuration: "yellow",
539
+ packageVersion: "208",
540
+ gitCommit: "bold green",
541
+ gitMetricsAdded: "bold green",
542
+ gitMetricsDeleted: "bold red",
543
+ username: "bold yellow",
544
+ time: "bold yellow",
545
+ os: "bold white",
546
+ },
547
+ components: defaultComponents,
548
+ footerFormat: defaultStarshipStyle.format,
549
+ responsiveFooter: defaultStarshipStyle.responsive,
550
+ compactFooterFormat: defaultStarshipStyle.compactFormat,
551
+ compactFooterMaxLines: defaultStarshipStyle.compactMaxLines,
552
+ editorMetadataFormat: DEFAULT_EDITOR_METADATA_FORMAT,
553
+ separator: defaultStarshipStyle.separator,
554
+ contextStyle: defaultStarshipStyle.contextStyle,
555
+ editorModelLabel: defaultComponents.editor.modelLabel,
556
+ editorStyle: defaultComponents.editor.style,
557
+ editorStyles: { minimalist: defaultMinimalistStyle },
558
+ editorBorderColorMode: defaultComponents.editor.borderColorMode,
559
+ contextThresholds: defaultStarshipStyle.contextThresholds,
560
+ pathDisplay: defaultStarshipStyle.pathDisplay,
561
+ gitBranch: defaultStarshipStyle.gitBranch,
562
+ colorSources: { starship: "theme", editor: "theme", userMessages: "theme" },
563
+ features: {
564
+ editor: true,
565
+ statusLine: true,
566
+ viewportIndicators: true,
567
+ },
568
+ footerSegments: defaultFooterSegments,
569
+ gitCommit: defaultStarshipStyle.gitCommit,
570
+ gitMetrics: defaultStarshipStyle.gitMetrics,
571
+ extensionStatuses: defaultStarshipStyle.extensionStatuses,
572
+ };
573
+
574
+ type ConfigRecord = Record<string, unknown>;
575
+
576
+ function isRecord(value: unknown): value is ConfigRecord {
577
+ return value !== null && typeof value === "object" && !Array.isArray(value);
578
+ }
579
+
580
+ function parseProjectRefreshIntervalMs(value: unknown): number {
581
+ if (value === 0) return 0;
582
+ if (typeof value !== "number" || !Number.isFinite(value)) {
583
+ return DEFAULT_PROJECT_REFRESH_INTERVAL_MS;
584
+ }
585
+
586
+ const interval = Math.round(value);
587
+ if (interval <= 0) return 0;
588
+ return Math.max(MIN_PROJECT_REFRESH_INTERVAL_MS, interval);
589
+ }
590
+
591
+ function clampPercent(value: number): number {
592
+ return Math.max(0, Math.min(100, value));
593
+ }
594
+
595
+ function parseContextStyle(value: unknown): ContextStyle {
596
+ if (value === "text" || value === "gauge" || value === "text+gauge") return value;
597
+ return defaultConfig.contextStyle;
598
+ }
599
+
600
+ function parseEditorModelLabel(
601
+ value: unknown,
602
+ fallback: ModelLabelSource = defaultComponents.editor.modelLabel,
603
+ ): ModelLabelSource {
604
+ if (value === "id" || value === "name") return value;
605
+ return fallback;
606
+ }
607
+
608
+ function parseEditorStyle(value: unknown): EditorStyle {
609
+ if (
610
+ value === "opencode" ||
611
+ value === "opencode-copy-friendly" ||
612
+ value === "accent-rail" ||
613
+ value === "minimalist"
614
+ ) {
615
+ return value;
616
+ }
617
+ if (value === "polished") return "opencode";
618
+ if (value === "polished-copy-friendly") return "opencode-copy-friendly";
619
+ return defaultConfig.editorStyle;
620
+ }
621
+
622
+ function parseEditorBorderColorMode(value: unknown): EditorBorderColorMode {
623
+ if (value === "static" || value === "adaptive") return value;
624
+ return defaultConfig.editorBorderColorMode;
625
+ }
626
+
627
+ function parseCompletionMenuStyle(value: unknown): CompletionMenuStyle {
628
+ return value === "native" || value === "palette"
629
+ ? value
630
+ : defaultComponents.editor.styles.opencode.completionMenu;
631
+ }
632
+
633
+ export function isSeparatorStyle(value: unknown): value is SeparatorStyle {
634
+ return value === "pipe" || value === "dot" || value === "chevron" || value === "none";
635
+ }
636
+
637
+ function parseSeparatorStyle(value: unknown): SeparatorStyle {
638
+ return isSeparatorStyle(value) ? value : defaultConfig.separator;
639
+ }
640
+
641
+ function parseContextThresholds(
642
+ value: unknown,
643
+ defaults: ContextThresholds = defaultStarshipStyle.contextThresholds,
644
+ ): ContextThresholds {
645
+ if (!isRecord(value)) return { ...defaults };
646
+
647
+ const warningRaw = value.warning;
648
+ const errorRaw = value.error;
649
+ let warning =
650
+ typeof warningRaw === "number" && Number.isFinite(warningRaw)
651
+ ? clampPercent(Math.round(warningRaw))
652
+ : defaults.warning;
653
+ let error =
654
+ typeof errorRaw === "number" && Number.isFinite(errorRaw)
655
+ ? clampPercent(Math.round(errorRaw))
656
+ : defaults.error;
657
+ if (error < warning) {
658
+ const swapped = warning;
659
+ warning = error;
660
+ error = swapped;
661
+ }
662
+ return { warning, error };
663
+ }
664
+
665
+ function parsePathDisplay(value: unknown): PathDisplayConfig {
666
+ const defaults = defaultConfig.pathDisplay;
667
+ if (!isRecord(value)) return { ...defaults };
668
+ const mode =
669
+ value.mode === "full" || value.mode === "basename" || value.mode === "repository"
670
+ ? value.mode
671
+ : defaults.mode;
672
+ const rawDepth = value.depth;
673
+ const depth =
674
+ typeof rawDepth === "number" && Number.isFinite(rawDepth) && rawDepth >= 0
675
+ ? Math.min(5, Math.floor(rawDepth))
676
+ : defaults.depth;
677
+ return { mode, depth };
678
+ }
679
+
680
+ function normalizeGitBranchMaxLength(value: unknown): GitBranchMaxLength {
681
+ if (value === "full") return value;
682
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
683
+ return defaultConfig.gitBranch.maxLength;
684
+ }
685
+
686
+ function parseGitBranchConfig(value: unknown): GitBranchConfig {
687
+ const defaults = defaultConfig.gitBranch;
688
+ if (!isRecord(value)) return { ...defaults };
689
+ return {
690
+ maxLength: normalizeGitBranchMaxLength(value.maxLength),
691
+ };
692
+ }
693
+
694
+ function stringValue(record: Record<string, unknown>, key: string): string | undefined {
695
+ const value = record[key];
696
+ return typeof value === "string" ? value : undefined;
697
+ }
698
+
699
+ function parseCompactFooterMaxLines(value: unknown): CompactFooterMaxLines {
700
+ return value === 1 || value === 2 || value === 3 || value === "unlimited"
701
+ ? value
702
+ : defaultStarshipStyle.compactMaxLines;
703
+ }
704
+
705
+ function colorValue(record: Record<string, unknown>, key: string): string | undefined {
706
+ const value = stringValue(record, key);
707
+ return value !== undefined && isSupportedColorSpec(value) ? value : undefined;
708
+ }
709
+
710
+ function definedColors(
711
+ colors: Partial<Record<keyof PolishedTuiConfig["colors"], string | undefined>>,
712
+ ): Partial<PolishedTuiConfig["colors"]> {
713
+ return Object.fromEntries(
714
+ Object.entries(colors).filter(
715
+ (entry): entry is [keyof PolishedTuiConfig["colors"], string] => typeof entry[1] === "string",
716
+ ),
717
+ ) as Partial<PolishedTuiConfig["colors"]>;
718
+ }
719
+
720
+ function normalizeIconOverrides(record: Record<string, unknown>): Partial<IconGlyphs> {
721
+ return Object.fromEntries(
722
+ ICON_GLYPH_KEYS.flatMap((key) => {
723
+ const value = stringValue(record, key);
724
+ return value === undefined ? [] : [[key, value]];
725
+ }),
726
+ ) as Partial<IconGlyphs>;
727
+ }
728
+
729
+ function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiConfig["colors"]> {
730
+ return definedColors({
731
+ cwd: colorValue(record, "cwd") ?? colorValue(record, "cwdText"),
732
+ sessionName: colorValue(record, "sessionName"),
733
+ gitBranch: colorValue(record, "gitBranch") ?? colorValue(record, "git"),
734
+ gitStatus: colorValue(record, "gitStatus"),
735
+ contextNormal: colorValue(record, "contextNormal"),
736
+ contextWarning: colorValue(record, "contextWarning"),
737
+ contextError: colorValue(record, "contextError"),
738
+ tokens: colorValue(record, "tokens"),
739
+ cost: colorValue(record, "cost"),
740
+ separator: colorValue(record, "separator"),
741
+ runtimePrefix: colorValue(record, "runtimePrefix"),
742
+ extensionStatus: colorValue(record, "extensionStatus"),
743
+ sessionDuration: colorValue(record, "sessionDuration"),
744
+ packageVersion: colorValue(record, "packageVersion"),
745
+ gitCommit: colorValue(record, "gitCommit"),
746
+ gitMetricsAdded: colorValue(record, "gitMetricsAdded"),
747
+ gitMetricsDeleted: colorValue(record, "gitMetricsDeleted"),
748
+ username: colorValue(record, "username"),
749
+ time: colorValue(record, "time"),
750
+ os: colorValue(record, "os"),
751
+ editorAccent: colorValue(record, "editorAccent"),
752
+ editorRail: colorValue(record, "editorRail"),
753
+ editorPrompt: colorValue(record, "editorPrompt"),
754
+ editorBorder: colorValue(record, "editorBorder"),
755
+ editorGitBranch: colorValue(record, "editorGitBranch"),
756
+ editorModel: colorValue(record, "editorModel"),
757
+ editorProvider: colorValue(record, "editorProvider"),
758
+ editorThinking: colorValue(record, "editorThinking"),
759
+ editorThinkingMinimal: colorValue(record, "editorThinkingMinimal"),
760
+ editorThinkingLow: colorValue(record, "editorThinkingLow"),
761
+ editorThinkingMedium: colorValue(record, "editorThinkingMedium"),
762
+ editorThinkingHigh: colorValue(record, "editorThinkingHigh"),
763
+ editorThinkingXhigh: colorValue(record, "editorThinkingXhigh"),
764
+ editorThinkingMax: colorValue(record, "editorThinkingMax"),
765
+ workingLineLow: colorValue(record, "workingLineLow"),
766
+ workingLineMid: colorValue(record, "workingLineMid"),
767
+ workingLineHigh: colorValue(record, "workingLineHigh"),
768
+ });
769
+ }
770
+
771
+ /** Clamp hashLength to Git's valid abbreviation range [4, 40]. */
772
+ function normalizeGitHashLength(value: unknown): number {
773
+ const parsed = typeof value === "number" ? value : Number(value);
774
+ if (!Number.isFinite(parsed)) return defaultConfig.gitCommit.hashLength;
775
+ const rounded = Math.round(parsed);
776
+ return Math.min(40, Math.max(4, rounded));
777
+ }
778
+
779
+ function normalizeGitCommitConfig(record: Record<string, unknown>): GitCommitConfig {
780
+ return {
781
+ hashLength: normalizeGitHashLength(record.hashLength),
782
+ onlyDetached:
783
+ typeof record.onlyDetached === "boolean"
784
+ ? record.onlyDetached
785
+ : defaultConfig.gitCommit.onlyDetached,
786
+ showTag: typeof record.showTag === "boolean" ? record.showTag : defaultConfig.gitCommit.showTag,
787
+ };
788
+ }
789
+
790
+ function normalizeGitMetricsConfig(record: Record<string, unknown>): GitMetricsConfig {
791
+ return {
792
+ onlyNonzero:
793
+ typeof record.onlyNonzero === "boolean"
794
+ ? record.onlyNonzero
795
+ : defaultConfig.gitMetrics.onlyNonzero,
796
+ ignoreSubmodules:
797
+ typeof record.ignoreSubmodules === "boolean"
798
+ ? record.ignoreSubmodules
799
+ : defaultConfig.gitMetrics.ignoreSubmodules,
800
+ };
801
+ }
802
+
803
+ export function isExtensionStatusPlacement(value: unknown): value is ExtensionStatusPlacement {
804
+ return value === "off" || value === "left" || value === "middle" || value === "right";
805
+ }
806
+
807
+ export function isExtensionStatusColorMode(value: unknown): value is ExtensionStatusColorMode {
808
+ return value === "zentui" || value === "original";
809
+ }
810
+
811
+ function normalizeExtensionStatuses(record: Record<string, unknown>): ExtensionStatusesConfig {
812
+ const defaultPlacement = isExtensionStatusPlacement(record.defaultPlacement)
813
+ ? record.defaultPlacement
814
+ : defaultConfig.extensionStatuses.defaultPlacement;
815
+ const placements = isRecord(record.placements)
816
+ ? Object.fromEntries(
817
+ Object.entries(record.placements).filter(
818
+ (entry): entry is [string, ExtensionStatusPlacement] =>
819
+ isExtensionStatusPlacement(entry[1]),
820
+ ),
821
+ )
822
+ : {};
823
+ const colorModes = isRecord(record.colorModes)
824
+ ? Object.fromEntries(
825
+ Object.entries(record.colorModes).filter(
826
+ (entry): entry is [string, ExtensionStatusColorMode] =>
827
+ isExtensionStatusColorMode(entry[1]),
828
+ ),
829
+ )
830
+ : {};
831
+
832
+ return {
833
+ defaultPlacement,
834
+ placements,
835
+ colorModes,
836
+ };
837
+ }
838
+
839
+ function isColorSourceKey(value: string): value is keyof ColorSourcesConfig {
840
+ return value === "starship" || value === "editor" || value === "userMessages";
841
+ }
842
+
843
+ function isUiFeatureKey(value: string): value is keyof UiFeaturesConfig {
844
+ return value === "editor" || value === "statusLine" || value === "viewportIndicators";
845
+ }
846
+
847
+ function isFooterSegmentKey(value: string): value is keyof FooterSegmentsConfig {
848
+ return (
849
+ value === "cwd" ||
850
+ value === "sessionName" ||
851
+ value === "gitBranch" ||
852
+ value === "gitStatus" ||
853
+ value === "gitCounts" ||
854
+ value === "runtime" ||
855
+ value === "modelInfo" ||
856
+ value === "context" ||
857
+ value === "tokens" ||
858
+ value === "cost" ||
859
+ value === "sessionDuration" ||
860
+ value === "username" ||
861
+ value === "time" ||
862
+ value === "os" ||
863
+ value === "packageVersion" ||
864
+ value === "gitCommit" ||
865
+ value === "gitMetrics"
866
+ );
867
+ }
868
+
869
+ function validColorSourceEntries(record: Record<string, unknown>): Partial<ColorSourcesConfig> {
870
+ return Object.fromEntries(
871
+ Object.entries(record).filter((entry): entry is [keyof ColorSourcesConfig, ColorSource] => {
872
+ const [key, value] = entry;
873
+ return isColorSourceKey(key) && (value === "theme" || value === "terminal");
874
+ }),
875
+ ) as Partial<ColorSourcesConfig>;
876
+ }
877
+
878
+ function validUiFeatureEntries(record: Record<string, unknown>): Partial<UiFeaturesConfig> {
879
+ return Object.fromEntries(
880
+ Object.entries(record).filter((entry): entry is [keyof UiFeaturesConfig, boolean] => {
881
+ const [key, value] = entry;
882
+ return isUiFeatureKey(key) && typeof value === "boolean";
883
+ }),
884
+ ) as Partial<UiFeaturesConfig>;
885
+ }
886
+
887
+ function validFooterSegmentEntries(record: Record<string, unknown>): Partial<FooterSegmentsConfig> {
888
+ return Object.fromEntries(
889
+ Object.entries(record).filter((entry): entry is [keyof FooterSegmentsConfig, boolean] => {
890
+ const [key, value] = entry;
891
+ return isFooterSegmentKey(key) && typeof value === "boolean";
892
+ }),
893
+ ) as Partial<FooterSegmentsConfig>;
894
+ }
895
+
896
+ type ConfigFileState =
897
+ | { kind: "missing"; record: ConfigRecord; writePath: string }
898
+ | { kind: "valid"; record: ConfigRecord; writePath: string; mode: number }
899
+ | { kind: "corrupt"; error: unknown };
900
+
901
+ function errorCode(error: unknown): string | undefined {
902
+ return typeof error === "object" && error !== null && "code" in error
903
+ ? String(error.code)
904
+ : undefined;
905
+ }
906
+
907
+ function readConfigFileState(path: string): ConfigFileState {
908
+ let writePath = path;
909
+ try {
910
+ const pathStat = lstatSync(path);
911
+ if (pathStat.isSymbolicLink()) writePath = realpathSync(path);
912
+ const targetStat = statSync(writePath);
913
+ const parsed = JSON.parse(readFileSync(writePath, "utf8"));
914
+ return isRecord(parsed)
915
+ ? { kind: "valid", record: parsed, writePath, mode: targetStat.mode & 0o7777 }
916
+ : { kind: "corrupt", error: new Error("top-level value must be a JSON object") };
917
+ } catch (error) {
918
+ if (errorCode(error) === "ENOENT") {
919
+ try {
920
+ lstatSync(path);
921
+ } catch (pathError) {
922
+ if (errorCode(pathError) === "ENOENT")
923
+ return { kind: "missing", record: {}, writePath: path };
924
+ }
925
+ }
926
+ return { kind: "corrupt", error };
927
+ }
928
+ }
929
+
930
+ function writeConfigAtomically(path: string, record: ConfigRecord, mode?: number): void {
931
+ const tempPath = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
932
+ let file: number | undefined;
933
+ try {
934
+ file = openSync(tempPath, "wx", mode ?? 0o666);
935
+ if (mode !== undefined) fchmodSync(file, mode);
936
+ writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, "utf8");
937
+ fsyncSync(file);
938
+ closeSync(file);
939
+ file = undefined;
940
+ renameSync(tempPath, path);
941
+ } catch (error) {
942
+ if (file !== undefined) {
943
+ try {
944
+ closeSync(file);
945
+ } catch {}
946
+ }
947
+ try {
948
+ unlinkSync(tempPath);
949
+ } catch (cleanupError) {
950
+ if (errorCode(cleanupError) !== "ENOENT") {
951
+ // Preserve the persistence failure; the best-effort cleanup error is secondary.
952
+ }
953
+ }
954
+ throw error;
955
+ }
956
+ }
957
+
958
+ function mutateConfig(path: string, mutate: (record: ConfigRecord) => void): PolishedTuiConfig {
959
+ const state = readConfigFileState(path);
960
+ if (state.kind === "corrupt") {
961
+ const detail = state.error instanceof Error ? ` (${state.error.message})` : "";
962
+ throw new Error(
963
+ `Refusing to save Zentui config because ${path} is corrupt or unreadable; fix or remove it first.${detail}`,
964
+ );
965
+ }
966
+ mutate(state.record);
967
+ writeConfigAtomically(
968
+ state.writePath,
969
+ state.record,
970
+ state.kind === "valid" ? state.mode : undefined,
971
+ );
972
+ return mergeConfig(state.record);
973
+ }
974
+
975
+ export function ensureConfigExists(_path = configPath): void {
976
+ // Intentionally left as a no-op. Zentui config is user-owned and
977
+ // compatibility-sensitive: runtime defaults come from `mergeConfig({})`, and
978
+ // the extension should not persist opinionated defaults unless the user
979
+ // explicitly changes a setting.
980
+ }
981
+
982
+ function hasOwn(record: ConfigRecord, key: string): boolean {
983
+ return Object.hasOwn(record, key);
984
+ }
985
+
986
+ function recordValue(value: unknown): ConfigRecord {
987
+ return isRecord(value) ? value : {};
988
+ }
989
+
990
+ function resolvedValue(
991
+ canonical: ConfigRecord,
992
+ canonicalKey: string,
993
+ legacy?: ConfigRecord,
994
+ legacyKey = canonicalKey,
995
+ ): unknown {
996
+ if (hasOwn(canonical, canonicalKey)) return canonical[canonicalKey];
997
+ if (legacy && hasOwn(legacy, legacyKey)) return legacy[legacyKey];
998
+ return undefined;
999
+ }
1000
+
1001
+ function parseBoolean(value: unknown, fallback: boolean): boolean {
1002
+ return typeof value === "boolean" ? value : fallback;
1003
+ }
1004
+
1005
+ function parseColorSource(value: unknown, fallback: ColorSource): ColorSource {
1006
+ return value === "theme" || value === "terminal" ? value : fallback;
1007
+ }
1008
+
1009
+ function parseSelectorBorderStyle(value: unknown): SelectorBorderStyle {
1010
+ return value === "zentui" ? value : "zentui";
1011
+ }
1012
+
1013
+ function parseFooterStyle(value: unknown): FooterStyle | undefined {
1014
+ return value === "native" || value === "starship" || value === "hidden" ? value : undefined;
1015
+ }
1016
+
1017
+ function parseNonEmptyString(value: unknown, fallback: string): string {
1018
+ return typeof value === "string" && value.length > 0 ? value : fallback;
1019
+ }
1020
+
1021
+ function resolveContextThresholds(
1022
+ canonical: unknown,
1023
+ legacy: unknown,
1024
+ defaults: ContextThresholds,
1025
+ ): ContextThresholds {
1026
+ const canonicalRecord = recordValue(canonical);
1027
+ const legacyRecord = recordValue(legacy);
1028
+ return parseContextThresholds(
1029
+ {
1030
+ warning: resolvedValue(canonicalRecord, "warning", legacyRecord),
1031
+ error: resolvedValue(canonicalRecord, "error", legacyRecord),
1032
+ },
1033
+ defaults,
1034
+ );
1035
+ }
1036
+
1037
+ function resolvePathDisplay(canonical: unknown, legacy: unknown): PathDisplayConfig {
1038
+ const canonicalRecord = recordValue(canonical);
1039
+ const legacyRecord = recordValue(legacy);
1040
+ return parsePathDisplay({
1041
+ mode: resolvedValue(canonicalRecord, "mode", legacyRecord),
1042
+ depth: resolvedValue(canonicalRecord, "depth", legacyRecord),
1043
+ });
1044
+ }
1045
+
1046
+ function resolveGitBranch(canonical: unknown, legacy: unknown): GitBranchConfig {
1047
+ const canonicalRecord = recordValue(canonical);
1048
+ const legacyRecord = recordValue(legacy);
1049
+ return parseGitBranchConfig({
1050
+ maxLength: resolvedValue(canonicalRecord, "maxLength", legacyRecord),
1051
+ });
1052
+ }
1053
+
1054
+ function resolveGitCommit(canonical: unknown, legacy: unknown): GitCommitConfig {
1055
+ const canonicalRecord = recordValue(canonical);
1056
+ const legacyRecord = recordValue(legacy);
1057
+ return normalizeGitCommitConfig({
1058
+ hashLength: resolvedValue(canonicalRecord, "hashLength", legacyRecord),
1059
+ onlyDetached: resolvedValue(canonicalRecord, "onlyDetached", legacyRecord),
1060
+ showTag: resolvedValue(canonicalRecord, "showTag", legacyRecord),
1061
+ });
1062
+ }
1063
+
1064
+ function resolveGitMetrics(canonical: unknown, legacy: unknown): GitMetricsConfig {
1065
+ const canonicalRecord = recordValue(canonical);
1066
+ const legacyRecord = recordValue(legacy);
1067
+ return normalizeGitMetricsConfig({
1068
+ onlyNonzero: resolvedValue(canonicalRecord, "onlyNonzero", legacyRecord),
1069
+ ignoreSubmodules: resolvedValue(canonicalRecord, "ignoreSubmodules", legacyRecord),
1070
+ });
1071
+ }
1072
+
1073
+ function resolveExtensionStatuses(canonical: unknown, legacy: unknown): ExtensionStatusesConfig {
1074
+ const canonicalRecord = recordValue(canonical);
1075
+ const legacyRecord = recordValue(legacy);
1076
+ return normalizeExtensionStatuses({
1077
+ defaultPlacement: resolvedValue(canonicalRecord, "defaultPlacement", legacyRecord),
1078
+ placements: resolvedValue(canonicalRecord, "placements", legacyRecord),
1079
+ colorModes: resolvedValue(canonicalRecord, "colorModes", legacyRecord),
1080
+ });
1081
+ }
1082
+
1083
+ const FOOTER_SEGMENT_KEYS = Object.keys(defaultFooterSegments) as Array<keyof FooterSegmentsConfig>;
1084
+
1085
+ function resolveFooterSegments(canonical: unknown, legacy: unknown): FooterSegmentsConfig {
1086
+ const canonicalRecord = recordValue(canonical);
1087
+ const legacyRecord = recordValue(legacy);
1088
+ return Object.fromEntries(
1089
+ FOOTER_SEGMENT_KEYS.map((key) => [
1090
+ key,
1091
+ parseBoolean(resolvedValue(canonicalRecord, key, legacyRecord), defaultFooterSegments[key]),
1092
+ ]),
1093
+ ) as FooterSegmentsConfig;
1094
+ }
1095
+
1096
+ function legacyCopyFriendly(record: ConfigRecord): boolean {
1097
+ return record.copyFriendly === true;
1098
+ }
1099
+
1100
+ function resolveEditorStyle(
1101
+ editor: ConfigRecord,
1102
+ polished: ConfigRecord,
1103
+ features: ConfigRecord,
1104
+ ): EditorStyle {
1105
+ const rawStyle = editor.style;
1106
+ if (
1107
+ rawStyle === "opencode" ||
1108
+ rawStyle === "opencode-copy-friendly" ||
1109
+ rawStyle === "accent-rail" ||
1110
+ rawStyle === "minimalist"
1111
+ ) {
1112
+ return rawStyle;
1113
+ }
1114
+ if (rawStyle === "polished-copy-friendly") return "opencode-copy-friendly";
1115
+ if (rawStyle === "polished") {
1116
+ return hasOwn(polished, "copyFriendly") && legacyCopyFriendly(polished)
1117
+ ? "opencode-copy-friendly"
1118
+ : "opencode";
1119
+ }
1120
+ if (hasOwn(polished, "copyFriendly")) {
1121
+ return legacyCopyFriendly(polished) ? "opencode-copy-friendly" : "opencode";
1122
+ }
1123
+ if (legacyCopyFriendly(features)) return "opencode-copy-friendly";
1124
+ return defaultComponents.editor.style;
1125
+ }
1126
+
1127
+ function resolveFooterStyle(footer: ConfigRecord, features: ConfigRecord): FooterStyle {
1128
+ const explicit = parseFooterStyle(footer.style);
1129
+ if (explicit) return explicit;
1130
+ if (typeof footer.enabled === "boolean") return footer.enabled ? "starship" : "native";
1131
+ if (typeof features.statusLine === "boolean") {
1132
+ return features.statusLine ? "starship" : "native";
1133
+ }
1134
+ return defaultComponents.footer.style;
1135
+ }
1136
+
1137
+ function resolveUserMessagesSelection(
1138
+ userMessages: ConfigRecord,
1139
+ framed: ConfigRecord,
1140
+ features: ConfigRecord,
1141
+ ): Pick<UserMessagesComponentConfig, "enabled" | "style"> {
1142
+ const normalEnabled = parseBoolean(
1143
+ resolvedValue(userMessages, "enabled", features, "editor"),
1144
+ defaultComponents.userMessages.enabled,
1145
+ );
1146
+ const rawStyle = userMessages.style;
1147
+ if (rawStyle === "compact" || rawStyle === "labeled" || rawStyle === "framed-copy-friendly") {
1148
+ return { style: rawStyle, enabled: normalEnabled };
1149
+ }
1150
+ if (rawStyle === "framed" || hasOwn(framed, "copyFriendly")) {
1151
+ return {
1152
+ style: legacyCopyFriendly(framed) ? "framed-copy-friendly" : "framed",
1153
+ enabled: normalEnabled,
1154
+ };
1155
+ }
1156
+ return {
1157
+ style: legacyCopyFriendly(features) ? "framed-copy-friendly" : defaultComponents.userMessages.style,
1158
+ enabled: normalEnabled,
1159
+ };
1160
+ }
1161
+
1162
+ function resolveWorkingLineMessages(messages: ConfigRecord): WorkingLineMessagesConfig {
1163
+ const preset = () => [...PI_WORKING_LINE_MESSAGES];
1164
+ const hasCanonicalCustom = hasOwn(messages, "custom");
1165
+ const custom = hasCanonicalCustom
1166
+ ? parseBoolean(messages.custom, true)
1167
+ : messages.mode !== "native";
1168
+ if (hasCanonicalCustom) {
1169
+ return {
1170
+ custom,
1171
+ values: hasOwn(messages, "values") ? normalizeWorkingLineMessages(messages.values) : preset(),
1172
+ };
1173
+ }
1174
+ const legacyValues = normalizeWorkingLineMessages(messages.values);
1175
+ if (messages.mode === "append") {
1176
+ return { custom, values: normalizeWorkingLineMessages([...preset(), ...legacyValues]) };
1177
+ }
1178
+ if (messages.mode === "replace" || messages.mode === "native") {
1179
+ return { custom, values: legacyValues.length > 0 ? legacyValues : preset() };
1180
+ }
1181
+ return { custom, values: hasOwn(messages, "values") ? legacyValues : preset() };
1182
+ }
1183
+
1184
+ function resolveComponents(config: ConfigRecord): ComponentsConfig {
1185
+ const components = recordValue(config.components);
1186
+ const editor = recordValue(components.editor);
1187
+ const editorStyles = recordValue(editor.styles);
1188
+ const opencode = recordValue(editorStyles.opencode);
1189
+ const polished = recordValue(editorStyles.polished);
1190
+ const opencodeCopyFriendly = recordValue(editorStyles["opencode-copy-friendly"]);
1191
+ const polishedCopyFriendly = recordValue(editorStyles["polished-copy-friendly"]);
1192
+ const accentRail = recordValue(editorStyles["accent-rail"]);
1193
+ const minimalist = recordValue(editorStyles.minimalist);
1194
+ const userMessages = recordValue(components.userMessages);
1195
+ const userMessageStyles = recordValue(userMessages.styles);
1196
+ const framed = recordValue(userMessageStyles.framed);
1197
+ const workingLine = recordValue(components.workingLine);
1198
+ const workingLineMessages = recordValue(workingLine.messages);
1199
+ const workingLineSegments = recordValue(workingLine.segments);
1200
+ const selectorBorders = recordValue(components.selectorBorders);
1201
+ const footer = recordValue(components.footer);
1202
+ const footerStyles = recordValue(footer.styles);
1203
+ const starship = recordValue(footerStyles.starship);
1204
+ const features = recordValue(config.features);
1205
+ const colorSources = recordValue(config.colorSources);
1206
+
1207
+ const minimalistThresholds = resolveContextThresholds(
1208
+ minimalist.contextThresholds,
1209
+ config.contextThresholds,
1210
+ defaultMinimalistStyle.contextThresholds,
1211
+ );
1212
+ const footerThresholds = resolveContextThresholds(
1213
+ starship.contextThresholds,
1214
+ config.contextThresholds,
1215
+ defaultStarshipStyle.contextThresholds,
1216
+ );
1217
+ const compactFormat = resolvedValue(starship, "compactFormat", config, "compactFooterFormat");
1218
+ const metadataFormat = hasOwn(opencode, "metadataFormat")
1219
+ ? opencode.metadataFormat
1220
+ : resolvedValue(polished, "metadataFormat", config, "editorMetadataFormat");
1221
+ const lowRailMetadataFormat = hasOwn(opencodeCopyFriendly, "metadataFormat")
1222
+ ? opencodeCopyFriendly.metadataFormat
1223
+ : hasOwn(polishedCopyFriendly, "metadataFormat")
1224
+ ? polishedCopyFriendly.metadataFormat
1225
+ : metadataFormat;
1226
+ const userMessagesSelection = resolveUserMessagesSelection(userMessages, framed, features);
1227
+
1228
+ return {
1229
+ editor: {
1230
+ enabled: parseBoolean(
1231
+ resolvedValue(editor, "enabled", features, "editor"),
1232
+ defaultComponents.editor.enabled,
1233
+ ),
1234
+ style: resolveEditorStyle(editor, polished, features),
1235
+ colorSource: parseColorSource(
1236
+ resolvedValue(editor, "colorSource", colorSources, "editor"),
1237
+ defaultComponents.editor.colorSource,
1238
+ ),
1239
+ borderColorMode: parseEditorBorderColorMode(
1240
+ resolvedValue(editor, "borderColorMode", config, "editorBorderColorMode"),
1241
+ ),
1242
+ modelLabel: parseEditorModelLabel(
1243
+ resolvedValue(editor, "modelLabel", config, "editorModelLabel"),
1244
+ defaultComponents.editor.modelLabel,
1245
+ ),
1246
+ viewportIndicators: parseBoolean(
1247
+ resolvedValue(editor, "viewportIndicators", features, "viewportIndicators"),
1248
+ defaultComponents.editor.viewportIndicators,
1249
+ ),
1250
+ styles: {
1251
+ opencode: {
1252
+ metadataFormat: parseNonEmptyString(metadataFormat, DEFAULT_EDITOR_METADATA_FORMAT),
1253
+ completionMenu: parseCompletionMenuStyle(opencode.completionMenu),
1254
+ },
1255
+ "opencode-copy-friendly": {
1256
+ metadataFormat: parseNonEmptyString(
1257
+ lowRailMetadataFormat,
1258
+ DEFAULT_EDITOR_METADATA_FORMAT,
1259
+ ),
1260
+ completionMenu: parseCompletionMenuStyle(opencodeCopyFriendly.completionMenu),
1261
+ },
1262
+ "accent-rail": {
1263
+ rail: parseNonEmptyString(accentRail.rail, defaultAccentRailStyle.rail),
1264
+ asciiRail: parseNonEmptyString(accentRail.asciiRail, defaultAccentRailStyle.asciiRail),
1265
+ transparent: parseBoolean(accentRail.transparent, defaultAccentRailStyle.transparent),
1266
+ },
1267
+ minimalist: {
1268
+ pathDisplay:
1269
+ minimalist.pathDisplay === "compact" ||
1270
+ minimalist.pathDisplay === "project" ||
1271
+ minimalist.pathDisplay === "full"
1272
+ ? minimalist.pathDisplay
1273
+ : defaultMinimalistStyle.pathDisplay,
1274
+ contextFormat:
1275
+ minimalist.contextFormat === "percent" || minimalist.contextFormat === "percent-total"
1276
+ ? minimalist.contextFormat
1277
+ : defaultMinimalistStyle.contextFormat,
1278
+ contextGauge: parseBoolean(minimalist.contextGauge, defaultMinimalistStyle.contextGauge),
1279
+ showSessionName: parseBoolean(
1280
+ minimalist.showSessionName,
1281
+ defaultMinimalistStyle.showSessionName,
1282
+ ),
1283
+ showTimer: parseBoolean(minimalist.showTimer, defaultMinimalistStyle.showTimer),
1284
+ showCost: parseBoolean(minimalist.showCost, defaultMinimalistStyle.showCost),
1285
+ showGit: parseBoolean(minimalist.showGit, defaultMinimalistStyle.showGit),
1286
+ contextThresholds: minimalistThresholds,
1287
+ },
1288
+ },
1289
+ },
1290
+ userMessages: {
1291
+ enabled: userMessagesSelection.enabled,
1292
+ style: userMessagesSelection.style,
1293
+ colorSource: parseColorSource(
1294
+ resolvedValue(userMessages, "colorSource", colorSources, "userMessages"),
1295
+ defaultComponents.userMessages.colorSource,
1296
+ ),
1297
+ styles: {
1298
+ framed: {},
1299
+ "framed-copy-friendly": {},
1300
+ compact: {},
1301
+ labeled: {},
1302
+ },
1303
+ },
1304
+ workingLine: {
1305
+ enabled: parseBoolean(workingLine.enabled, defaultComponents.workingLine.enabled),
1306
+ turnSummary: parseBoolean(workingLine.turnSummary, defaultComponents.workingLine.turnSummary),
1307
+ spinner:
1308
+ workingLine.spinner === "braille" ||
1309
+ workingLine.spinner === "star-bloom" ||
1310
+ workingLine.spinner === "pinwheel" ||
1311
+ workingLine.spinner === "claude-inspired" ||
1312
+ workingLine.spinner === "pulse"
1313
+ ? workingLine.spinner
1314
+ : defaultComponents.workingLine.spinner,
1315
+ spinnerIntervalMs: isValidWorkingLineIntervalMs(
1316
+ resolvedValue(workingLine, "spinnerIntervalMs", workingLine, "intervalMs"),
1317
+ )
1318
+ ? (resolvedValue(workingLine, "spinnerIntervalMs", workingLine, "intervalMs") as number)
1319
+ : defaultComponents.workingLine.spinnerIntervalMs,
1320
+ animateSpinnerColor: parseBoolean(
1321
+ workingLine.animateSpinnerColor,
1322
+ defaultComponents.workingLine.animateSpinnerColor,
1323
+ ),
1324
+ textIntervalMs: isValidWorkingLineIntervalMs(workingLine.textIntervalMs)
1325
+ ? workingLine.textIntervalMs
1326
+ : defaultComponents.workingLine.textIntervalMs,
1327
+ textAnimation:
1328
+ workingLine.textAnimation === "classic" ||
1329
+ workingLine.textAnimation === "kitt" ||
1330
+ workingLine.textAnimation === "disabled"
1331
+ ? workingLine.textAnimation
1332
+ : defaultComponents.workingLine.textAnimation,
1333
+ colorSource: parseColorSource(
1334
+ workingLine.colorSource,
1335
+ defaultComponents.workingLine.colorSource,
1336
+ ),
1337
+ messages: resolveWorkingLineMessages(workingLineMessages),
1338
+ segments: {
1339
+ tool: parseBoolean(workingLineSegments.tool, defaultComponents.workingLine.segments.tool),
1340
+ elapsed: parseBoolean(
1341
+ workingLineSegments.elapsed,
1342
+ defaultComponents.workingLine.segments.elapsed,
1343
+ ),
1344
+ thought: parseBoolean(
1345
+ workingLineSegments.thought,
1346
+ defaultComponents.workingLine.segments.thought,
1347
+ ),
1348
+ tokens: parseBoolean(
1349
+ workingLineSegments.tokens,
1350
+ defaultComponents.workingLine.segments.tokens,
1351
+ ),
1352
+ },
1353
+ },
1354
+ selectorBorders: {
1355
+ enabled: parseBoolean(
1356
+ resolvedValue(selectorBorders, "enabled", features, "editor"),
1357
+ defaultComponents.selectorBorders.enabled,
1358
+ ),
1359
+ style: parseSelectorBorderStyle(resolvedValue(selectorBorders, "style")),
1360
+ colorSource: parseColorSource(
1361
+ resolvedValue(selectorBorders, "colorSource", colorSources, "editor"),
1362
+ defaultComponents.selectorBorders.colorSource,
1363
+ ),
1364
+ },
1365
+ footer: {
1366
+ style: resolveFooterStyle(footer, features),
1367
+ colorSource: parseColorSource(
1368
+ resolvedValue(footer, "colorSource", colorSources, "starship"),
1369
+ defaultComponents.footer.colorSource,
1370
+ ),
1371
+ modelLabel: parseEditorModelLabel(
1372
+ resolvedValue(footer, "modelLabel", config, "editorModelLabel"),
1373
+ defaultComponents.footer.modelLabel,
1374
+ ),
1375
+ styles: {
1376
+ starship: {
1377
+ format:
1378
+ typeof resolvedValue(starship, "format", config, "footerFormat") === "string"
1379
+ ? (resolvedValue(starship, "format", config, "footerFormat") as string)
1380
+ : defaultStarshipStyle.format,
1381
+ responsive: parseBoolean(
1382
+ resolvedValue(starship, "responsive", config, "responsiveFooter"),
1383
+ defaultStarshipStyle.responsive,
1384
+ ),
1385
+ compactFormat: parseNonEmptyString(compactFormat, defaultStarshipStyle.compactFormat),
1386
+ compactMaxLines: parseCompactFooterMaxLines(
1387
+ resolvedValue(starship, "compactMaxLines", config, "compactFooterMaxLines"),
1388
+ ),
1389
+ separator: parseSeparatorStyle(resolvedValue(starship, "separator", config, "separator")),
1390
+ contextStyle: parseContextStyle(
1391
+ resolvedValue(starship, "contextStyle", config, "contextStyle"),
1392
+ ),
1393
+ contextThresholds: footerThresholds,
1394
+ pathDisplay: resolvePathDisplay(starship.pathDisplay, config.pathDisplay),
1395
+ segments: resolveFooterSegments(starship.segments, config.footerSegments),
1396
+ gitBranch: resolveGitBranch(starship.gitBranch, config.gitBranch),
1397
+ gitCommit: resolveGitCommit(starship.gitCommit, config.gitCommit),
1398
+ gitMetrics: resolveGitMetrics(starship.gitMetrics, config.gitMetrics),
1399
+ extensionStatuses: resolveExtensionStatuses(
1400
+ starship.extensionStatuses,
1401
+ config.extensionStatuses,
1402
+ ),
1403
+ },
1404
+ },
1405
+ },
1406
+ };
1407
+ }
1408
+
1409
+ function compatibilityView(config: ZentuiConfig): PolishedTuiConfig {
1410
+ const starship = config.components.footer.styles.starship;
1411
+ const minimalist = config.components.editor.styles.minimalist;
1412
+ return {
1413
+ ...config,
1414
+ footerFormat: starship.format,
1415
+ responsiveFooter: starship.responsive,
1416
+ compactFooterFormat: starship.compactFormat,
1417
+ compactFooterMaxLines: starship.compactMaxLines,
1418
+ separator: starship.separator,
1419
+ contextStyle: starship.contextStyle,
1420
+ contextThresholds: starship.contextThresholds,
1421
+ pathDisplay: starship.pathDisplay,
1422
+ gitBranch: starship.gitBranch,
1423
+ footerSegments: starship.segments,
1424
+ gitCommit: starship.gitCommit,
1425
+ gitMetrics: starship.gitMetrics,
1426
+ extensionStatuses: starship.extensionStatuses,
1427
+ editorStyle: config.components.editor.style,
1428
+ editorStyles: { minimalist },
1429
+ editorMetadataFormat: config.components.editor.styles.opencode.metadataFormat,
1430
+ editorBorderColorMode: config.components.editor.borderColorMode,
1431
+ editorModelLabel: config.components.editor.modelLabel,
1432
+ colorSources: {
1433
+ starship: config.components.footer.colorSource,
1434
+ editor: config.components.editor.colorSource,
1435
+ userMessages: config.components.userMessages.colorSource,
1436
+ },
1437
+ features: {
1438
+ editor: config.components.editor.enabled,
1439
+ statusLine: config.components.footer.style === "starship",
1440
+ viewportIndicators: config.components.editor.viewportIndicators,
1441
+ },
1442
+ };
1443
+ }
1444
+
1445
+ const unsupportedComponentStyles = new WeakMap<ZentuiConfig, ReadonlySet<ComponentStyleOwner>>();
1446
+
1447
+ const knownComponentStyleIds: Record<ComponentStyleOwner, ReadonlySet<string>> = {
1448
+ editor: new Set([
1449
+ "opencode",
1450
+ "opencode-copy-friendly",
1451
+ "accent-rail",
1452
+ "minimalist",
1453
+ "polished",
1454
+ "polished-copy-friendly",
1455
+ ]),
1456
+ userMessages: new Set(["framed", "framed-copy-friendly", "compact", "labeled"]),
1457
+ selectorBorders: new Set(["zentui"]),
1458
+ footer: new Set(["native", "starship", "hidden"]),
1459
+ };
1460
+
1461
+ function unsupportedSelectedStyleId(
1462
+ record: ConfigRecord,
1463
+ owner: ComponentStyleOwner,
1464
+ ): string | undefined {
1465
+ const component = recordValue(recordValue(record.components)[owner]);
1466
+ if (!hasOwn(component, "style")) return undefined;
1467
+ const style = component.style;
1468
+ return typeof style === "string" && style.trim() && !knownComponentStyleIds[owner].has(style)
1469
+ ? style
1470
+ : undefined;
1471
+ }
1472
+
1473
+ export function hasUnsupportedComponentStyle(
1474
+ config: ZentuiConfig,
1475
+ owner: ComponentStyleOwner,
1476
+ ): boolean {
1477
+ return unsupportedComponentStyles.get(config)?.has(owner) ?? false;
1478
+ }
1479
+
1480
+ export function parseIconMode(value: unknown): IconMode {
1481
+ return value === "auto" || value === "nerd" || value === "ascii"
1482
+ ? value
1483
+ : defaultConfig.icons.mode;
1484
+ }
1485
+
1486
+ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
1487
+ const config = isRecord(parsed) ? parsed : {};
1488
+ const iconsRecord = recordValue(config.icons);
1489
+ const colorsRecord = recordValue(config.colors);
1490
+ const colors = normalizeColors(colorsRecord);
1491
+ const canonical: ZentuiConfig = {
1492
+ projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
1493
+ icons: resolveConfiguredIcons(
1494
+ parseIconMode(iconsRecord.mode),
1495
+ normalizeIconOverrides(iconsRecord),
1496
+ ),
1497
+ colors: {
1498
+ ...defaultConfig.colors,
1499
+ ...colors,
1500
+ ...(colors.editorGitBranch === undefined && colors.gitBranch !== undefined
1501
+ ? { editorGitBranch: colors.gitBranch }
1502
+ : {}),
1503
+ },
1504
+ components: resolveComponents(config),
1505
+ };
1506
+ const view = compatibilityView(canonical);
1507
+ const unsupported = new Set<ComponentStyleOwner>();
1508
+ for (const owner of ["editor", "userMessages", "selectorBorders", "footer"] as const) {
1509
+ if (unsupportedSelectedStyleId(config, owner) !== undefined) unsupported.add(owner);
1510
+ }
1511
+ if (unsupported.size > 0) unsupportedComponentStyles.set(view, unsupported);
1512
+ return view;
1513
+ }
1514
+
1515
+ export function getExtensionStatusPlacement(
1516
+ config: ZentuiConfig,
1517
+ key: string,
1518
+ ): ExtensionStatusPlacement {
1519
+ const statuses = config.components.footer.styles.starship.extensionStatuses;
1520
+ if (Object.hasOwn(statuses.placements, key)) {
1521
+ const placement = statuses.placements[key];
1522
+ if (isExtensionStatusPlacement(placement)) return placement;
1523
+ }
1524
+ return isExtensionStatusPlacement(statuses.defaultPlacement)
1525
+ ? statuses.defaultPlacement
1526
+ : DEFAULT_EXTENSION_STATUS_PLACEMENT;
1527
+ }
1528
+
1529
+ export function getExtensionStatusColorMode(
1530
+ config: ZentuiConfig,
1531
+ key: string,
1532
+ ): ExtensionStatusColorMode {
1533
+ const colorModes = config.components.footer.styles.starship.extensionStatuses.colorModes;
1534
+ if (Object.hasOwn(colorModes, key)) {
1535
+ const colorMode = colorModes[key];
1536
+ if (isExtensionStatusColorMode(colorMode)) return colorMode;
1537
+ }
1538
+ return DEFAULT_EXTENSION_STATUS_COLOR_MODE;
1539
+ }
1540
+
1541
+ export function loadConfig(): PolishedTuiConfig {
1542
+ try {
1543
+ if (!existsSync(configPath)) return mergeConfig({});
1544
+ return mergeConfig(JSON.parse(readFileSync(configPath, "utf8")));
1545
+ } catch {
1546
+ return mergeConfig({});
1547
+ }
1548
+ }
1549
+
1550
+ function overlayKnown(raw: unknown, known: unknown): unknown {
1551
+ if (!isRecord(known)) return known;
1552
+ const output: ConfigRecord = isRecord(raw) ? { ...raw } : {};
1553
+ for (const [key, value] of Object.entries(known)) {
1554
+ Object.defineProperty(output, key, {
1555
+ value: overlayKnown(output[key], value),
1556
+ enumerable: true,
1557
+ configurable: true,
1558
+ writable: true,
1559
+ });
1560
+ }
1561
+ return output;
1562
+ }
1563
+
1564
+ type PreservedStyleIds = Partial<Record<ComponentStyleOwner, string>>;
1565
+
1566
+ function unknownSelectedStyleIds(record: ConfigRecord): PreservedStyleIds {
1567
+ return {
1568
+ editor: unsupportedSelectedStyleId(record, "editor"),
1569
+ userMessages: unsupportedSelectedStyleId(record, "userMessages"),
1570
+ selectorBorders: unsupportedSelectedStyleId(record, "selectorBorders"),
1571
+ footer: unsupportedSelectedStyleId(record, "footer"),
1572
+ };
1573
+ }
1574
+
1575
+ function restoreUnknownSelectedStyleIds(
1576
+ record: ConfigRecord,
1577
+ preserved: PreservedStyleIds,
1578
+ replacedStyle?: ComponentStyleOwner,
1579
+ ): void {
1580
+ const components = recordValue(record.components);
1581
+ for (const [owner, style] of Object.entries(preserved) as [ComponentStyleOwner, string][]) {
1582
+ if (style === undefined || owner === replacedStyle) continue;
1583
+ const component = recordValue(components[owner]);
1584
+ component.style = style;
1585
+ components[owner] = component;
1586
+ }
1587
+ record.components = components;
1588
+ }
1589
+
1590
+ function saveComponentsMutation(
1591
+ update: (components: ComponentsConfig) => void,
1592
+ path: string,
1593
+ cleanupRaw?: (record: ConfigRecord) => void,
1594
+ replacedStyle?: ComponentStyleOwner,
1595
+ ): PolishedTuiConfig {
1596
+ return mutateConfig(path, (record) => {
1597
+ const preservedStyles = unknownSelectedStyleIds(record);
1598
+ const components = mergeConfig(record).components;
1599
+ update(components);
1600
+ const normalized = resolveComponents({ components });
1601
+ record.components = overlayKnown(record.components, normalized);
1602
+ restoreUnknownSelectedStyleIds(record, preservedStyles, replacedStyle);
1603
+ cleanupRaw?.(record);
1604
+ });
1605
+ }
1606
+
1607
+ function deleteLegacyEditorCopyFriendly(record: ConfigRecord): void {
1608
+ const components = record.components;
1609
+ if (!isRecord(components)) return;
1610
+ const editor = components.editor;
1611
+ if (!isRecord(editor)) return;
1612
+ const styles = editor.styles;
1613
+ if (!isRecord(styles)) return;
1614
+ const polished = styles.polished;
1615
+ if (isRecord(polished)) delete polished.copyFriendly;
1616
+ }
1617
+
1618
+ function deleteLegacyFooterEnabled(record: ConfigRecord): void {
1619
+ const components = record.components;
1620
+ if (!isRecord(components)) return;
1621
+ const footer = components.footer;
1622
+ if (isRecord(footer)) delete footer.enabled;
1623
+ }
1624
+
1625
+ function deleteLegacyMessageCopyFriendly(record: ConfigRecord): void {
1626
+ const components = record.components;
1627
+ if (!isRecord(components)) return;
1628
+ const userMessages = components.userMessages;
1629
+ if (!isRecord(userMessages)) return;
1630
+ const styles = userMessages.styles;
1631
+ if (!isRecord(styles)) return;
1632
+ const framed = styles.framed;
1633
+ if (isRecord(framed)) delete framed.copyFriendly;
1634
+ }
1635
+
1636
+ function applyEditorComponentPatch(
1637
+ component: EditorComponentConfig,
1638
+ patch: Partial<
1639
+ Pick<
1640
+ EditorComponentConfig,
1641
+ "enabled" | "style" | "colorSource" | "borderColorMode" | "modelLabel" | "viewportIndicators"
1642
+ >
1643
+ >,
1644
+ ): void {
1645
+ if (patch.enabled !== undefined) component.enabled = patch.enabled;
1646
+ if (patch.style !== undefined) component.style = patch.style;
1647
+ if (patch.colorSource !== undefined) component.colorSource = patch.colorSource;
1648
+ if (patch.borderColorMode !== undefined) component.borderColorMode = patch.borderColorMode;
1649
+ if (patch.modelLabel !== undefined) component.modelLabel = patch.modelLabel;
1650
+ if (patch.viewportIndicators !== undefined) {
1651
+ component.viewportIndicators = patch.viewportIndicators;
1652
+ }
1653
+ }
1654
+
1655
+ export function saveEditorComponentPatch(
1656
+ patch: Partial<
1657
+ Pick<
1658
+ EditorComponentConfig,
1659
+ "enabled" | "style" | "colorSource" | "borderColorMode" | "modelLabel" | "viewportIndicators"
1660
+ >
1661
+ >,
1662
+ path = configPath,
1663
+ ): PolishedTuiConfig {
1664
+ return saveComponentsMutation(
1665
+ (components) => applyEditorComponentPatch(components.editor, patch),
1666
+ path,
1667
+ patch.style !== undefined ? deleteLegacyEditorCopyFriendly : undefined,
1668
+ patch.style !== undefined ? "editor" : undefined,
1669
+ );
1670
+ }
1671
+
1672
+ export function savePolishedEditorStylePatch(
1673
+ patch: Partial<PolishedEditorStyleConfig>,
1674
+ path = configPath,
1675
+ ): PolishedTuiConfig {
1676
+ return saveComponentsMutation((components) => {
1677
+ const style = components.editor.styles.opencode;
1678
+ if (patch.metadataFormat !== undefined) style.metadataFormat = patch.metadataFormat;
1679
+ if (patch.completionMenu !== undefined) style.completionMenu = patch.completionMenu;
1680
+ }, path);
1681
+ }
1682
+
1683
+ export function savePolishedCopyFriendlyEditorStylePatch(
1684
+ patch: Partial<PolishedCopyFriendlyEditorStyleConfig>,
1685
+ path = configPath,
1686
+ ): PolishedTuiConfig {
1687
+ return saveComponentsMutation((components) => {
1688
+ const style = components.editor.styles["opencode-copy-friendly"];
1689
+ if (patch.metadataFormat !== undefined) style.metadataFormat = patch.metadataFormat;
1690
+ if (patch.completionMenu !== undefined) style.completionMenu = patch.completionMenu;
1691
+ }, path);
1692
+ }
1693
+
1694
+ export function saveAccentRailEditorStylePatch(
1695
+ patch: Partial<AccentRailEditorStyleConfig>,
1696
+ path = configPath,
1697
+ ): PolishedTuiConfig {
1698
+ return saveComponentsMutation((components) => {
1699
+ const style = components.editor.styles["accent-rail"];
1700
+ if (patch.rail !== undefined) style.rail = patch.rail;
1701
+ if (patch.asciiRail !== undefined) style.asciiRail = patch.asciiRail;
1702
+ if (patch.transparent !== undefined) style.transparent = patch.transparent;
1703
+ }, path);
1704
+ }
1705
+
1706
+ function applyMinimalistStylePatch(
1707
+ style: MinimalistEditorStyleConfig,
1708
+ patch: Partial<MinimalistEditorStyleConfig>,
1709
+ ): void {
1710
+ if (patch.pathDisplay !== undefined) style.pathDisplay = patch.pathDisplay;
1711
+ if (patch.contextFormat !== undefined) style.contextFormat = patch.contextFormat;
1712
+ if (patch.contextGauge !== undefined) style.contextGauge = patch.contextGauge;
1713
+ if (patch.showSessionName !== undefined) style.showSessionName = patch.showSessionName;
1714
+ if (patch.showTimer !== undefined) style.showTimer = patch.showTimer;
1715
+ if (patch.showCost !== undefined) style.showCost = patch.showCost;
1716
+ if (patch.showGit !== undefined) style.showGit = patch.showGit;
1717
+ if (patch.contextThresholds !== undefined) {
1718
+ style.contextThresholds = { ...style.contextThresholds, ...patch.contextThresholds };
1719
+ }
1720
+ }
1721
+
1722
+ export function saveMinimalistEditorStylePatch(
1723
+ patch: Partial<MinimalistEditorStyleConfig>,
1724
+ path = configPath,
1725
+ ): PolishedTuiConfig {
1726
+ return saveComponentsMutation(
1727
+ (components) => applyMinimalistStylePatch(components.editor.styles.minimalist, patch),
1728
+ path,
1729
+ );
1730
+ }
1731
+
1732
+ export function saveUserMessagesComponentPatch(
1733
+ patch: Partial<Pick<UserMessagesComponentConfig, "enabled" | "style" | "colorSource">>,
1734
+ path = configPath,
1735
+ ): PolishedTuiConfig {
1736
+ return saveComponentsMutation(
1737
+ (components) => {
1738
+ const component = components.userMessages;
1739
+ if (patch.enabled !== undefined) component.enabled = patch.enabled;
1740
+ if (patch.style !== undefined) component.style = patch.style;
1741
+ if (patch.colorSource !== undefined) component.colorSource = patch.colorSource;
1742
+ },
1743
+ path,
1744
+ patch.style !== undefined ? deleteLegacyMessageCopyFriendly : undefined,
1745
+ patch.style !== undefined ? "userMessages" : undefined,
1746
+ );
1747
+ }
1748
+
1749
+ export function saveWorkingLineComponentPatch(
1750
+ patch: WorkingLineComponentPatch,
1751
+ path = configPath,
1752
+ ): PolishedTuiConfig {
1753
+ return saveComponentsMutation(
1754
+ (components) => {
1755
+ const component = components.workingLine;
1756
+ if (patch.enabled !== undefined) component.enabled = patch.enabled;
1757
+ if (patch.turnSummary !== undefined) component.turnSummary = patch.turnSummary;
1758
+ if (patch.spinner !== undefined) component.spinner = patch.spinner;
1759
+ if (patch.spinnerIntervalMs !== undefined)
1760
+ component.spinnerIntervalMs = patch.spinnerIntervalMs;
1761
+ if (patch.animateSpinnerColor !== undefined)
1762
+ component.animateSpinnerColor = patch.animateSpinnerColor;
1763
+ if (patch.textIntervalMs !== undefined) component.textIntervalMs = patch.textIntervalMs;
1764
+ if (patch.textAnimation !== undefined) component.textAnimation = patch.textAnimation;
1765
+ if (patch.colorSource !== undefined) component.colorSource = patch.colorSource;
1766
+ if (patch.messages?.custom !== undefined) component.messages.custom = patch.messages.custom;
1767
+ if (patch.messages?.values !== undefined) {
1768
+ component.messages.values = normalizeWorkingLineMessages([...patch.messages.values]);
1769
+ }
1770
+ if (patch.segments?.tool !== undefined) component.segments.tool = patch.segments.tool;
1771
+ if (patch.segments?.elapsed !== undefined)
1772
+ component.segments.elapsed = patch.segments.elapsed;
1773
+ if (patch.segments?.thought !== undefined)
1774
+ component.segments.thought = patch.segments.thought;
1775
+ if (patch.segments?.tokens !== undefined) component.segments.tokens = patch.segments.tokens;
1776
+ },
1777
+ path,
1778
+ (record) => {
1779
+ const workingLine = recordValue(recordValue(record.components).workingLine);
1780
+ delete workingLine.intervalMs;
1781
+ const messages = recordValue(workingLine.messages);
1782
+ delete messages.mode;
1783
+ },
1784
+ );
1785
+ }
1786
+
1787
+ export function saveSelectorBordersComponentPatch(
1788
+ patch: Partial<SelectorBordersComponentConfig>,
1789
+ path = configPath,
1790
+ ): PolishedTuiConfig {
1791
+ return saveComponentsMutation(
1792
+ (components) => {
1793
+ const component = components.selectorBorders;
1794
+ if (patch.enabled !== undefined) component.enabled = patch.enabled;
1795
+ if (patch.style !== undefined) component.style = patch.style;
1796
+ if (patch.colorSource !== undefined) component.colorSource = patch.colorSource;
1797
+ },
1798
+ path,
1799
+ undefined,
1800
+ patch.style !== undefined ? "selectorBorders" : undefined,
1801
+ );
1802
+ }
1803
+
1804
+ export function saveFooterComponentPatch(
1805
+ patch: Partial<Pick<FooterComponentConfig, "style" | "colorSource" | "modelLabel">>,
1806
+ path = configPath,
1807
+ ): PolishedTuiConfig {
1808
+ return saveComponentsMutation(
1809
+ (components) => {
1810
+ const component = components.footer;
1811
+ if (patch.style !== undefined) component.style = patch.style;
1812
+ if (patch.colorSource !== undefined) component.colorSource = patch.colorSource;
1813
+ if (patch.modelLabel !== undefined) component.modelLabel = patch.modelLabel;
1814
+ },
1815
+ path,
1816
+ patch.style !== undefined ? deleteLegacyFooterEnabled : undefined,
1817
+ patch.style !== undefined ? "footer" : undefined,
1818
+ );
1819
+ }
1820
+
1821
+ function applyStarshipStylePatch(
1822
+ style: StarshipFooterStyleConfig,
1823
+ patch: Partial<StarshipFooterStyleConfig>,
1824
+ ): void {
1825
+ if (patch.format !== undefined) style.format = patch.format;
1826
+ if (patch.responsive !== undefined) style.responsive = patch.responsive;
1827
+ if (patch.compactFormat !== undefined) style.compactFormat = patch.compactFormat;
1828
+ if (patch.compactMaxLines !== undefined) style.compactMaxLines = patch.compactMaxLines;
1829
+ if (patch.separator !== undefined) style.separator = patch.separator;
1830
+ if (patch.contextStyle !== undefined) style.contextStyle = patch.contextStyle;
1831
+ if (patch.contextThresholds !== undefined) {
1832
+ style.contextThresholds = { ...style.contextThresholds, ...patch.contextThresholds };
1833
+ }
1834
+ if (patch.pathDisplay !== undefined) {
1835
+ style.pathDisplay = { ...style.pathDisplay, ...patch.pathDisplay };
1836
+ }
1837
+ if (patch.segments !== undefined) style.segments = { ...style.segments, ...patch.segments };
1838
+ if (patch.gitBranch !== undefined) style.gitBranch = { ...style.gitBranch, ...patch.gitBranch };
1839
+ if (patch.gitCommit !== undefined) style.gitCommit = { ...style.gitCommit, ...patch.gitCommit };
1840
+ if (patch.gitMetrics !== undefined)
1841
+ style.gitMetrics = { ...style.gitMetrics, ...patch.gitMetrics };
1842
+ if (patch.extensionStatuses !== undefined) {
1843
+ style.extensionStatuses = {
1844
+ ...style.extensionStatuses,
1845
+ ...patch.extensionStatuses,
1846
+ placements: {
1847
+ ...style.extensionStatuses.placements,
1848
+ ...patch.extensionStatuses.placements,
1849
+ },
1850
+ colorModes: {
1851
+ ...style.extensionStatuses.colorModes,
1852
+ ...patch.extensionStatuses.colorModes,
1853
+ },
1854
+ };
1855
+ }
1856
+ }
1857
+
1858
+ export function saveStarshipFooterStylePatch(
1859
+ patch: Partial<StarshipFooterStyleConfig>,
1860
+ path = configPath,
1861
+ ): PolishedTuiConfig {
1862
+ return saveComponentsMutation(
1863
+ (components) => applyStarshipStylePatch(components.footer.styles.starship, patch),
1864
+ path,
1865
+ );
1866
+ }
1867
+
1868
+ export function saveColorSourcesPatch(
1869
+ patch: Partial<ColorSourcesConfig>,
1870
+ path = configPath,
1871
+ ): PolishedTuiConfig {
1872
+ const valid = validColorSourceEntries(patch);
1873
+ return saveComponentsMutation((components) => {
1874
+ if (valid.starship !== undefined) components.footer.colorSource = valid.starship;
1875
+ if (valid.editor !== undefined) {
1876
+ components.editor.colorSource = valid.editor;
1877
+ components.selectorBorders.colorSource = valid.editor;
1878
+ }
1879
+ if (valid.userMessages !== undefined) {
1880
+ components.userMessages.colorSource = valid.userMessages;
1881
+ }
1882
+ }, path);
1883
+ }
1884
+
1885
+ export function saveUiFeaturesPatch(
1886
+ patch: Partial<UiFeaturesConfig>,
1887
+ path = configPath,
1888
+ ): PolishedTuiConfig {
1889
+ const valid = validUiFeatureEntries(patch);
1890
+ return saveComponentsMutation(
1891
+ (components) => {
1892
+ if (valid.editor !== undefined) {
1893
+ components.editor.enabled = valid.editor;
1894
+ components.userMessages.enabled = valid.editor;
1895
+ components.selectorBorders.enabled = valid.editor;
1896
+ }
1897
+ if (valid.statusLine !== undefined) {
1898
+ components.footer.style = valid.statusLine ? "starship" : "native";
1899
+ }
1900
+ if (valid.viewportIndicators !== undefined) {
1901
+ components.editor.viewportIndicators = valid.viewportIndicators;
1902
+ }
1903
+ },
1904
+ path,
1905
+ valid.statusLine !== undefined ? deleteLegacyFooterEnabled : undefined,
1906
+ valid.statusLine !== undefined ? "footer" : undefined,
1907
+ );
1908
+ }
1909
+
1910
+ export function saveFooterSegmentsPatch(
1911
+ patch: Partial<FooterSegmentsConfig>,
1912
+ path = configPath,
1913
+ ): PolishedTuiConfig {
1914
+ return saveStarshipFooterStylePatch(
1915
+ { segments: validFooterSegmentEntries(patch) as FooterSegmentsConfig },
1916
+ path,
1917
+ );
1918
+ }
1919
+
1920
+ export function saveFooterFormatPatch(value: string, path = configPath): PolishedTuiConfig {
1921
+ return saveStarshipFooterStylePatch({ format: typeof value === "string" ? value : "" }, path);
1922
+ }
1923
+
1924
+ export function saveResponsiveFooterPatch(
1925
+ patch: Partial<
1926
+ Pick<PolishedTuiConfig, "responsiveFooter" | "compactFooterFormat" | "compactFooterMaxLines">
1927
+ >,
1928
+ path = configPath,
1929
+ ): PolishedTuiConfig {
1930
+ const canonical: Partial<StarshipFooterStyleConfig> = {};
1931
+ if (typeof patch.responsiveFooter === "boolean") canonical.responsive = patch.responsiveFooter;
1932
+ if (typeof patch.compactFooterFormat === "string")
1933
+ canonical.compactFormat = patch.compactFooterFormat;
1934
+ if (patch.compactFooterMaxLines !== undefined) {
1935
+ canonical.compactMaxLines = parseCompactFooterMaxLines(patch.compactFooterMaxLines);
1936
+ }
1937
+ return saveStarshipFooterStylePatch(canonical, path);
1938
+ }
1939
+
1940
+ export function saveIconsModePatch(mode: IconMode, path = configPath): PolishedTuiConfig {
1941
+ return mutateConfig(path, (record) => {
1942
+ const existing = isRecord(record.icons) ? { ...record.icons } : {};
1943
+ record.icons = { ...existing, mode: normalizeIconMode(mode) };
1944
+ });
1945
+ }
1946
+
1947
+ export function saveContextStylePatch(style: ContextStyle, path = configPath): PolishedTuiConfig {
1948
+ return saveStarshipFooterStylePatch({ contextStyle: parseContextStyle(style) }, path);
1949
+ }
1950
+
1951
+ export function saveSeparatorPatch(
1952
+ separator: SeparatorStyle,
1953
+ path = configPath,
1954
+ ): PolishedTuiConfig {
1955
+ return saveStarshipFooterStylePatch({ separator: parseSeparatorStyle(separator) }, path);
1956
+ }
1957
+
1958
+ export function saveContextThresholdsPatch(
1959
+ thresholds: Partial<ContextThresholds>,
1960
+ path = configPath,
1961
+ ): PolishedTuiConfig {
1962
+ if (typeof thresholds.warning === "bigint" || typeof thresholds.error === "bigint") {
1963
+ throw new TypeError("Context thresholds must be JSON-serializable numbers");
1964
+ }
1965
+ return saveComponentsMutation((components) => {
1966
+ components.editor.styles.minimalist.contextThresholds = {
1967
+ ...components.editor.styles.minimalist.contextThresholds,
1968
+ ...thresholds,
1969
+ };
1970
+ components.footer.styles.starship.contextThresholds = {
1971
+ ...components.footer.styles.starship.contextThresholds,
1972
+ ...thresholds,
1973
+ };
1974
+ }, path);
1975
+ }
1976
+
1977
+ export function savePathDisplayPatch(
1978
+ patch: Partial<PathDisplayConfig>,
1979
+ path = configPath,
1980
+ ): PolishedTuiConfig {
1981
+ return saveComponentsMutation((components) => {
1982
+ components.footer.styles.starship.pathDisplay = {
1983
+ ...components.footer.styles.starship.pathDisplay,
1984
+ ...patch,
1985
+ };
1986
+ }, path);
1987
+ }
1988
+
1989
+ export function saveGitBranchPatch(
1990
+ patch: Partial<GitBranchConfig>,
1991
+ path = configPath,
1992
+ ): PolishedTuiConfig {
1993
+ return saveComponentsMutation((components) => {
1994
+ components.footer.styles.starship.gitBranch = {
1995
+ ...components.footer.styles.starship.gitBranch,
1996
+ ...patch,
1997
+ };
1998
+ }, path);
1999
+ }
2000
+
2001
+ export function saveEditorModelLabel(
2002
+ value: ModelLabelSource,
2003
+ path = configPath,
2004
+ ): PolishedTuiConfig {
2005
+ return saveComponentsMutation((components) => {
2006
+ const normalized = parseEditorModelLabel(value);
2007
+ components.editor.modelLabel = normalized;
2008
+ components.footer.modelLabel = normalized;
2009
+ }, path);
2010
+ }
2011
+
2012
+ export function saveEditorStyle(value: EditorStyle, path = configPath): PolishedTuiConfig {
2013
+ return saveEditorComponentPatch({ style: parseEditorStyle(value) }, path);
2014
+ }
2015
+
2016
+ export function saveMinimalistPatch(
2017
+ patch: Partial<MinimalistConfig>,
2018
+ path = configPath,
2019
+ ): PolishedTuiConfig {
2020
+ return saveMinimalistEditorStylePatch(patch, path);
2021
+ }
2022
+
2023
+ export function saveEditorBorderColorMode(
2024
+ value: EditorBorderColorMode,
2025
+ path = configPath,
2026
+ ): PolishedTuiConfig {
2027
+ return saveEditorComponentPatch({ borderColorMode: parseEditorBorderColorMode(value) }, path);
2028
+ }
2029
+
2030
+ export function saveGitCommitPatch(
2031
+ patch: Partial<Pick<GitCommitConfig, "onlyDetached" | "showTag">>,
2032
+ path = configPath,
2033
+ ): PolishedTuiConfig {
2034
+ const valid: Partial<GitCommitConfig> = {};
2035
+ if (typeof patch.onlyDetached === "boolean") valid.onlyDetached = patch.onlyDetached;
2036
+ if (typeof patch.showTag === "boolean") valid.showTag = patch.showTag;
2037
+ return saveComponentsMutation((components) => {
2038
+ components.footer.styles.starship.gitCommit = {
2039
+ ...components.footer.styles.starship.gitCommit,
2040
+ ...valid,
2041
+ };
2042
+ }, path);
2043
+ }
2044
+
2045
+ export function saveGitMetricsPatch(
2046
+ patch: Partial<GitMetricsConfig>,
2047
+ path = configPath,
2048
+ ): PolishedTuiConfig {
2049
+ const valid: Partial<GitMetricsConfig> = {};
2050
+ if (typeof patch.onlyNonzero === "boolean") valid.onlyNonzero = patch.onlyNonzero;
2051
+ if (typeof patch.ignoreSubmodules === "boolean") valid.ignoreSubmodules = patch.ignoreSubmodules;
2052
+ return saveComponentsMutation((components) => {
2053
+ components.footer.styles.starship.gitMetrics = {
2054
+ ...components.footer.styles.starship.gitMetrics,
2055
+ ...valid,
2056
+ };
2057
+ }, path);
2058
+ }
2059
+
2060
+ export function saveExtensionStatusDefaultPlacement(
2061
+ placement: ExtensionStatusPlacement,
2062
+ path = configPath,
2063
+ ): PolishedTuiConfig {
2064
+ return saveComponentsMutation((components) => {
2065
+ components.footer.styles.starship.extensionStatuses.defaultPlacement =
2066
+ isExtensionStatusPlacement(placement)
2067
+ ? placement
2068
+ : defaultConfig.extensionStatuses.defaultPlacement;
2069
+ }, path);
2070
+ }
2071
+
2072
+ export function saveExtensionStatusPlacement(
2073
+ key: string,
2074
+ placement: ExtensionStatusPlacement,
2075
+ path = configPath,
2076
+ ): PolishedTuiConfig {
2077
+ return saveComponentsMutation((components) => {
2078
+ Object.defineProperty(components.footer.styles.starship.extensionStatuses.placements, key, {
2079
+ value: placement,
2080
+ enumerable: true,
2081
+ configurable: true,
2082
+ writable: true,
2083
+ });
2084
+ }, path);
2085
+ }
2086
+
2087
+ export function saveExtensionStatusColorMode(
2088
+ key: string,
2089
+ colorMode: ExtensionStatusColorMode,
2090
+ path = configPath,
2091
+ ): PolishedTuiConfig {
2092
+ return saveComponentsMutation((components) => {
2093
+ Object.defineProperty(components.footer.styles.starship.extensionStatuses.colorModes, key, {
2094
+ value: colorMode,
2095
+ enumerable: true,
2096
+ configurable: true,
2097
+ writable: true,
2098
+ });
2099
+ }, path);
2100
+ }