@agi-fans/dsh-tui 0.21.0 → 0.22.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 (243) hide show
  1. package/lib/CHANGELOG.md +158 -1
  2. package/lib/chrome/box.d.ts +47 -16
  3. package/lib/chrome/box.js +172 -130
  4. package/lib/chrome/box.js.map +1 -1
  5. package/lib/chrome/code-highlight.d.ts +15 -0
  6. package/lib/chrome/code-highlight.js +60 -4
  7. package/lib/chrome/code-highlight.js.map +1 -1
  8. package/lib/chrome/command-output.d.ts +29 -2
  9. package/lib/chrome/command-output.js +100 -4
  10. package/lib/chrome/command-output.js.map +1 -1
  11. package/lib/chrome/main-screen-renderer.d.ts +16 -0
  12. package/lib/chrome/main-screen-renderer.js +53 -5
  13. package/lib/chrome/main-screen-renderer.js.map +1 -1
  14. package/lib/chrome/markdown.js +30 -10
  15. package/lib/chrome/markdown.js.map +1 -1
  16. package/lib/chrome/relative-time.d.ts +17 -0
  17. package/lib/chrome/relative-time.js +36 -0
  18. package/lib/chrome/relative-time.js.map +1 -1
  19. package/lib/chrome/secret-mask.d.ts +35 -0
  20. package/lib/chrome/secret-mask.js +180 -0
  21. package/lib/chrome/secret-mask.js.map +1 -0
  22. package/lib/chrome/status-config.d.ts +161 -51
  23. package/lib/chrome/status-config.js +237 -126
  24. package/lib/chrome/status-config.js.map +1 -1
  25. package/lib/chrome/status-line.d.ts +3 -2
  26. package/lib/chrome/status-line.js +105 -63
  27. package/lib/chrome/status-line.js.map +1 -1
  28. package/lib/chrome/terminal-colors.d.ts +85 -0
  29. package/lib/chrome/terminal-colors.js +230 -0
  30. package/lib/chrome/terminal-colors.js.map +1 -0
  31. package/lib/chrome/terminal-images.d.ts +9 -1
  32. package/lib/chrome/terminal-images.js +27 -4
  33. package/lib/chrome/terminal-images.js.map +1 -1
  34. package/lib/chrome/theme.d.ts +32 -9
  35. package/lib/chrome/theme.js +595 -498
  36. package/lib/chrome/theme.js.map +1 -1
  37. package/lib/chrome/tool-renderers.js +69 -13
  38. package/lib/chrome/tool-renderers.js.map +1 -1
  39. package/lib/chrome/tools-list.d.ts +1 -4
  40. package/lib/chrome/tools-list.js +1 -41
  41. package/lib/chrome/tools-list.js.map +1 -1
  42. package/lib/chrome/welcome-tips.d.ts +20 -8
  43. package/lib/chrome/welcome-tips.js +22 -42
  44. package/lib/chrome/welcome-tips.js.map +1 -1
  45. package/lib/commands/auth-ids.d.ts +25 -0
  46. package/lib/commands/auth-ids.js +27 -0
  47. package/lib/commands/auth-ids.js.map +1 -0
  48. package/lib/commands/auth-input.d.ts +34 -0
  49. package/lib/commands/auth-input.js +96 -0
  50. package/lib/commands/auth-input.js.map +1 -0
  51. package/lib/commands/auth-providers.d.ts +39 -0
  52. package/lib/commands/auth-providers.js +125 -0
  53. package/lib/commands/auth-providers.js.map +1 -0
  54. package/lib/commands/auth.d.ts +12 -20
  55. package/lib/commands/auth.js +42 -229
  56. package/lib/commands/auth.js.map +1 -1
  57. package/lib/commands/btw.d.ts +13 -1
  58. package/lib/commands/btw.js +37 -104
  59. package/lib/commands/btw.js.map +1 -1
  60. package/lib/commands/diff.js +1 -96
  61. package/lib/commands/diff.js.map +1 -1
  62. package/lib/commands/export.d.ts +1 -1
  63. package/lib/commands/export.js +3 -27
  64. package/lib/commands/export.js.map +1 -1
  65. package/lib/commands/files.d.ts +17 -1
  66. package/lib/commands/files.js +67 -107
  67. package/lib/commands/files.js.map +1 -1
  68. package/lib/commands/jobs.d.ts +19 -4
  69. package/lib/commands/jobs.js +50 -7
  70. package/lib/commands/jobs.js.map +1 -1
  71. package/lib/commands/model.js +108 -76
  72. package/lib/commands/model.js.map +1 -1
  73. package/lib/commands/permission.js +15 -16
  74. package/lib/commands/permission.js.map +1 -1
  75. package/lib/commands/plugins.js +133 -16
  76. package/lib/commands/plugins.js.map +1 -1
  77. package/lib/commands/schedule.d.ts +10 -0
  78. package/lib/commands/schedule.js +45 -14
  79. package/lib/commands/schedule.js.map +1 -1
  80. package/lib/commands/session-configuration.js +2 -10
  81. package/lib/commands/session-configuration.js.map +1 -1
  82. package/lib/commands/session.js +14 -38
  83. package/lib/commands/session.js.map +1 -1
  84. package/lib/commands/terminal.d.ts +14 -8
  85. package/lib/commands/terminal.js +19 -178
  86. package/lib/commands/terminal.js.map +1 -1
  87. package/lib/commands/trajectory.js +4 -1
  88. package/lib/commands/trajectory.js.map +1 -1
  89. package/lib/definition.d.ts +122 -22
  90. package/lib/definition.js.map +1 -1
  91. package/lib/index.d.ts +1 -0
  92. package/lib/index.js +1 -0
  93. package/lib/index.js.map +1 -1
  94. package/lib/input/actions.d.ts +51 -0
  95. package/lib/input/actions.js +120 -0
  96. package/lib/input/actions.js.map +1 -0
  97. package/lib/input/editor.d.ts +0 -10
  98. package/lib/input/editor.js +11 -9
  99. package/lib/input/editor.js.map +1 -1
  100. package/lib/input/image-paste.d.ts +19 -1
  101. package/lib/input/image-paste.js +82 -1
  102. package/lib/input/image-paste.js.map +1 -1
  103. package/lib/input/keybindings-config.d.ts +12 -5
  104. package/lib/input/keybindings-config.js +30 -57
  105. package/lib/input/keybindings-config.js.map +1 -1
  106. package/lib/input/keys.d.ts +16 -0
  107. package/lib/input/keys.js +38 -3
  108. package/lib/input/keys.js.map +1 -1
  109. package/lib/input/wheel-scroll.d.ts +46 -0
  110. package/lib/input/wheel-scroll.js +69 -0
  111. package/lib/input/wheel-scroll.js.map +1 -0
  112. package/lib/prompt-intents.d.ts +182 -0
  113. package/lib/prompt-intents.js +127 -0
  114. package/lib/prompt-intents.js.map +1 -0
  115. package/lib/runtime/cancellable.d.ts +75 -0
  116. package/lib/runtime/cancellable.js +76 -0
  117. package/lib/runtime/cancellable.js.map +1 -0
  118. package/lib/runtime/composer-images.js +5 -4
  119. package/lib/runtime/composer-images.js.map +1 -1
  120. package/lib/runtime/composer-shell.d.ts +17 -2
  121. package/lib/runtime/composer-shell.js +109 -43
  122. package/lib/runtime/composer-shell.js.map +1 -1
  123. package/lib/runtime/file-recovery-review.d.ts +0 -1
  124. package/lib/runtime/file-recovery-review.js +29 -30
  125. package/lib/runtime/file-recovery-review.js.map +1 -1
  126. package/lib/runtime/file-recovery-store.js +5 -1
  127. package/lib/runtime/file-recovery-store.js.map +1 -1
  128. package/lib/runtime/file-review.js +1 -1
  129. package/lib/runtime/file-review.js.map +1 -1
  130. package/lib/runtime/hook-settings.d.ts +2 -2
  131. package/lib/runtime/plain-tui.d.ts +2 -0
  132. package/lib/runtime/plain-tui.js +7 -1
  133. package/lib/runtime/plain-tui.js.map +1 -1
  134. package/lib/runtime/plugin-settings.d.ts +1 -1
  135. package/lib/runtime/plugin-settings.js +10 -8
  136. package/lib/runtime/plugin-settings.js.map +1 -1
  137. package/lib/runtime/provider-local.d.ts +57 -15
  138. package/lib/runtime/provider-local.js +1112 -471
  139. package/lib/runtime/provider-local.js.map +1 -1
  140. package/lib/runtime/runner.js +5 -11
  141. package/lib/runtime/runner.js.map +1 -1
  142. package/lib/runtime/settings-preferences.js +1 -1
  143. package/lib/runtime/settings-preferences.js.map +1 -1
  144. package/lib/runtime/startup-notices.js +2 -0
  145. package/lib/runtime/startup-notices.js.map +1 -1
  146. package/lib/runtime/terminal-modes.d.ts +47 -0
  147. package/lib/runtime/terminal-modes.js +86 -0
  148. package/lib/runtime/terminal-modes.js.map +1 -0
  149. package/lib/runtime/terminal-notifications.d.ts +3 -5
  150. package/lib/runtime/terminal-notifications.js +4 -8
  151. package/lib/runtime/terminal-notifications.js.map +1 -1
  152. package/lib/runtime/terminal-probe.d.ts +36 -0
  153. package/lib/runtime/terminal-probe.js +37 -0
  154. package/lib/runtime/terminal-probe.js.map +1 -0
  155. package/lib/runtime/terminal-session.d.ts +57 -0
  156. package/lib/runtime/terminal-session.js +164 -0
  157. package/lib/runtime/terminal-session.js.map +1 -0
  158. package/lib/session/interaction-adapter.js +7 -8
  159. package/lib/session/interaction-adapter.js.map +1 -1
  160. package/lib/session/message-queue.js +1 -1
  161. package/lib/session/message-queue.js.map +1 -1
  162. package/lib/session/package-metadata.d.ts +8 -0
  163. package/lib/session/package-metadata.js +8 -0
  164. package/lib/session/package-metadata.js.map +1 -1
  165. package/lib/session/session-controller.d.ts +10 -11
  166. package/lib/session/session-controller.js +95 -13
  167. package/lib/session/session-controller.js.map +1 -1
  168. package/lib/session/session-tree.d.ts +2 -0
  169. package/lib/session/session-tree.js +2 -0
  170. package/lib/session/session-tree.js.map +1 -1
  171. package/lib/session/tui-settings.d.ts +40 -11
  172. package/lib/session/tui-settings.js +73 -34
  173. package/lib/session/tui-settings.js.map +1 -1
  174. package/lib/views/autocomplete.d.ts +18 -2
  175. package/lib/views/autocomplete.js +51 -38
  176. package/lib/views/autocomplete.js.map +1 -1
  177. package/lib/views/copy-targets.d.ts +12 -13
  178. package/lib/views/copy-targets.js +16 -46
  179. package/lib/views/copy-targets.js.map +1 -1
  180. package/lib/views/document-reader.js +18 -5
  181. package/lib/views/document-reader.js.map +1 -1
  182. package/lib/views/event-views.js +42 -6
  183. package/lib/views/event-views.js.map +1 -1
  184. package/lib/views/history-search.js +15 -21
  185. package/lib/views/history-search.js.map +1 -1
  186. package/lib/views/hotkeys.d.ts +7 -1
  187. package/lib/views/hotkeys.js +49 -25
  188. package/lib/views/hotkeys.js.map +1 -1
  189. package/lib/views/list-navigation.d.ts +62 -0
  190. package/lib/views/list-navigation.js +73 -0
  191. package/lib/views/list-navigation.js.map +1 -0
  192. package/lib/views/list-view.d.ts +99 -0
  193. package/lib/views/list-view.js +165 -0
  194. package/lib/views/list-view.js.map +1 -0
  195. package/lib/views/prompt-selector.d.ts +31 -3
  196. package/lib/views/prompt-selector.js +249 -125
  197. package/lib/views/prompt-selector.js.map +1 -1
  198. package/lib/views/session-files.d.ts +1 -2
  199. package/lib/views/session-files.js +1 -11
  200. package/lib/views/session-files.js.map +1 -1
  201. package/lib/views/settings-list.d.ts +10 -8
  202. package/lib/views/settings-list.js +116 -283
  203. package/lib/views/settings-list.js.map +1 -1
  204. package/lib/views/trajectory-timeline.d.ts +72 -0
  205. package/lib/views/trajectory-timeline.js +133 -0
  206. package/lib/views/trajectory-timeline.js.map +1 -0
  207. package/lib/views/trajectory.d.ts +15 -4
  208. package/lib/views/trajectory.js +557 -123
  209. package/lib/views/trajectory.js.map +1 -1
  210. package/lib/views/transcript-render.d.ts +95 -25
  211. package/lib/views/transcript-render.js +501 -167
  212. package/lib/views/transcript-render.js.map +1 -1
  213. package/lib/views/transcript-search.d.ts +39 -6
  214. package/lib/views/transcript-search.js +46 -11
  215. package/lib/views/transcript-search.js.map +1 -1
  216. package/lib/views/transcript-types.d.ts +63 -5
  217. package/lib/views/transcript-types.js +23 -11
  218. package/lib/views/transcript-types.js.map +1 -1
  219. package/package.json +67 -86
  220. package/lib/commands/advisor.d.ts +0 -15
  221. package/lib/commands/advisor.js +0 -200
  222. package/lib/commands/advisor.js.map +0 -1
  223. package/lib/commands/loop.d.ts +0 -52
  224. package/lib/commands/loop.js +0 -307
  225. package/lib/commands/loop.js.map +0 -1
  226. package/lib/commands/rate.d.ts +0 -33
  227. package/lib/commands/rate.js +0 -212
  228. package/lib/commands/rate.js.map +0 -1
  229. package/lib/commands/steer.d.ts +0 -5
  230. package/lib/commands/steer.js +0 -24
  231. package/lib/commands/steer.js.map +0 -1
  232. package/lib/commands/worktree.d.ts +0 -5
  233. package/lib/commands/worktree.js +0 -103
  234. package/lib/commands/worktree.js.map +0 -1
  235. package/lib/runtime/terminal-reader.d.ts +0 -11
  236. package/lib/runtime/terminal-reader.js +0 -87
  237. package/lib/runtime/terminal-reader.js.map +0 -1
  238. package/lib/views/agent-hub.d.ts +0 -32
  239. package/lib/views/agent-hub.js +0 -209
  240. package/lib/views/agent-hub.js.map +0 -1
  241. package/lib/views/copy-selector.d.ts +0 -54
  242. package/lib/views/copy-selector.js +0 -170
  243. package/lib/views/copy-selector.js.map +0 -1
@@ -3,6 +3,7 @@
3
3
  * glyphs used by terminal views.
4
4
  * @module @agi-fans/dsh-tui
5
5
  */
6
+ import { contrastRatio, labDistance, nearestPaletteIndex, nearestXterm256, nearestXterm256Grey, relativeLuminance, toOklab, terminalColorsKey, } from "./terminal-colors.js";
6
7
  /** Rounded-box drawing characters (OMP unicode preset). */
7
8
  export const BOX = {
8
9
  topLeft: '╭',
@@ -55,35 +56,62 @@ const DARK_PALETTE = {
55
56
  border: '#3556b8',
56
57
  borderAccent: '#4d6bfe',
57
58
  borderMuted: '#3d424a',
59
+ // Every content colour sits near 67% lightness, which is what lets one ink
60
+ // stay readable across all three card fills. A 55% red reached 4.5:1 on a
61
+ // near-black card and only 3.4:1 once that card became a real grey.
58
62
  success: '#89d281',
59
- error: '#fc4353',
63
+ error: '#ff8a8a',
60
64
  warning: '#e4c00f',
61
- muted: '#858a94',
62
- dim: '#666d7b',
65
+ muted: '#a3a8b2',
66
+ dim: '#858c99',
63
67
  text: '',
64
68
  userMessageText: '#e4e9f7',
65
69
  userMessageBg: '#141b30',
66
- toolPendingBg: '#171e2e',
67
- toolSuccessBg: '#141b26',
68
- toolErrorBg: '#291d1d',
70
+ // The three states differ by hue and saturation at one lightness, the way pi's
71
+ // do. Three near-black fills a few units apart could only ever read as "dark";
72
+ // at 24% lightness a neutral pending, a green success, and a red failure name
73
+ // themselves without any status glyph.
74
+ toolPendingBg: '#34383a',
75
+ toolSuccessBg: '#254131',
76
+ toolAltBg: '#2b2f31',
77
+ toolErrorBg: '#5b282a',
69
78
  toolTitle: '#e0e0e0',
70
- toolOutput: '#858a94',
79
+ toolOutput: '#a3a8b2',
71
80
  toolDiffAdded: '#89d281',
72
- toolDiffRemoved: '#fc4353',
73
- toolDiffContext: '#838a98',
74
- mdHeading: '#7c93ff',
75
- mdLink: '#7c93ff',
76
- mdLinkUrl: '#7186bd',
77
- mdCode: '#8a9099',
78
- mdCodeBlock: '#9cdcfe',
81
+ toolDiffRemoved: '#ff8a8a',
82
+ toolDiffContext: '#a8aeb8',
83
+ // Content inks share one lightness and differ only in hue, the way pi's dark
84
+ // palette does. Two rules follow from that and neither held before:
85
+ //
86
+ // - A hue names a role, so no single hue may carry several. `mdHeading`,
87
+ // `mdLink` and `mdListBullet` were all the accent blue, which made every
88
+ // document look like one long link.
89
+ // - Nothing that is prose may be achromatic. Inline code was `#8a9099`
90
+ // (chroma 0.015) and a prose-shaped codespan was `muted` — two greys the
91
+ // reader cannot tell apart, for the one role that has to read as "this is
92
+ // code".
93
+ //
94
+ // The syntax set used to be VSCode Dark+ values, which are drawn for a #1e1e1e
95
+ // editor and span 29 points of lightness here. These sit at 78% with chroma
96
+ // under 0.12, so a highlighted block varies in hue and not in weight.
97
+ mdHeading: '#ceb75b',
98
+ mdLink: '#83bdf8',
99
+ mdLinkUrl: '#7e95ad',
100
+ mdCode: '#d3a1ea',
101
+ mdCodeBlock: '#82cb9b',
79
102
  mdCodeBlockBorder: '#3d424a',
80
- mdKeyword: '#569cd6',
81
- syntaxString: '#ce9178',
82
- syntaxNumber: '#b5cea8',
83
- syntaxFunction: '#dcdcaa',
84
- syntaxType: '#4ec9b0',
103
+ mdKeyword: '#83bdf8',
104
+ syntaxString: '#e7a875',
105
+ syntaxNumber: '#82cb9b',
106
+ syntaxFunction: '#cdb760',
107
+ syntaxType: '#d1a3e6',
85
108
  mdQuote: '#777d88',
86
- mdListBullet: '#7c93ff',
109
+ mdQuoteBorder: '#3d424a',
110
+ mdHr: '#3d424a',
111
+ mdListBullet: '#a595c4',
112
+ // The composer's border while a `!` line is being typed. Green because that
113
+ // is the one signal that says "this leaves the model and runs on your machine".
114
+ shellMode: '#89d281',
87
115
  thinkingText: '#6b7280',
88
116
  customMessageLabel: '#b281d6',
89
117
  };
@@ -94,351 +122,51 @@ const LIGHT_PALETTE = {
94
122
  borderAccent: '#4d6bfe',
95
123
  borderMuted: '#b0b0b0',
96
124
  success: '#588458',
97
- error: '#9f4f4f',
125
+ error: '#8f3a3a',
98
126
  warning: '#9a7326',
99
127
  muted: '#666666',
100
128
  dim: '#767676',
101
129
  text: '',
102
130
  userMessageText: '#1c2438',
103
131
  userMessageBg: '#e7ecf9',
104
- toolPendingBg: '#eef1fa',
105
- toolSuccessBg: '#e8f0e8',
106
- toolErrorBg: '#f0e8e8',
132
+ // The same three states on paper: one lightness, three hues.
133
+ toolPendingBg: '#e7e8ea',
134
+ toolSuccessBg: '#dee9e1',
135
+ toolAltBg: '#eff0f2',
136
+ toolErrorBg: '#eee2e1',
107
137
  toolTitle: '#242424',
108
138
  toolOutput: '#666666',
109
139
  toolDiffAdded: '#4a6f4a',
110
- toolDiffRemoved: '#9f4f4f',
140
+ toolDiffRemoved: '#93403f',
111
141
  toolDiffContext: '#666666',
112
- mdHeading: '#3b57e0',
113
- mdLink: '#3b57e0',
114
- mdLinkUrl: '#5b6f9e',
115
- mdCode: '#567b7b',
116
- mdCodeBlock: '#567b7b',
142
+ // The same hue-per-role map as the dark palette, darkened for paper. Both
143
+ // palettes have to name a role with a hue, or switching themes would teach a
144
+ // reader a second visual language.
145
+ mdHeading: '#6d5900',
146
+ mdLink: '#1c62b3',
147
+ mdLinkUrl: '#465f7f',
148
+ mdCode: '#804797',
149
+ mdCodeBlock: '#247448',
117
150
  mdCodeBlockBorder: '#6c6c6c',
118
- mdKeyword: '#0451a5',
119
- syntaxString: '#a31515',
120
- syntaxNumber: '#098558',
121
- syntaxFunction: '#795e26',
122
- syntaxType: '#267e97',
151
+ mdKeyword: '#1d5da7',
152
+ syntaxString: '#a24e10',
153
+ syntaxNumber: '#247448',
154
+ syntaxFunction: '#6d5900',
155
+ syntaxType: '#7e4a93',
123
156
  mdQuote: '#6c6c6c',
124
- mdListBullet: '#3b57e0',
157
+ mdQuoteBorder: '#b8b8b8',
158
+ mdHr: '#b8b8b8',
159
+ mdListBullet: '#66508b',
160
+ shellMode: '#2f8f4e',
125
161
  thinkingText: '#6c6c6c',
126
162
  customMessageLabel: '#7e57c2',
127
163
  };
128
- const MIDNIGHT_PALETTE = {
129
- ...DARK_PALETTE,
130
- accent: '#7aa2f7',
131
- border: '#3d59a1',
132
- borderAccent: '#7dcfff',
133
- borderMuted: '#3d424a',
134
- success: '#9ece6a',
135
- error: '#f7768e',
136
- warning: '#e0af68',
137
- muted: '#878d96',
138
- dim: '#686f7e',
139
- text: '',
140
- userMessageBg: '#1a1b26',
141
- toolPendingBg: '#16161e',
142
- toolSuccessBg: '#1b2430',
143
- toolErrorBg: '#2a1b26',
144
- toolOutput: '#878d96',
145
- toolDiffAdded: '#9ece6a',
146
- toolDiffRemoved: '#f7768e',
147
- toolDiffContext: '#858d9a',
148
- mdHeading: '#bb9af7',
149
- mdLink: '#7dcfff',
150
- mdLinkUrl: '#626a77',
151
- mdCode: '#7f8799',
152
- mdCodeBlock: '#9aa5ce',
153
- mdCodeBlockBorder: '#3d424a',
154
- mdKeyword: '#bb9af7',
155
- syntaxString: '#9ece6a',
156
- syntaxNumber: '#ff9e64',
157
- syntaxFunction: '#7aa2f7',
158
- syntaxType: '#2ac3de',
159
- mdQuote: '#777d88',
160
- mdListBullet: '#7aa2f7',
161
- thinkingText: '#6a7394',
162
- customMessageLabel: '#bb9af7',
163
- userMessageText: '#c0caf5',
164
- toolTitle: '#c0caf5',
165
- };
166
- const SOLARIZED_PALETTE = {
167
- ...DARK_PALETTE,
168
- accent: '#c39400',
169
- border: '#268bd2',
170
- borderAccent: '#2aa198',
171
- borderMuted: '#586e75',
172
- success: '#859900',
173
- error: '#e87b79',
174
- warning: '#cb4b16',
175
- muted: '#8e9e9f',
176
- dim: '#668088',
177
- text: '',
178
- userMessageBg: '#073642',
179
- toolPendingBg: '#002b36',
180
- toolSuccessBg: '#073642',
181
- toolErrorBg: '#3b2020',
182
- toolOutput: '#8e9e9f',
183
- toolDiffAdded: '#8fa500',
184
- toolDiffRemoved: '#e87b79',
185
- toolDiffContext: '#889fa6',
186
- mdHeading: '#b58900',
187
- mdLink: '#3496da',
188
- mdLinkUrl: '#5e767d',
189
- mdCode: '#93a1a1',
190
- mdCodeBlock: '#2aa198',
191
- mdCodeBlockBorder: '#073642',
192
- mdKeyword: '#859900',
193
- syntaxString: '#2aa198',
194
- syntaxNumber: '#de66a0',
195
- syntaxFunction: '#3496da',
196
- syntaxType: '#b58900',
197
- mdQuote: '#839496',
198
- mdListBullet: '#b58900',
199
- thinkingText: '#5e767d',
200
- customMessageLabel: '#6c71c4',
201
- userMessageText: '#93a1a1',
202
- toolTitle: '#93a1a1',
203
- };
204
- const CATPPUCCIN_PALETTE = {
205
- ...DARK_PALETTE,
206
- accent: '#fab387',
207
- border: '#89b4fa',
208
- borderAccent: '#b4befe',
209
- borderMuted: '#313244',
210
- success: '#a6e3a1',
211
- error: '#f38ba8',
212
- warning: '#f9e2af',
213
- muted: '#989db0',
214
- dim: '#7a7d93',
215
- text: '',
216
- userMessageBg: '#181825',
217
- toolPendingBg: '#313244',
218
- toolSuccessBg: '#181825',
219
- toolErrorBg: '#11111b',
220
- toolTitle: '#b4befe',
221
- toolOutput: '#989db0',
222
- toolDiffAdded: '#a6e3a1',
223
- toolDiffRemoved: '#f38ba8',
224
- toolDiffContext: '#989db0',
225
- mdHeading: '#fab387',
226
- mdLink: '#89b4fa',
227
- mdLinkUrl: '#6c7086',
228
- mdCode: '#a6adc8',
229
- mdCodeBlock: '#cdd6f4',
230
- mdCodeBlockBorder: '#313244',
231
- mdKeyword: '#cba6f7',
232
- syntaxString: '#a6e3a1',
233
- syntaxNumber: '#fab387',
234
- syntaxFunction: '#89b4fa',
235
- syntaxType: '#f9e2af',
236
- mdQuote: '#7f849c',
237
- mdListBullet: '#fab387',
238
- thinkingText: '#6c7086',
239
- customMessageLabel: '#cba6f7',
240
- userMessageText: '#cdd6f4',
241
- };
242
- const DRACULA_PALETTE = {
243
- ...DARK_PALETTE,
244
- accent: '#bd93f9',
245
- border: '#bd93f9',
246
- borderAccent: '#ff79c6',
247
- borderMuted: '#44475a',
248
- success: '#50fa7b',
249
- error: '#ff5a5a',
250
- warning: '#f1fa8c',
251
- muted: '#8693b9',
252
- dim: '#6f7492',
253
- text: '',
254
- userMessageBg: '#1f2029',
255
- toolPendingBg: '#21222c',
256
- toolSuccessBg: '#1a1f1e',
257
- toolErrorBg: '#2a2028',
258
- toolTitle: '#8be9fd',
259
- toolOutput: '#8693b9',
260
- toolDiffAdded: '#50fa7b',
261
- toolDiffRemoved: '#ff5a5a',
262
- toolDiffContext: '#8693b9',
263
- mdHeading: '#bd93f9',
264
- mdLink: '#8be9fd',
265
- mdLinkUrl: '#6474a5',
266
- mdCode: '#9aa3c7',
267
- mdCodeBlock: '#f8f8f2',
268
- mdCodeBlockBorder: '#44475a',
269
- mdKeyword: '#ff79c6',
270
- syntaxString: '#f1fa8c',
271
- syntaxNumber: '#bd93f9',
272
- syntaxFunction: '#50fa7b',
273
- syntaxType: '#8be9fd',
274
- mdQuote: '#6474a5',
275
- mdListBullet: '#ff79c6',
276
- thinkingText: '#6474a5',
277
- customMessageLabel: '#bd93f9',
278
- userMessageText: '#f8f8f2',
279
- };
280
- const NORD_PALETTE = {
281
- ...DARK_PALETTE,
282
- accent: '#88c0d0',
283
- border: '#5e81ac',
284
- borderAccent: '#88c0d0',
285
- borderMuted: '#434c5e',
286
- success: '#a3be8c',
287
- error: '#daa2a8',
288
- warning: '#ebcb8b',
289
- muted: '#a8b1c1',
290
- dim: '#8490a7',
291
- text: '',
292
- userMessageBg: '#3b4252',
293
- toolPendingBg: '#3b4252',
294
- toolSuccessBg: '#2e3440',
295
- toolErrorBg: '#3b2f31',
296
- toolTitle: '#88c0d0',
297
- toolOutput: '#a8b1c1',
298
- toolDiffAdded: '#a3be8c',
299
- toolDiffRemoved: '#daa2a8',
300
- toolDiffContext: '#a8b1c1',
301
- mdHeading: '#88c0d0',
302
- mdLink: '#88c0d0',
303
- mdLinkUrl: '#727f9a',
304
- mdCode: '#82a2c1',
305
- mdCodeBlock: '#d8dee9',
306
- mdCodeBlockBorder: '#434c5e',
307
- mdKeyword: '#82a2c1',
308
- syntaxString: '#a3be8c',
309
- syntaxNumber: '#b793b1',
310
- syntaxFunction: '#88c0d0',
311
- syntaxType: '#8fbcbb',
312
- mdQuote: '#7b88a1',
313
- mdListBullet: '#81a1c1',
314
- thinkingText: '#747f95',
315
- customMessageLabel: '#b48ead',
316
- userMessageText: '#d8dee9',
317
- };
318
- const GRUVBOX_PALETTE = {
319
- ...DARK_PALETTE,
320
- accent: '#fe8019',
321
- border: '#458588',
322
- borderAccent: '#8ec07c',
323
- borderMuted: '#504945',
324
- success: '#b8bb26',
325
- error: '#fc6c5c',
326
- warning: '#fabd2f',
327
- muted: '#a4988b',
328
- dim: '#87796d',
329
- text: '',
330
- userMessageBg: '#1d2021',
331
- toolPendingBg: '#32302f',
332
- toolSuccessBg: '#1d2021',
333
- toolErrorBg: '#3c2021',
334
- toolTitle: '#ebdbb2',
335
- toolOutput: '#a4988b',
336
- toolDiffAdded: '#b8bb26',
337
- toolDiffRemoved: '#fc6c5c',
338
- toolDiffContext: '#a4988b',
339
- mdHeading: '#fabd2f',
340
- mdLink: '#8ec07c',
341
- mdLinkUrl: '#7e7165',
342
- mdCode: '#bdae93',
343
- mdCodeBlock: '#ebdbb2',
344
- mdCodeBlockBorder: '#504945',
345
- mdKeyword: '#d3869b',
346
- syntaxString: '#b8bb26',
347
- syntaxNumber: '#d3869b',
348
- syntaxFunction: '#fabd2f',
349
- syntaxType: '#8ec07c',
350
- mdQuote: '#928374',
351
- mdListBullet: '#fe8019',
352
- thinkingText: '#7e7165',
353
- customMessageLabel: '#d3869b',
354
- userMessageText: '#ebdbb2',
355
- };
356
- const ROSE_PINE_PALETTE = {
357
- ...DARK_PALETTE,
358
- accent: '#c4a7e7',
359
- border: '#31748f',
360
- borderAccent: '#9ccfd8',
361
- borderMuted: '#403d52',
362
- success: '#9ccfd8',
363
- error: '#eb6f92',
364
- warning: '#f6c177',
365
- muted: '#8c89a2',
366
- dim: '#6f6b8b',
367
- text: '',
368
- userMessageBg: '#21202e',
369
- toolPendingBg: '#1f1d2e',
370
- toolSuccessBg: '#21202e',
371
- toolErrorBg: '#2d1f26',
372
- toolTitle: '#9ccfd8',
373
- toolOutput: '#8c89a2',
374
- toolDiffAdded: '#9ccfd8',
375
- toolDiffRemoved: '#eb6f92',
376
- toolDiffContext: '#8c89a2',
377
- mdHeading: '#c4a7e7',
378
- mdLink: '#9ccfd8',
379
- mdLinkUrl: '#908caa',
380
- mdCode: '#a8a4c4',
381
- mdCodeBlock: '#e0def4',
382
- mdCodeBlockBorder: '#403d52',
383
- mdKeyword: '#3b8cac',
384
- syntaxString: '#f6c177',
385
- syntaxNumber: '#ebbcba',
386
- syntaxFunction: '#9ccfd8',
387
- syntaxType: '#c4a7e7',
388
- mdQuote: '#6e6a86',
389
- mdListBullet: '#c4a7e7',
390
- thinkingText: '#6e6a86',
391
- customMessageLabel: '#c4a7e7',
392
- userMessageText: '#e0def4',
393
- };
394
- const MONO_PALETTE = {
395
- accent: '#e8e8e8',
396
- border: '#888888',
397
- borderAccent: '#b8b8b8',
398
- borderMuted: '#444444',
399
- success: '#c0c0c0',
400
- error: '#f0f0f0',
401
- warning: '#a8a8a8',
402
- muted: '#939393',
403
- dim: '#757575',
404
- text: '',
405
- userMessageText: '#e8e8e8',
406
- userMessageBg: '#202020',
407
- toolPendingBg: '#242424',
408
- toolSuccessBg: '#1c1c1c',
409
- toolErrorBg: '#2a2a2a',
410
- toolTitle: '#e8e8e8',
411
- toolOutput: '#939393',
412
- toolDiffAdded: '#c0c0c0',
413
- toolDiffRemoved: '#f0f0f0',
414
- toolDiffContext: '#939393',
415
- mdHeading: '#e8e8e8',
416
- mdLink: '#b8b8b8',
417
- mdLinkUrl: '#666666',
418
- mdCode: '#a0a0a0',
419
- mdCodeBlock: '#c0c0c0',
420
- mdCodeBlockBorder: '#444444',
421
- mdKeyword: '#b8b8b8',
422
- syntaxString: '#c0c0c0',
423
- syntaxNumber: '#a0a0a0',
424
- syntaxFunction: '#e8e8e8',
425
- syntaxType: '#b8b8b8',
426
- mdQuote: '#888888',
427
- mdListBullet: '#b8b8b8',
428
- thinkingText: '#767676',
429
- customMessageLabel: '#a8a8a8',
430
- };
431
164
  const PALETTES = {
432
165
  dark: DARK_PALETTE,
433
166
  light: LIGHT_PALETTE,
434
- midnight: MIDNIGHT_PALETTE,
435
- solarized: SOLARIZED_PALETTE,
436
- catppuccin: CATPPUCCIN_PALETTE,
437
- dracula: DRACULA_PALETTE,
438
- nord: NORD_PALETTE,
439
- gruvbox: GRUVBOX_PALETTE,
440
- 'rose-pine': ROSE_PINE_PALETTE,
441
- mono: MONO_PALETTE,
167
+ // `system` starts from the neutral palette and is rebuilt from the reported
168
+ // terminal colors when a launch can probe them.
169
+ system: DARK_PALETTE,
442
170
  };
443
171
  /**
444
172
  * 16-color foreground fallbacks when the terminal is not truecolor. Entries
@@ -465,6 +193,7 @@ const DARK_ANSI16 = {
465
193
  // Neutral fills avoid same-index collisions with error and diff ink.
466
194
  toolPendingBg: '30',
467
195
  toolSuccessBg: '30',
196
+ toolAltBg: '30',
468
197
  toolErrorBg: '30',
469
198
  toolTitle: '37',
470
199
  toolOutput: '37',
@@ -479,7 +208,10 @@ const DARK_ANSI16 = {
479
208
  mdCodeBlockBorder: '90',
480
209
  mdKeyword: '36',
481
210
  mdQuote: '37',
211
+ mdQuoteBorder: '90',
212
+ mdHr: '90',
482
213
  mdListBullet: '94',
214
+ shellMode: '92',
483
215
  thinkingText: '90',
484
216
  customMessageLabel: '35',
485
217
  };
@@ -500,8 +232,12 @@ const LIGHT_ANSI16 = {
500
232
  text: '39',
501
233
  userMessageText: '30',
502
234
  userMessageBg: '37',
235
+ // This table holds *foreground* codes; `toBackgrounds` adds the 10 that turns
236
+ // them into fills, so `37` here is the white `\x1b[47m` card a light terminal
237
+ // needs under its dark ink.
503
238
  toolPendingBg: '37',
504
239
  toolSuccessBg: '37',
240
+ toolAltBg: '37',
505
241
  toolErrorBg: '37',
506
242
  toolTitle: '30',
507
243
  toolOutput: '30',
@@ -516,119 +252,17 @@ const LIGHT_ANSI16 = {
516
252
  mdCodeBlockBorder: '90',
517
253
  mdKeyword: '34',
518
254
  mdQuote: '30',
255
+ mdQuoteBorder: '90',
256
+ mdHr: '90',
519
257
  mdListBullet: '34',
258
+ shellMode: '32',
520
259
  thinkingText: '90',
521
260
  customMessageLabel: '35',
522
261
  };
523
- const MIDNIGHT_ANSI16 = {
524
- ...DARK_ANSI16,
525
- accent: '34',
526
- border: '34',
527
- borderAccent: '36',
528
- mdHeading: '35',
529
- mdLink: '36',
530
- mdKeyword: '35',
531
- mdListBullet: '34',
532
- customMessageLabel: '35',
533
- };
534
- const SOLARIZED_ANSI16 = {
535
- ...DARK_ANSI16,
536
- accent: '33',
537
- border: '34',
538
- borderAccent: '36',
539
- mdHeading: '33',
540
- mdLink: '34',
541
- mdKeyword: '32',
542
- mdCodeBlock: '36',
543
- mdListBullet: '33',
544
- customMessageLabel: '35',
545
- };
546
- const CATPPUCCIN_ANSI16 = {
547
- ...DARK_ANSI16,
548
- accent: '33',
549
- border: '34',
550
- borderAccent: '35',
551
- mdHeading: '33',
552
- mdLink: '34',
553
- mdKeyword: '35',
554
- mdListBullet: '33',
555
- customMessageLabel: '35',
556
- };
557
- const DRACULA_ANSI16 = {
558
- ...DARK_ANSI16,
559
- accent: '35',
560
- border: '35',
561
- borderAccent: '35',
562
- mdHeading: '35',
563
- mdLink: '36',
564
- mdKeyword: '35',
565
- mdListBullet: '35',
566
- customMessageLabel: '35',
567
- };
568
- const NORD_ANSI16 = {
569
- ...DARK_ANSI16,
570
- accent: '36',
571
- border: '34',
572
- borderAccent: '36',
573
- mdHeading: '36',
574
- mdLink: '36',
575
- mdKeyword: '34',
576
- mdListBullet: '34',
577
- customMessageLabel: '35',
578
- };
579
- const GRUVBOX_ANSI16 = {
580
- ...DARK_ANSI16,
581
- accent: '33',
582
- border: '36',
583
- borderAccent: '32',
584
- mdHeading: '33',
585
- mdLink: '32',
586
- mdKeyword: '35',
587
- mdListBullet: '33',
588
- customMessageLabel: '35',
589
- };
590
- const ROSE_PINE_ANSI16 = {
591
- ...DARK_ANSI16,
592
- accent: '35',
593
- border: '36',
594
- borderAccent: '36',
595
- mdHeading: '35',
596
- mdLink: '36',
597
- mdKeyword: '36',
598
- mdListBullet: '35',
599
- customMessageLabel: '35',
600
- };
601
- const MONO_ANSI16 = {
602
- ...DARK_ANSI16,
603
- syntaxString: '37',
604
- syntaxNumber: '37',
605
- syntaxFunction: '97',
606
- syntaxType: '37',
607
- accent: '97',
608
- border: '37',
609
- borderAccent: '97',
610
- success: '37',
611
- error: '97',
612
- warning: '37',
613
- muted: '37',
614
- mdHeading: '97',
615
- mdLink: '37',
616
- mdCodeBlock: '37',
617
- mdKeyword: '37',
618
- mdListBullet: '37',
619
- customMessageLabel: '37',
620
- };
621
262
  const ANSI16 = {
622
263
  dark: DARK_ANSI16,
623
264
  light: LIGHT_ANSI16,
624
- midnight: MIDNIGHT_ANSI16,
625
- solarized: SOLARIZED_ANSI16,
626
- catppuccin: CATPPUCCIN_ANSI16,
627
- dracula: DRACULA_ANSI16,
628
- nord: NORD_ANSI16,
629
- gruvbox: GRUVBOX_ANSI16,
630
- 'rose-pine': ROSE_PINE_ANSI16,
631
- mono: MONO_ANSI16,
265
+ system: DARK_ANSI16,
632
266
  };
633
267
  /** Distance from a foreground SGR code to its background twin: 31 → 41, 39 → 49. */
634
268
  const BG_CODE_OFFSET = 10;
@@ -645,18 +279,164 @@ function toBackgrounds(foreground) {
645
279
  }
646
280
  return result;
647
281
  }
282
+ /**
283
+ * Backgrounds for the three tool states, chosen so they stay distinct.
284
+ *
285
+ * This is the group form of the problem {@link nearestXterm256} solves for one
286
+ * colour: the states differ mostly in hue and only slightly in lightness, so
287
+ * quantising each swatch on its own collapses them. The assignment below picks a
288
+ * distinct entry per state and then orders the three by lightness, so pending,
289
+ * success, and error remain recognizable even on a terminal that offers only
290
+ * eight backgrounds.
291
+ *
292
+ * A 16-colour terminal simply has fewer backgrounds than the states that need
293
+ * one. Rather than paint three states the same black, the ones it cannot tint
294
+ * fall back to `toolSuccessBg` and the error state — the only one a reader must
295
+ * act on — is given the red it already has in `ansiBg` when the palette has a
296
+ * usable red background.
297
+ */
298
+ function resolveToolFills(name, depth, terminal) {
299
+ const palette = name === 'system' ? systemPalette(terminal) : PALETTES[name];
300
+ const reported = depth === 'ansi16' && terminal?.palette !== undefined && terminal.palette.length === 16
301
+ ? terminal.palette
302
+ : undefined;
303
+ const states = ['toolPendingBg', 'toolSuccessBg', 'toolErrorBg'];
304
+ const swatches = states.map(state => {
305
+ const swatch = palette[state];
306
+ if (typeof swatch !== 'string' || swatch === '')
307
+ return undefined;
308
+ const [r, g, b] = hexToRgb(swatch);
309
+ return { r, g, b };
310
+ });
311
+ const filled = swatches.every((swatch) => swatch !== undefined);
312
+ if (!filled)
313
+ return undefined;
314
+ if (depth === 'ansi256') {
315
+ // The neutral ramp is where the three near-black surfaces actually live; the
316
+ // colour cube would round them all to the same corner.
317
+ const indices = swatches.map(swatch => nearestXterm256Grey(swatch));
318
+ return assignDistinct(states, indices, index => `\x1b[48;5;${index}m`);
319
+ }
320
+ if (reported !== undefined) {
321
+ // Sixteen colours are not enough for three surfaces, so each state claims a
322
+ // *different* entry from the ones its own hue actually reaches, rather than
323
+ // nudging a number that has no meaning in this palette.
324
+ return pickDistinctPaletteEntries(name, states, swatches, reported, index => ansiIndexSgr(index, true));
325
+ }
326
+ // The terminal never answered our colour query, so the standard xterm ANSI
327
+ // meanings are assumed: three different backgrounds that carry text. Guessing
328
+ // per swatch here is exactly what collapsed the three states into one. Red is
329
+ // deliberately not one of them — a red fill would share its palette slot with
330
+ // the red `error` and `toolDiffRemoved` ink drawn on top of it.
331
+ // A light card is white paper under dark ink, and the eight backgrounds an
332
+ // ANSI-16 terminal offers contain exactly one that carries it. Pending and
333
+ // success therefore share it, and the failing card — the state a reader has to
334
+ // act on — is the one that gets a fill of its own.
335
+ return name === 'light'
336
+ ? { toolPendingBg: '\x1b[47m', toolSuccessBg: '\x1b[47m', toolErrorBg: '\x1b[45m' }
337
+ : { toolPendingBg: '\x1b[44m', toolSuccessBg: '\x1b[40m', toolErrorBg: '\x1b[45m' };
338
+ }
339
+ /**
340
+ * Give each state the nearest palette entry no other state has taken.
341
+ *
342
+ * A 16-colour terminal cannot express a subtle tint, so the question stops being
343
+ * "which entry is nearest" and becomes "which entry can this state have". Each
344
+ * state is served in turn, by increasing distance, and only entries whose
345
+ * luminance is low enough to hold card text are considered — a bright palette
346
+ * entry would turn a tool card into a block of white.
347
+ */
348
+ function pickDistinctPaletteEntries(name, states, swatches, palette, encode) {
349
+ // A palette slot already used as ink on a card is not available as a fill: a
350
+ // red background under red diff lines erases them. The check runs against the
351
+ // same fallback table the foreground codes come from, so the two can never
352
+ // disagree about which slot a colour occupies.
353
+ const takenByInk = new Set(CARD_INK_COLORS
354
+ .map(color => Number.parseInt(ANSI16[name][color], 10))
355
+ .filter(code => Number.isInteger(code) && code < 8));
356
+ const ceiling = SURFACE_MAX_LUMINANCE[name];
357
+ // A light card's ink is a black that owns slot 0, so its fills come from the
358
+ // bright end instead; the test is "far enough from the ink", not "dark".
359
+ const eligible = (entry, index) => entry !== undefined
360
+ && !takenByInk.has(index)
361
+ && (name === 'light' ? relativeLuminance(entry) >= ceiling : relativeLuminance(entry) < ceiling);
362
+ if (palette.filter(eligible).length < states.length)
363
+ return undefined;
364
+ const ranked = palette
365
+ .flatMap((entry, index) => entry === undefined ? [] : [{ index, entry: entry }])
366
+ .filter(({ entry, index }) => eligible(entry, index))
367
+ .map(({ index, entry }) => ({ index, lab: toOklab(entry) }));
368
+ if (ranked.length < states.length)
369
+ return undefined;
370
+ const used = new Set();
371
+ const result = {};
372
+ for (let at = 0; at < states.length; at += 1) {
373
+ const target = toOklab(swatches[at]);
374
+ let best;
375
+ for (const candidate of ranked) {
376
+ if (used.has(candidate.index))
377
+ continue;
378
+ const distance = labDistance(target, candidate.lab);
379
+ if (best === undefined || distance < best.distance)
380
+ best = { index: candidate.index, distance };
381
+ }
382
+ if (best === undefined)
383
+ return undefined;
384
+ used.add(best.index);
385
+ result[states[at]] = encode(best.index);
386
+ }
387
+ return result;
388
+ }
389
+ /** Ink drawn on a tool card, by theme token and ANSI-16 fallback code. */
390
+ const CARD_INK_COLORS = [
391
+ 'toolTitle', 'toolOutput', 'toolDiffAdded', 'toolDiffRemoved', 'toolDiffContext',
392
+ 'mdKeyword', 'syntaxString', 'syntaxNumber', 'syntaxFunction', 'syntaxType',
393
+ 'mdCodeBlock', 'mdLink', 'mdHeading', 'mdQuote', 'dim', 'muted', 'accent',
394
+ 'success', 'error', 'warning', 'thinkingText', 'mdListBullet', 'mdHr',
395
+ 'mdQuoteBorder', 'mdCodeBlockBorder', 'mdCode', 'mdLinkUrl',
396
+ ];
397
+ /**
398
+ * How light a card fill may be, per palette.
399
+ *
400
+ * The constraint is not "dark" — it is "far from the ink". A dark theme draws
401
+ * light text on a dark fill and a light theme draws dark text on a light fill, so
402
+ * each palette states its own ceiling instead of sharing a dark one that would
403
+ * make every card on a light terminal a black block.
404
+ */
405
+ const SURFACE_MAX_LUMINANCE = {
406
+ dark: 0.5, system: 0.5,
407
+ // A light card's fill comes from the bright end; the ceiling is its floor.
408
+ light: 0.98,
409
+ };
410
+ /**
411
+ * Give every state its own encoding, nudging the ones that collided.
412
+ *
413
+ * `preferred` is the honest per-state choice; when two states land on it, the
414
+ * one further from the light end is pushed to a neighbouring entry. A terminal
415
+ * whose palette has no room left keeps the collision rather than inventing a
416
+ * colour it cannot draw, which is why this returns `undefined` in that case.
417
+ */
418
+ function assignDistinct(states, preferred, encode) {
419
+ const used = new Set();
420
+ const result = {};
421
+ for (let at = 0; at < states.length; at += 1) {
422
+ const state = states[at];
423
+ let index = preferred[at] ?? 0;
424
+ for (let step = 1; used.has(index) && step < 8; step += 1) {
425
+ // Alternate lighter and darker so a nudge stays close to the intent.
426
+ index = preferred[at] + (step % 2 === 1 ? -step : step);
427
+ }
428
+ if (used.has(index))
429
+ return undefined;
430
+ used.add(index);
431
+ result[state] = encode(index);
432
+ }
433
+ return result;
434
+ }
648
435
  /** 16-color background fallbacks when the terminal is not truecolor. */
649
436
  const ANSI16_BG = {
650
437
  dark: toBackgrounds(DARK_ANSI16),
651
438
  light: toBackgrounds(LIGHT_ANSI16),
652
- midnight: toBackgrounds(MIDNIGHT_ANSI16),
653
- solarized: toBackgrounds(SOLARIZED_ANSI16),
654
- catppuccin: toBackgrounds(CATPPUCCIN_ANSI16),
655
- dracula: toBackgrounds(DRACULA_ANSI16),
656
- nord: toBackgrounds(NORD_ANSI16),
657
- gruvbox: toBackgrounds(GRUVBOX_ANSI16),
658
- 'rose-pine': toBackgrounds(ROSE_PINE_ANSI16),
659
- mono: toBackgrounds(MONO_ANSI16),
439
+ system: toBackgrounds(DARK_ANSI16),
660
440
  };
661
441
  const FG_RESET = '\x1b[39m';
662
442
  const BG_RESET = '\x1b[49m';
@@ -666,11 +446,15 @@ const UNDERLINE_RESET = '\x1b[24m';
666
446
  const STRIKE_RESET = '\x1b[29m';
667
447
  const INVERSE_RESET = '\x1b[27m';
668
448
  /** Built-in palettes, including a few well-known oh-my-pi coding themes. */
669
- export const THEME_NAMES = [
670
- 'dark', 'light', 'midnight', 'solarized',
671
- 'catppuccin', 'dracula', 'nord', 'gruvbox', 'rose-pine',
672
- 'mono',
673
- ];
449
+ /**
450
+ * Shipped palettes: one dark, one light, and the one that follows the terminal.
451
+ *
452
+ * A theme is a contract the whole TUI is read through — every card, diff, and
453
+ * syntax token has to stay legible on it — so the set stays small enough to hold
454
+ * to that standard. `system` is not a third design: it derives its surfaces from
455
+ * the colours the terminal reports and then follows the same rules.
456
+ */
457
+ export const THEME_NAMES = ['dark', 'light', 'system'];
674
458
  /** True when `value` is a shipped palette name. */
675
459
  export function isThemeName(value) {
676
460
  return THEME_NAMES.includes(value);
@@ -726,27 +510,282 @@ function hexToRgb(hex) {
726
510
  Number.parseInt(body.slice(4, 6), 16),
727
511
  ];
728
512
  }
729
- /** Foreground SGR for one swatch; `fallback` is a code from {@link ANSI16}. */
730
- function fgCode(swatch, trueColor, fallback) {
513
+ /**
514
+ * SGR for one swatch at a color depth; `fallback` is a code from
515
+ * {@link ANSI16} or {@link ANSI16_BG}. `layer` is 38 for ink and 48 for fill.
516
+ */
517
+ function swatchCode(swatch, depth, fallback, layer) {
731
518
  if (swatch === '')
732
- return '\x1b[39m';
519
+ return layer === 38 ? '\x1b[39m' : '\x1b[49m';
733
520
  if (typeof swatch === 'number')
734
- return `\x1b[38;5;${swatch}m`;
735
- if (!trueColor)
521
+ return `\x1b[${layer};5;${swatch}m`;
522
+ if (depth === 'ansi16')
736
523
  return `\x1b[${fallback}m`;
737
524
  const [r, g, b] = hexToRgb(swatch);
738
- return `\x1b[38;2;${r};${g};${b}m`;
525
+ if (depth === 'ansi256')
526
+ return `\x1b[${layer};5;${nearestXterm256({ r, g, b })}m`;
527
+ return `\x1b[${layer};2;${r};${g};${b}m`;
739
528
  }
740
- /** Background SGR for one swatch; `fallback` is a code from {@link ANSI16_BG}. */
741
- function bgCode(swatch, trueColor, fallback) {
742
- if (swatch === '')
743
- return '\x1b[49m';
744
- if (typeof swatch === 'number')
745
- return `\x1b[48;5;${swatch}m`;
746
- if (!trueColor)
747
- return `\x1b[${fallback}m`;
748
- const [r, g, b] = hexToRgb(swatch);
749
- return `\x1b[48;2;${r};${g};${b}m`;
529
+ /** Mix two colors in linear light; `weight` is the share of `to`. */
530
+ function mixRgb(from, to, weight) {
531
+ const linear = (value) => {
532
+ const channelValue = value / 255;
533
+ return channelValue <= 0.04045 ? channelValue / 12.92 : ((channelValue + 0.055) / 1.055) ** 2.4;
534
+ };
535
+ const encode = (value) => {
536
+ const clamped = Math.max(0, Math.min(1, value));
537
+ return Math.round(255 * (clamped <= 0.003_130_8 ? clamped * 12.92 : 1.055 * clamped ** (1 / 2.4) - 0.055));
538
+ };
539
+ const mix = (left, right) => linear(left) * (1 - weight) + linear(right) * weight;
540
+ return { r: encode(mix(from.r, to.r)), g: encode(mix(from.g, to.g)), b: encode(mix(from.b, to.b)) };
541
+ }
542
+ function toHex(color) {
543
+ const part = (value) => Math.round(value).toString(16).padStart(2, '0');
544
+ return `#${part(color.r)}${part(color.g)}${part(color.b)}`;
545
+ }
546
+ /** The fixed red a `system` error surface mixes toward when the palette has none. */
547
+ const SYSTEM_ERROR_RED = { r: 0xff, g: 0x5f, b: 0x5f };
548
+ /** The fixed green a `system` success surface mixes toward when the palette has none. */
549
+ const SYSTEM_SUCCESS_GREEN = { r: 0x4a, g: 0xc8, b: 0x7a };
550
+ /** The fixed blue a `system` notice surface mixes toward when the palette has none. */
551
+ const SYSTEM_NOTICE_BLUE = { r: 0x6f, g: 0xa8, b: 0xff };
552
+ /**
553
+ * Relative luminance each card state is held to on a `system` palette.
554
+ *
555
+ * These are the shipped dark palette's own fills measured as luminance, so a
556
+ * `system` card sits where a `dark` one does no matter what green the terminal
557
+ * reports. They are the one part of the card that must not follow the terminal:
558
+ * a terminal's green is chosen for *ink* on its background, and spending that
559
+ * green on a large fill is what pushed the shipped value from 4.4% to 11.7%
560
+ * luminance and flattened dimmed text on a successful card to 3.3:1.
561
+ */
562
+ const SYSTEM_CARD_LUMINANCE = {
563
+ pending: 0.0386,
564
+ success: 0.0440,
565
+ error: 0.0391,
566
+ };
567
+ /**
568
+ * How far a card may move from the terminal background toward a state hue.
569
+ *
570
+ * A card is a step off the reader's own background, not a swatch of the terminal
571
+ * palette. Scaling a tint to the target luminance directly keeps the hue but not
572
+ * the calm: a saturated green has to be dimmed so far to reach 4.4% that the
573
+ * scaled channels clip, and a light terminal turns it neon. Interpolating from the
574
+ * background toward the hue instead bounds how much of the hue a card can ever
575
+ * contain, so the fill stays a surface whatever the terminal reports.
576
+ */
577
+ const SYSTEM_CARD_TINT = 0.34;
578
+ /**
579
+ * Where the alternate successful surface sits relative to the primary one.
580
+ *
581
+ * A slight step, not a second state: the pair exists so two adjacent successful
582
+ * calls do not merge into one block. Anything larger starts naming a difference
583
+ * that is not there.
584
+ */
585
+ const SYSTEM_ALT_STEP = 1.065;
586
+ /**
587
+ * The background-to-hue step that lands closest to a target relative luminance.
588
+ *
589
+ * A fixed weight cannot hold a card at one lightness — the same weight applied to
590
+ * a bright terminal green and to a dim one lands almost three stops apart — so the
591
+ * weight is solved for instead, by bisection. Hue still comes from the terminal,
592
+ * which is what makes the fill say "success"; how *much* of it a card spends is
593
+ * the theme's decision, and the tint cap is what keeps the result a surface.
594
+ */
595
+ function tintAtLuminance(background, tint, target) {
596
+ const at = (weight) => mixRgb(background, tint, weight);
597
+ const luminanceAt = (weight) => relativeLuminance(at(weight));
598
+ // Interpolating between two luminance endpoints is monotonic only while one
599
+ // side sits below the target; a background already brighter than the target
600
+ // reverses which direction helps, so solve outward from the background.
601
+ const towardsBrighter = relativeLuminance(background) < target;
602
+ let low = 0;
603
+ let high = SYSTEM_CARD_TINT;
604
+ if (towardsBrighter ? luminanceAt(high) < target : luminanceAt(high) > target) {
605
+ // The capped step already overshoots or undershoots the target: the cap is
606
+ // the answer, and the card keeps the calm it was given.
607
+ return at(high);
608
+ }
609
+ for (let step = 0; step < 40; step += 1) {
610
+ const middle = (low + high) / 2;
611
+ if (towardsBrighter ? luminanceAt(middle) < target : luminanceAt(middle) > target)
612
+ low = middle;
613
+ else
614
+ high = middle;
615
+ }
616
+ return at((low + high) / 2);
617
+ }
618
+ /**
619
+ * The background lifted toward white until it reaches a target luminance.
620
+ *
621
+ * Neutral surfaces need a lightness step, not a hue: interpolating toward the
622
+ * foreground flattens whatever faint tint the terminal's background carries, so
623
+ * the step is taken toward white and the background's own channel ratios survive.
624
+ */
625
+ function liftedToLuminance(background, target) {
626
+ // On paper the background is already brighter than a card wants to be, so the
627
+ // step goes toward black instead; either way it keeps the background's ratios.
628
+ const pole = relativeLuminance(background) < target ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 };
629
+ const towardsBrighter = pole.r === 255;
630
+ const at = (weight) => mixRgb(background, pole, weight);
631
+ const luminanceAt = (weight) => relativeLuminance(at(weight));
632
+ let low = 0;
633
+ let high = 1;
634
+ for (let step = 0; step < 40; step += 1) {
635
+ const middle = (low + high) / 2;
636
+ if (towardsBrighter ? luminanceAt(middle) < target : luminanceAt(middle) > target)
637
+ low = middle;
638
+ else
639
+ high = middle;
640
+ }
641
+ return at((low + high) / 2);
642
+ }
643
+ /**
644
+ * The contrast a swatch keeps against the surfaces it is drawn on.
645
+ *
646
+ * Taken against the *weakest* card rather than against the page, because a
647
+ * secondary ink is spent on card fills — a hint on a successful card is the case
648
+ * that has to stay readable, not the same hint on bare background.
649
+ */
650
+ function worstContrast(ink, cards) {
651
+ return Math.min(...cards.map(card => contrastRatio(ink, card)));
652
+ }
653
+ /**
654
+ * A secondary ink held at the contrast the shipped palette achieved.
655
+ *
656
+ * A fixed mix weight made the result depend on which side was bright: on a dark
657
+ * background the step lightened the ink and stayed safe, but against paper it
658
+ * moved it toward the page, so a light terminal's `dim` ended at 1.75:1 — nearly
659
+ * invisible. The weight is solved from the contrast actually achieved instead, so
660
+ * the live cards keep the hierarchy the palette was drawn with: this ink is as
661
+ * far from the text as the shipped one was from its own cards, no further.
662
+ */
663
+ function quietInk(foreground, background, cards, target) {
664
+ const at = (weight) => mixRgb(foreground, background, weight);
665
+ /**
666
+ * Which way the weight moves contrast is the host's answer, not a constant.
667
+ *
668
+ * Toward a dark background the ink lightens as it is mixed, so weight raises
669
+ * contrast; toward paper it fades toward the page, so weight lowers it. Solving
670
+ * one direction silently returned the palette's cap on every light host, which
671
+ * is how `dim` came to sit at 1.75:1 while looking correct in the code.
672
+ */
673
+ const towardsBrighter = relativeLuminance(background) < relativeLuminance(foreground);
674
+ let low = 0;
675
+ let high = 1;
676
+ // A background that is itself close to the cards cannot reach the target at any
677
+ // weight; full foreground is the readable answer and the palette's floor.
678
+ if (worstContrast(at(0), cards) <= target)
679
+ return at(0);
680
+ for (let step = 0; step < 40; step += 1) {
681
+ const middle = (low + high) / 2;
682
+ const contrast = worstContrast(at(middle), cards);
683
+ if (towardsBrighter ? contrast > target : contrast >= target)
684
+ low = middle;
685
+ else
686
+ high = middle;
687
+ }
688
+ return at((low + high) / 2);
689
+ }
690
+ /**
691
+ * Rebuild the surface and ink swatches from what the terminal reported, so the
692
+ * cards sit on the user's own background instead of a guessed one. The two
693
+ * successful-call surfaces are separated by design: alternation only reads if
694
+ * the pair is perceptibly different.
695
+ */
696
+ function systemPalette(terminal) {
697
+ const background = terminal?.background;
698
+ const light = terminal?.scheme === 'light';
699
+ const base = light ? LIGHT_PALETTE : DARK_PALETTE;
700
+ if (background === undefined)
701
+ return base;
702
+ const foreground = terminal?.foreground ?? (light ? { r: 0x24, g: 0x24, b: 0x24 } : { r: 0xe0, g: 0xe0, b: 0xe0 });
703
+ const accent = terminal?.palette?.[4] ?? SYSTEM_NOTICE_BLUE;
704
+ const error = terminal?.palette?.[1] ?? SYSTEM_ERROR_RED;
705
+ const successInk = terminal?.palette?.[2] ?? SYSTEM_SUCCESS_GREEN;
706
+ /**
707
+ * A state fill carries its own hue, the way the shipped palettes do.
708
+ *
709
+ * Mixing toward the terminal's *foreground* produced three greys: the states
710
+ * were separated only by lightness, which on a dark background is the one axis
711
+ * a reader cannot see. Mixing toward the state colour keeps the card on the
712
+ * user's own background and gives it the meaning at the same time — but the
713
+ * *amount* of that colour is fixed by the target luminance, not by a weight.
714
+ * A weight made the fill follow the terminal's swatch, and a bright terminal
715
+ * green turned every successful card into a pale one.
716
+ */
717
+ const stateCard = (tint, target) => toHex(tintAtLuminance(background, tint, target));
718
+ /** Relative luminance of a shipped swatch, keeping the light palette's own paper tone. */
719
+ const swatchLuminance = (color) => {
720
+ const [r, g, b] = hexToRgb(base[color]);
721
+ return relativeLuminance({ r, g, b });
722
+ };
723
+ // A light terminal wants the same three states as tinted paper, so its cards
724
+ // are held at the light palette's own lightness rather than inverted.
725
+ const target = (state) => light
726
+ ? swatchLuminance(`tool${state[0].toUpperCase()}${state.slice(1)}Bg`)
727
+ : SYSTEM_CARD_LUMINANCE[state];
728
+ // Pending is neutral, so it keeps the background's own hue and is only lifted in
729
+ // lightness: it must stay quieter than a settled card, and it must still read as
730
+ // belonging to this terminal rather than as a generic grey. Lifting the
731
+ // background keeps whatever faint tint it has; mixing toward the foreground
732
+ // instead traded that tint for the foreground's flat grey.
733
+ const pending = toHex(liftedToLuminance(background, target('pending')));
734
+ const successRgb = tintAtLuminance(background, successInk, target('success'));
735
+ const success = toHex(successRgb);
736
+ /**
737
+ * The alternating pair is a plain lightness step, and it has to stay one: two
738
+ * successive successful cards must be told apart, not two different states. It
739
+ * is derived from the success fill rather than from the background, because a
740
+ * success fill held at a fixed luminance no longer sits where the old background
741
+ * step assumed — measuring against the background made the pair collide and
742
+ * pushed the alternate surface to a brightness no card should reach.
743
+ */
744
+ const alt = toHex(tintAtLuminance(background, successInk, target('success') * SYSTEM_ALT_STEP));
745
+ const errorCard = stateCard(error, target('error'));
746
+ const cardFills = [pending, success, alt, errorCard].map(hexToRgb).map(([r, g, b]) => ({ r, g, b }));
747
+ /**
748
+ * The contrast the shipped `dim` reached against the shipped cards.
749
+ *
750
+ * That is the hierarchy to reproduce: the live cards are derived here, so a
751
+ * fixed weight cannot know how far it may weaken the ink before it disappears.
752
+ * `dim` is the only secondary ink that needs solving — it sits closest to the
753
+ * surface, so it is the first to become unreadable there.
754
+ */
755
+ const shippedDimContrast = () => {
756
+ const [r, g, b] = hexToRgb(base.dim);
757
+ const shippedCards = ['toolPendingBg', 'toolSuccessBg', 'toolAltBg', 'toolErrorBg']
758
+ .map(color => {
759
+ const [cr, cg, cb] = hexToRgb(base[color]);
760
+ return { r: cr, g: cg, b: cb };
761
+ });
762
+ return worstContrast({ r, g, b }, shippedCards);
763
+ };
764
+ return {
765
+ ...base,
766
+ text: toHex(foreground),
767
+ /**
768
+ * `muted` is not derived. Both shipped palettes set it to the same swatch as
769
+ * `toolOutput`, and that swatch already clears its cards on either host;
770
+ * deriving it separately only produced a second, near-identical grey that
771
+ * sometimes landed on the wrong side of the ink it was meant to match.
772
+ */
773
+ muted: base.muted,
774
+ dim: toHex(quietInk(foreground, background, cardFills, shippedDimContrast())),
775
+ userMessageBg: toHex(mixRgb(background, accent, 0.14)),
776
+ toolPendingBg: pending,
777
+ toolSuccessBg: success,
778
+ toolAltBg: alt,
779
+ toolErrorBg: errorCard,
780
+ };
781
+ }
782
+ /** 16-color SGR for one ANSI palette index, as ink or as a fill. */
783
+ function ansiIndexSgr(index, asBackground) {
784
+ const base = asBackground ? 4 : 3;
785
+ if (index < 8)
786
+ return `\x1b[${base}${index}m`;
787
+ const bright = asBackground ? 10 : 9;
788
+ return `\x1b[${bright}${index - 8}m`;
750
789
  }
751
790
  /**
752
791
  * True when the environment asks for no color at all: a non-empty `NO_COLOR`
@@ -757,41 +796,99 @@ export function colorDisabledByEnv(env = process.env) {
757
796
  return (env.NO_COLOR !== undefined && env.NO_COLOR !== '') || env.FORCE_COLOR === '0';
758
797
  }
759
798
  /**
760
- * Detect 24-bit color. `NO_COLOR` (any non-empty value) and `FORCE_COLOR=0`
761
- * suppress 24-bit color and outrank every capability hint; `COLORTERM` and
762
- * Windows Terminal enable it, and only known 16-color hosts disqualify it.
799
+ * Detect the color depth. `NO_COLOR` (any non-empty value) and `FORCE_COLOR=0`
800
+ * outrank every capability hint and leave 16 colors; `COLORTERM` and Windows
801
+ * Terminal prove 24-bit color. Apple Terminal advertises `xterm-256color` but
802
+ * misdraws 24-bit sequences on the releases still in use, so it gets the
803
+ * 256-color cube. Known 16-color hosts stay at 16, and every other terminal is
804
+ * assumed to be a modern 24-bit one.
763
805
  */
764
- export function detectTrueColor(env = process.env) {
806
+ export function detectColorDepth(env = process.env) {
765
807
  if (colorDisabledByEnv(env))
766
- return false;
767
- const colorterm = env.COLORTERM ?? '';
808
+ return 'ansi16';
809
+ const colorterm = (env.COLORTERM ?? '').toLowerCase();
768
810
  if (colorterm === 'truecolor' || colorterm === '24bit')
769
- return true;
811
+ return 'truecolor';
770
812
  if (env.WT_SESSION)
771
- return true;
813
+ return 'truecolor';
772
814
  const term = env.TERM ?? '';
773
815
  if (term === 'dumb' || term === '' || term === 'linux')
774
- return false;
775
- return true;
816
+ return 'ansi16';
817
+ if (env.TERM_PROGRAM === 'Apple_Terminal') {
818
+ const build = Number.parseInt((env.TERM_PROGRAM_VERSION ?? '').split('.')[0] ?? '', 10);
819
+ return Number.isInteger(build) && build >= FIRST_TRUECOLOR_APPLE_TERMINAL_BUILD ? 'truecolor' : 'ansi256';
820
+ }
821
+ return 'truecolor';
776
822
  }
823
+ /** Terminal 2.15, macOS 26 Tahoe, is the first Apple Terminal with real 24-bit color. */
824
+ const FIRST_TRUECOLOR_APPLE_TERMINAL_BUILD = 464;
777
825
  /**
778
826
  * Build a theme.
779
827
  * @param colors - emit SGR when true.
780
- * @param trueColor - 24-bit hex; ignored when colors is false.
828
+ * @param depth - color depth, or a boolean for 24-bit versus 16 colors; ignored when colors is false.
781
829
  * @param name - shipped palette (`dark` default).
830
+ * @param terminal - what the terminal reported about itself, when a probe answered.
782
831
  */
783
- export function createTheme(colors, trueColor = detectTrueColor(), name = 'dark') {
784
- const tc = colors && trueColor;
785
- const palette = PALETTES[name];
832
+ export function createTheme(colors, depth = detectColorDepth(), name = 'dark', terminal) {
833
+ const resolvedDepth = depth === true ? 'truecolor' : depth === false ? 'ansi16' : depth;
834
+ const tc = colors && resolvedDepth === 'truecolor';
835
+ const palette = name === 'system' ? systemPalette(terminal) : PALETTES[name];
786
836
  const ansi = ANSI16[name];
787
837
  const ansiBg = ANSI16_BG[name];
788
- const getFgAnsi = (color) => colors ? fgCode(palette[color], tc, ansi[color]) : '';
789
- const getBgAnsi = (color) => colors ? bgCode(palette[color], tc, ansiBg[color]) : '';
838
+ // The reported sixteen colors decide the encoding only on a 16-color
839
+ // terminal; 256- and 24-bit terminals encode the exact swatch themselves.
840
+ const reported = resolvedDepth === 'ansi16' && terminal?.palette !== undefined && terminal.palette.length === 16 ? terminal.palette : undefined;
841
+ /**
842
+ * Without 24-bit color a hex swatch is quantized to the entry the terminal
843
+ * actually has, which is perceptual nearest rather than a fixed code shift.
844
+ */
845
+ const quantized = (swatch, asBackground) => {
846
+ if (reported === undefined || typeof swatch !== 'string' || swatch === '')
847
+ return undefined;
848
+ const [r, g, b] = hexToRgb(swatch);
849
+ const index = nearestPaletteIndex({ r, g, b }, reported);
850
+ return index === undefined ? undefined : ansiIndexSgr(index, asBackground);
851
+ };
852
+ const getFgAnsi = (color) => {
853
+ if (!colors)
854
+ return '';
855
+ return quantized(palette[color], false) ?? swatchCode(palette[color], resolvedDepth, ansi[color], 38);
856
+ };
857
+ const rawBgAnsi = (color) => {
858
+ if (!colors)
859
+ return '';
860
+ return quantized(palette[color], true) ?? swatchCode(palette[color], resolvedDepth, ansiBg[color], 48);
861
+ };
862
+ /**
863
+ * A tool card must keep telling pending from done from failed on every
864
+ * terminal, so the three fills are resolved together rather than one at a
865
+ * time.
866
+ *
867
+ * A palette with only sixteen backgrounds, or a 256-color cube with six
868
+ * levels per channel, has no entries that differ by the handful of units the
869
+ * truecolor swatches do: nearest-neighbour quantisation sent pending,
870
+ * success, and error to one and the same black. Every icon that says which
871
+ * state a card is in was therefore invisible on any terminal that was not
872
+ * truecolor.
873
+ */
874
+ const toolFills = colors && !tc ? resolveToolFills(name, resolvedDepth, terminal) : undefined;
875
+ const fillsFor = (color) => toolFills !== undefined && Object.hasOwn(toolFills, color)
876
+ ? toolFills[color]
877
+ : rawBgAnsi(color);
878
+ /**
879
+ * A 16- or 256-color terminal can still collapse the two *alternating* card
880
+ * surfaces onto one entry. The alternating card then gives up its fill instead
881
+ * of pretending to have a second one, which keeps consecutive calls readable.
882
+ */
883
+ const altBg = colors && !tc && fillsFor('toolSuccessBg') === fillsFor('toolAltBg') ? BG_RESET : undefined;
884
+ const getBgAnsi = (color) => color === 'toolAltBg' && altBg !== undefined ? altBg : fillsFor(color);
790
885
  const paint = (open, text, close) => colors && open !== '' ? open + text + close : text;
791
886
  return {
792
887
  name,
793
888
  colors,
794
889
  trueColor: tc,
890
+ depth: resolvedDepth,
891
+ key: [name, colors ? 1 : 0, resolvedDepth, terminalColorsKey(terminal)].join('|'),
795
892
  getFgAnsi,
796
893
  getBgAnsi,
797
894
  fg: (color, text) => paint(getFgAnsi(color), text, FG_RESET),