@visual-z/xeno 0.2.1 → 0.4.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 (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -1,700 +0,0 @@
1
- /** Ported from Pi (MIT), coding-agent/theme/system-theme.ts at 02eed88. Only imports/types adapted. */
2
- /**
3
- * The `system` theme: pi's colors derived from the terminal's own theme.
4
- *
5
- * Every token belongs to a color family (its hue) and has contrast rules: it must reach a contrast level
6
- * on the background and on the panels it is drawn on. Hue and saturation come from the terminal's palette
7
- * color for the family's ANSI slot, or from the family's own hue when the terminal reports no palette.
8
- * Lightness comes from the rules alone. Colors are built in OKHSL, whose saturation is relative to the
9
- * sRGB gamut, and fade toward gray near black and white.
10
- *
11
- * A contrast level is a target-lightness curve: the OKLab lightness a token needs, given the lightness of
12
- * the surface below it. The curves were fitted to the reference theme design from the "Pi themes: system
13
- * and light/dark" review. On dark backgrounds they aim for nearly fixed lightness; on light backgrounds the
14
- * required difference grows as the background darkens.
15
- *
16
- * Depending on what the terminal reports, the theme is generated in one of three tiers:
17
- * - background and palette: hues from the palette, lightness from the background;
18
- * - background only: the families' own hues, lightness from the background;
19
- * - nothing: ANSI palette indices and the default colors, which the terminal renders itself.
20
- */
21
-
22
- import {
23
- colorToOkhsl,
24
- colorToOklch,
25
- type OkhslChannels,
26
- okhslColor,
27
- oklabToOkhslLightness,
28
- type RgbColor,
29
- rgbColor,
30
- } from "../vendor/pi-tui/index.ts";
31
- export type ThemeColor =
32
- | "accent"
33
- | "border"
34
- | "borderAccent"
35
- | "borderMuted"
36
- | "success"
37
- | "error"
38
- | "warning"
39
- | "muted"
40
- | "dim"
41
- | "text"
42
- | "thinkingText"
43
- | "scrollbarTrack"
44
- | "scrollbarThumb"
45
- | "searchMatchText"
46
- | "userMessageText"
47
- | "customMessageText"
48
- | "customMessageLabel"
49
- | "toolTitle"
50
- | "toolOutput"
51
- | "mdHeading"
52
- | "mdLink"
53
- | "mdLinkUrl"
54
- | "mdCode"
55
- | "mdCodeBlock"
56
- | "mdCodeBlockBorder"
57
- | "mdQuote"
58
- | "mdQuoteBorder"
59
- | "mdHr"
60
- | "mdListBullet"
61
- | "toolDiffAdded"
62
- | "toolDiffRemoved"
63
- | "toolDiffContext"
64
- | "syntaxComment"
65
- | "syntaxKeyword"
66
- | "syntaxFunction"
67
- | "syntaxVariable"
68
- | "syntaxString"
69
- | "syntaxNumber"
70
- | "syntaxType"
71
- | "syntaxOperator"
72
- | "syntaxPunctuation"
73
- | "thinkingOff"
74
- | "thinkingMinimal"
75
- | "thinkingLow"
76
- | "thinkingMedium"
77
- | "thinkingHigh"
78
- | "thinkingXhigh"
79
- | "thinkingMax"
80
- | "bashMode";
81
-
82
- export type ThemeBg =
83
- | "selectedBg"
84
- | "searchMatchBg"
85
- | "userMessageBg"
86
- | "customMessageBg"
87
- | "toolPendingBg"
88
- | "toolSuccessBg"
89
- | "toolErrorBg";
90
-
91
- export type ThemeToken = ThemeColor | ThemeBg;
92
-
93
-
94
- type ThemeAppearance = "dark" | "light";
95
-
96
- export const SYSTEM_THEME_NAME = "system";
97
-
98
- // ============================================================================
99
- // Recipe: color families and their tokens
100
- // ============================================================================
101
-
102
- /** A family's OKHSL hue and saturation: `max` at mid lightness, falling toward `min` at black and white. */
103
- interface Family {
104
- hue: number;
105
- saturation: { min: number; max: number };
106
- /** ANSI palette slot the family takes its hue and saturation from. */
107
- slot: number;
108
- }
109
-
110
- const FAMILIES = {
111
- neutral: { hue: 231.49, saturation: { min: 0.02, max: 0.08 }, slot: 8 },
112
- blue: { hue: 231.49, saturation: { min: 0.1, max: 0.68 }, slot: 4 },
113
- green: { hue: 158.68, saturation: { min: 0.1, max: 0.76 }, slot: 2 },
114
- red: { hue: 20, saturation: { min: 0.1, max: 0.92 }, slot: 1 },
115
- yellow: { hue: 82.36, saturation: { min: 0.5, max: 1 }, slot: 3 },
116
- orange: { hue: 52, saturation: { min: 0.12, max: 0.85 }, slot: 3 },
117
- violet: { hue: 295, saturation: { min: 0.2, max: 0.6 }, slot: 5 },
118
- calamine: { hue: 202.43, saturation: { min: 0.1, max: 0.74 }, slot: 6 },
119
- thinkingSlate: { hue: 231.49, saturation: { min: 0.08, max: 0.2 }, slot: 4 },
120
- thinkingBlue: { hue: 231.49, saturation: { min: 0.2, max: 0.45 }, slot: 4 },
121
- thinkingPeriwinkle: { hue: 263.25, saturation: { min: 0.3, max: 0.6 }, slot: 6 },
122
- thinkingViolet: { hue: 295, saturation: { min: 0.4, max: 0.75 }, slot: 5 },
123
- thinkingMagenta: { hue: 337.5, saturation: { min: 0.5, max: 0.85 }, slot: 13 },
124
- thinkingRed: { hue: 20, saturation: { min: 0.95, max: 1 }, slot: 1 },
125
- } satisfies Record<string, Family>;
126
-
127
- type FamilyName = keyof typeof FAMILIES;
128
-
129
- const TOKEN_FAMILIES: Record<ThemeToken, FamilyName> = {
130
- selectedBg: "blue",
131
- searchMatchBg: "orange",
132
- userMessageBg: "blue",
133
- customMessageBg: "violet",
134
- toolPendingBg: "neutral",
135
- toolSuccessBg: "green",
136
- toolErrorBg: "red",
137
-
138
- text: "neutral",
139
- userMessageText: "neutral",
140
- customMessageText: "neutral",
141
- toolTitle: "neutral",
142
- syntaxOperator: "neutral",
143
- syntaxPunctuation: "neutral",
144
- muted: "neutral",
145
- dim: "neutral",
146
- thinkingText: "neutral",
147
- toolOutput: "neutral",
148
- mdLinkUrl: "neutral",
149
- mdQuote: "neutral",
150
- mdQuoteBorder: "neutral",
151
- mdHr: "neutral",
152
- mdCodeBlockBorder: "neutral",
153
- toolDiffContext: "neutral",
154
- syntaxComment: "neutral",
155
- scrollbarTrack: "neutral",
156
- scrollbarThumb: "neutral",
157
- searchMatchText: "neutral",
158
- borderMuted: "neutral",
159
-
160
- accent: "violet",
161
- borderAccent: "violet",
162
- customMessageLabel: "violet",
163
- mdCode: "violet",
164
- mdListBullet: "violet",
165
- syntaxType: "violet",
166
- border: "blue",
167
- mdLink: "blue",
168
- syntaxKeyword: "blue",
169
- syntaxVariable: "calamine",
170
- success: "green",
171
- mdCodeBlock: "green",
172
- toolDiffAdded: "green",
173
- bashMode: "green",
174
- syntaxNumber: "green",
175
- error: "red",
176
- toolDiffRemoved: "red",
177
- warning: "yellow",
178
- mdHeading: "yellow",
179
- syntaxFunction: "yellow",
180
- syntaxString: "orange",
181
-
182
- thinkingOff: "neutral",
183
- thinkingMinimal: "thinkingSlate",
184
- thinkingLow: "thinkingBlue",
185
- thinkingMedium: "thinkingPeriwinkle",
186
- thinkingHigh: "thinkingViolet",
187
- thinkingXhigh: "thinkingMagenta",
188
- thinkingMax: "thinkingRed",
189
- };
190
-
191
- /** Palette slots for tokens that would otherwise share a hue with a similar token. */
192
- const TOKEN_SLOTS: Partial<Record<ThemeToken, number>> = { syntaxString: 2, syntaxNumber: 5, searchMatchBg: 3 };
193
-
194
- // ============================================================================
195
- // Contrast levels and rules
196
- // ============================================================================
197
-
198
- /**
199
- * Target-lightness curves: a polynomial in the surface's OKLab lightness giving the OKLab lightness a token
200
- * needs on it. `reachable` is the range of surface lightness where the level can be reached; beyond it the
201
- * level is relaxed.
202
- */
203
- interface Curve {
204
- coefficients: number[];
205
- reachable: [number, number];
206
- }
207
-
208
- const LEVELS = {
209
- panel: {
210
- dark: { coefficients: [0.29131, -0.39746, 2.33185, -0.85524, -1.2076, 0.86276], reachable: [0, 0.979] },
211
- light: { coefficients: [-3.74073, 27.94549, -78.44258, 112.6798, -79.60015, 22.11277], reachable: [0.348, 1] },
212
- },
213
- track: {
214
- dark: { coefficients: [0.39028, -0.23015, 0.83573, 2.43829, -4.38292, 2.01582], reachable: [0, 0.946] },
215
- light: { coefficients: [-5.24921, 38.37322, -107.28833, 152.10005, -106.17127, 29.18061], reachable: [0.368, 1] },
216
- },
217
- thinking0: {
218
- dark: { coefficients: [0.52988, -0.05809, -0.30924, 4.63567, -6.52933, 2.89108], reachable: [0, 0.873] },
219
- light: {
220
- coefficients: [-28.27749, 182.85284, -469.62416, 603.15916, -384.59976, 97.35147],
221
- reachable: [0.51, 1],
222
- },
223
- },
224
- thinking1: {
225
- dark: { coefficients: [0.55278, -0.03667, -0.45659, 4.95347, -6.90265, 3.0706], reachable: [0, 0.858] },
226
- light: {
227
- coefficients: [-37.10484, 235.86282, -596.62344, 754.3633, -474.00763, 118.3551],
228
- reachable: [0.535, 1],
229
- },
230
- },
231
- thinking2: {
232
- dark: { coefficients: [0.57486, -0.01765, -0.58987, 5.25227, -7.27175, 3.25532], reachable: [0, 0.842] },
233
- light: {
234
- coefficients: [-59.89653, 377.05024, -945.07843, 1182.03145, -734.96375, 181.68658],
235
- reachable: [0.556, 1],
236
- },
237
- },
238
- thinking3: {
239
- dark: { coefficients: [0.59621, -0.00062, -0.71148, 5.53588, -7.6392, 3.44606], reachable: [0, 0.827] },
240
- light: {
241
- coefficients: [-72.07122, 445.84082, -1099.57352, 1353.88793, -829.53392, 202.26164],
242
- reachable: [0.58, 1],
243
- },
244
- },
245
- thinking4: {
246
- dark: { coefficients: [0.61691, 0.01462, -0.82288, 5.80651, -8.00641, 3.64333], reachable: [0, 0.811] },
247
- light: {
248
- coefficients: [-110.14338, 674.21488, -1645.75941, 2004.32367, -1215.15899, 293.3183],
249
- reachable: [0.6, 1],
250
- },
251
- },
252
- thinking5: {
253
- dark: { coefficients: [0.63702, 0.02826, -0.92498, 6.06465, -8.37246, 3.84651], reachable: [0, 0.795] },
254
- light: {
255
- coefficients: [-175.47701, 1063.54495, -2570.70594, 3098.80776, -1860.15527, 444.76392],
256
- reachable: [0.62, 1],
257
- },
258
- },
259
- thinking6: {
260
- dark: { coefficients: [0.65658, 0.04044, -1.01835, 6.30989, -8.73529, 4.05439], reachable: [0, 0.779] },
261
- light: {
262
- coefficients: [-183.81712, 1094.70055, -2602.68539, 3088.71276, -1826.91131, 430.75931],
263
- reachable: [0.643, 1],
264
- },
265
- },
266
- subtle: {
267
- dark: { coefficients: [0.56762, -0.02475, -0.5383, 5.12628, -7.10931, 3.17324], reachable: [0, 0.848] },
268
- light: {
269
- coefficients: [-232.85459, 1376.54473, -3249.11801, 3827.91186, -2248.29472, 526.55751],
270
- reachable: [0.657, 1],
271
- },
272
- },
273
- thumb: {
274
- dark: { coefficients: [0.60323, 0.00278, -0.73328, 5.57157, -7.68067, 3.46933], reachable: [0, 0.823] },
275
- light: {
276
- coefficients: [-82.89897, 511.01355, -1255.98095, 1540.76821, -940.68087, 228.58523],
277
- reachable: [0.586, 1],
278
- },
279
- },
280
- readable: {
281
- dark: { coefficients: [0.66937, 0.04704, -1.06871, 6.43941, -8.9332, 4.17229], reachable: [0, 0.77] },
282
- light: {
283
- coefficients: [-1554.52576, 8733.56817, -19604.93507, 21977.72696, -12300.99599, 2749.81288],
284
- reachable: [0.751, 1],
285
- },
286
- },
287
- emphasis: {
288
- dark: { coefficients: [0.7303, 0.07695, -1.31626, 7.1681, -10.14436, 4.92846], reachable: [0, 0.712] },
289
- light: {
290
- coefficients: [-4948.31942, 26870.91986, -58334.48399, 63280.17197, -34298.01053, 7430.30146],
291
- reachable: [0.811, 1],
292
- },
293
- },
294
- textOnPanel: {
295
- dark: { coefficients: [0.86713, 0.05232, -0.89428, 4.79014, -5.5432, 1.75023], reachable: [0, 0.542] },
296
- light: {
297
- coefficients: [-8570.89457, 43954.60805, -90084.00702, 92220.6791, -47152.15802, 9632.27113],
298
- reachable: [0.867, 1],
299
- },
300
- },
301
- text: {
302
- dark: { coefficients: [0.89242, 0.02311, -0.44862, 2.34417, -0.06084, -2.63844], reachable: [0, 0.5] },
303
- light: {
304
- coefficients: [-2004.67048, 6664.47299, -6060.70202, -1792.61209, 5133.82359, -1939.85583],
305
- reachable: [0.894, 1],
306
- },
307
- },
308
- } satisfies Record<string, Record<ThemeAppearance, Curve>>;
309
-
310
- type Level = keyof typeof LEVELS;
311
-
312
- type Surface = ThemeBg | "background" | "scrollbarTrack";
313
-
314
- interface Rule {
315
- token: ThemeToken;
316
- on: Surface[];
317
- level: Level;
318
- }
319
-
320
- const TOOL_PANELS: Surface[] = ["toolPendingBg", "toolSuccessBg", "toolErrorBg"];
321
- const MESSAGE_PANELS: Surface[] = ["userMessageBg", "customMessageBg"];
322
- const PANELS: ThemeBg[] = [
323
- "userMessageBg",
324
- "toolPendingBg",
325
- "toolSuccessBg",
326
- "toolErrorBg",
327
- "selectedBg",
328
- "searchMatchBg",
329
- "customMessageBg",
330
- ];
331
- const THINKING: ThemeColor[] = [
332
- "thinkingOff",
333
- "thinkingMinimal",
334
- "thinkingLow",
335
- "thinkingMedium",
336
- "thinkingHigh",
337
- "thinkingXhigh",
338
- "thinkingMax",
339
- ];
340
- const THINKING_LEVELS: Level[] = [
341
- "thinking0",
342
- "thinking1",
343
- "thinking2",
344
- "thinking3",
345
- "thinking4",
346
- "thinking5",
347
- "thinking6",
348
- ];
349
-
350
- const each = (tokens: ThemeToken[], on: Surface[], level: Level): Rule[] =>
351
- tokens.map((token) => ({ token, on, level }));
352
-
353
- const RULES: Rule[] = [
354
- ...each(PANELS, ["background"], "panel"),
355
- { token: "text", on: ["background"], level: "text" },
356
- { token: "text", on: ["selectedBg"], level: "textOnPanel" },
357
- { token: "userMessageText", on: ["userMessageBg"], level: "textOnPanel" },
358
- { token: "toolTitle", on: TOOL_PANELS, level: "textOnPanel" },
359
- ...each(["accent", "success", "error", "warning"], ["background", "selectedBg", ...TOOL_PANELS], "readable"),
360
- { token: "muted", on: ["background", "selectedBg", "customMessageBg", ...TOOL_PANELS], level: "readable" },
361
- { token: "dim", on: ["background", "selectedBg", "customMessageBg", ...TOOL_PANELS], level: "subtle" },
362
- { token: "thinkingText", on: ["background"], level: "readable" },
363
- { token: "customMessageText", on: ["customMessageBg", ...TOOL_PANELS], level: "readable" },
364
- {
365
- token: "customMessageLabel",
366
- on: ["background", "customMessageBg", "selectedBg", ...TOOL_PANELS],
367
- level: "readable",
368
- },
369
- { token: "toolOutput", on: ["background", ...TOOL_PANELS], level: "readable" },
370
- ...each(
371
- ["mdHeading", "mdLink", "mdLinkUrl", "mdCode", "mdQuote", "mdCodeBlockBorder", "mdListBullet"],
372
- ["background", ...MESSAGE_PANELS],
373
- "readable",
374
- ),
375
- { token: "mdCodeBlock", on: ["background", ...MESSAGE_PANELS, ...TOOL_PANELS], level: "readable" },
376
- ...each(["toolDiffAdded", "toolDiffRemoved", "toolDiffContext"], ["background", ...TOOL_PANELS], "readable"),
377
- ...each(
378
- [
379
- "syntaxComment",
380
- "syntaxKeyword",
381
- "syntaxFunction",
382
- "syntaxVariable",
383
- "syntaxString",
384
- "syntaxNumber",
385
- "syntaxType",
386
- "syntaxOperator",
387
- "syntaxPunctuation",
388
- ],
389
- ["background", ...MESSAGE_PANELS, ...TOOL_PANELS],
390
- "readable",
391
- ),
392
- { token: "searchMatchText", on: ["searchMatchBg"], level: "readable" },
393
- ...each(["bashMode", "border", "borderAccent"], ["background"], "readable"),
394
- { token: "borderMuted", on: ["background"], level: "subtle" },
395
- ...each(["mdQuoteBorder", "mdHr"], ["background", ...MESSAGE_PANELS, ...TOOL_PANELS], "readable"),
396
- { token: "scrollbarTrack", on: ["background"], level: "track" },
397
- { token: "scrollbarThumb", on: ["scrollbarTrack"], level: "thumb" },
398
- ...THINKING.map((token, index): Rule => ({ token, on: ["background"], level: THINKING_LEVELS[index]! })),
399
- ];
400
-
401
- /** Relaxation compresses levels stronger than this one toward it before weakening all levels. */
402
- const READABLE_FLOOR: Record<ThemeAppearance, Level> = { dark: "readable", light: "subtle" };
403
-
404
- /** Body text uses the terminal's foreground when it reaches this level, which is clearly stronger than muted. */
405
- const FOREGROUND_LEVEL: Level = "emphasis";
406
-
407
- /** Text-level tokens that take the terminal's foreground. */
408
- const FOREGROUND_TOKENS: ThemeColor[] = ["text", "userMessageText", "toolTitle"];
409
-
410
- /** WCAG 2 contrast ratio that body text must reach on the surfaces it is drawn on. */
411
- const TEXT_MINIMUM_WCAG_CONTRAST = 4.5;
412
-
413
- /** Tokens in dependency order: every surface before the tokens drawn on it. */
414
- const SOLVE_ORDER: ThemeToken[] = (() => {
415
- const order: ThemeToken[] = [];
416
- const visit = (token: ThemeToken): void => {
417
- if (order.includes(token)) return;
418
- for (const rule of RULES) {
419
- if (rule.token !== token) continue;
420
- for (const surface of rule.on) if (surface !== "background") visit(surface);
421
- }
422
- order.push(token);
423
- };
424
- for (const rule of RULES) visit(rule.token);
425
- return order;
426
- })();
427
-
428
- // ============================================================================
429
- // Public API
430
- // ============================================================================
431
-
432
- export interface SystemThemeInput {
433
- foreground?: RgbColor;
434
- background?: RgbColor;
435
- /** ANSI colors 0-15. */
436
- palette?: RgbColor[];
437
- /** Saturation multiplier from 0 (grayscale) to 1. The first frame renders in grayscale until colors arrive. */
438
- saturation?: number;
439
- /** Appearance when the terminal did not report its background, e.g. from its light/dark report or COLORFGBG. */
440
- appearanceHint?: ThemeAppearance;
441
- }
442
-
443
- export interface SystemThemeColors {
444
- /** Hex colors, ANSI palette indices, or "" for the terminal default. */
445
- colors: Record<ThemeToken, string | number>;
446
- /** Foreground tokens rendered faint (SGR 2), for terminals that did not report colors. */
447
- dim: ThemeColor[];
448
- appearance: ThemeAppearance | undefined;
449
- }
450
-
451
- /** OKLab lightness of an sRGB color, 0-1. */
452
- function oklabLightness(color: RgbColor): number {
453
- return colorToOklch(rgbColor(color.r, color.g, color.b)).l;
454
- }
455
-
456
- /** WCAG 2 relative luminance. */
457
- export function relativeLuminance({ r, g, b }: RgbColor): number {
458
- const linear = (channel: number) => {
459
- const value = channel / 255;
460
- return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
461
- };
462
- return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
463
- }
464
-
465
- /** WCAG 2 contrast ratio, 1-21. */
466
- export function wcagContrast(first: RgbColor, second: RgbColor): number {
467
- const a = relativeLuminance(first);
468
- const b = relativeLuminance(second);
469
- return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
470
- }
471
-
472
- /**
473
- * Whether a terminal is dark or light, from its reported colors: the direction of its own foreground
474
- * when text can be readable that way, otherwise dark when white text has more contrast on the
475
- * background than black text.
476
- */
477
- export function terminalAppearance(background: RgbColor, foreground?: RgbColor): ThemeAppearance {
478
- const white = { r: 255, g: 255, b: 255 };
479
- const black = { r: 0, g: 0, b: 0 };
480
- const whiteContrast = wcagContrast(white, background);
481
- const blackContrast = wcagContrast(black, background);
482
- if (foreground) {
483
- const foregroundL = oklabLightness(foreground);
484
- const backgroundL = oklabLightness(background);
485
- if (Math.abs(foregroundL - backgroundL) > 0.05) {
486
- const appearance = foregroundL > backgroundL ? "dark" : "light";
487
- const best = appearance === "dark" ? whiteContrast : blackContrast;
488
- if (best >= TEXT_MINIMUM_WCAG_CONTRAST) return appearance;
489
- }
490
- }
491
- return whiteContrast >= blackContrast ? "dark" : "light";
492
- }
493
-
494
- // ============================================================================
495
- // Generation
496
- // ============================================================================
497
-
498
- const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
499
-
500
- function hexOf({ r, g, b }: RgbColor): string {
501
- return `#${[r, g, b].map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("")}`;
502
- }
503
-
504
- /** Saturation weight at a lightness: a Gaussian (center 0.5, sigma 0.25), 0 at black and white, 1 in the middle. */
505
- function bellWeight(lightness: number): number {
506
- const gaussian = (x: number): number => Math.exp(-((x - 0.5) ** 2) / (2 * 0.25 ** 2));
507
- return (gaussian(lightness) - gaussian(0)) / (1 - gaussian(0));
508
- }
509
-
510
- /** A family's saturation curve relative to its maximum: 1 at mid lightness, `min / max` at black and white. */
511
- function saturationCurve({ saturation: { min, max } }: Family, lightness: number): number {
512
- const floor = max > 0 ? min / max : 1;
513
- return floor + (1 - floor) * bellWeight(lightness);
514
- }
515
-
516
- /** The target lightness for a level on a surface, or undefined where the level cannot be reached. */
517
- function levelTarget(level: Level, appearance: ThemeAppearance, surfaceL: number): number | undefined {
518
- const curve: Curve = LEVELS[level][appearance];
519
- if (surfaceL < curve.reachable[0] || surfaceL > curve.reachable[1]) return undefined;
520
- return curve.coefficients.reduce((sum, coefficient, power) => sum + coefficient * surfaceL ** power, 0);
521
- }
522
-
523
- /**
524
- * Generate the system theme's colors from the terminal's reported colors.
525
- */
526
- export function generateSystemThemeColors(input: SystemThemeInput): SystemThemeColors {
527
- const saturation = clamp(input.saturation ?? 1, 0, 1);
528
- const { background, foreground } = input;
529
- if (!background) return indexedColors(saturation, input.appearanceHint);
530
- const palette = input.palette?.length === 16 ? input.palette.map(okhslOf) : undefined;
531
-
532
- const appearance = terminalAppearance(background, foreground);
533
- const lighter = appearance === "dark";
534
- const extreme = lighter ? 1 : 0;
535
- const backgroundL = oklabLightness(background);
536
-
537
- /**
538
- * A token's color at an OKLab lightness. With a palette, the palette color's saturation applies at its
539
- * own lightness and falls off toward black and white along the family's curve, never rising above it.
540
- */
541
- const paint = (token: ThemeToken, oklabL: number): RgbColor => {
542
- const lightness = oklabToOkhslLightness(oklabL);
543
- const family: Family = FAMILIES[TOKEN_FAMILIES[token]];
544
- if (!palette) {
545
- const { min, max } = family.saturation;
546
- return okhslColor(family.hue, (min + (max - min) * bellWeight(lightness)) * saturation, lightness);
547
- }
548
- return anchored(palette[TOKEN_SLOTS[token] ?? family.slot]!, family, lightness, saturation);
549
- };
550
-
551
- /**
552
- * The lightness a rule needs on a surface, relaxed by `t`: from 0 to 1, levels stronger than the readable
553
- * floor move toward it; from 1 to 2, all levels move toward the surface itself.
554
- */
555
- const target = (level: Level, surfaceL: number, t: number): number | undefined => {
556
- const reached = levelTarget(level, appearance, surfaceL);
557
- if (reached === undefined && t === 0) return undefined;
558
- const distance = (reached ?? extreme) - surfaceL;
559
- const floor = (levelTarget(READABLE_FLOOR[appearance], appearance, surfaceL) ?? extreme) - surfaceL;
560
- const compressed =
561
- Math.abs(distance) > Math.abs(floor) ? distance - (distance - floor) * Math.min(t, 1) : distance;
562
- return surfaceL + compressed * (1 - Math.max(0, t - 1));
563
- };
564
-
565
- /**
566
- * Keep a panel light enough (or dark enough) that white (or black) text still reaches the body text
567
- * minimum on it. This only matters for backgrounds near mid-gray, where it barely does on the background.
568
- */
569
- const extremeText = lighter ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 };
570
- const readable = (color: RgbColor) => wcagContrast(extremeText, color) >= TEXT_MINIMUM_WCAG_CONTRAST;
571
- const limitPanel = (token: ThemeToken, l: number): RgbColor => {
572
- const color = paint(token, l);
573
- if (readable(color)) return color;
574
- let [low, high] = [backgroundL, l];
575
- for (let index = 0; index < 20; index++) {
576
- const middle = (low + high) / 2;
577
- if (readable(paint(token, middle))) low = middle;
578
- else high = middle;
579
- }
580
- return paint(token, low);
581
- };
582
-
583
- const solve = (t: number): Map<Surface | ThemeToken, RgbColor> | undefined => {
584
- const colors = new Map<Surface | ThemeToken, RgbColor>([["background", background]]);
585
- for (const token of SOLVE_ORDER) {
586
- const targets: number[] = [];
587
- for (const rule of RULES) {
588
- if (rule.token !== token) continue;
589
- for (const surface of rule.on) {
590
- const value = target(rule.level, oklabLightness(colors.get(surface) ?? background), t);
591
- if (value === undefined || value < 0 || value > 1) return undefined;
592
- targets.push(value);
593
- }
594
- }
595
- const l = lighter ? Math.max(...targets) : Math.min(...targets);
596
- colors.set(token, PANELS.includes(token as ThemeBg) ? limitPanel(token, l) : paint(token, l));
597
- }
598
- return colors;
599
- };
600
-
601
- let relaxation = 0;
602
- let colors = solve(0);
603
- if (!colors) {
604
- // Mid-gray backgrounds cannot fit every level: relax as little as possible. Full relaxation always fits.
605
- let [low, high] = [0, 2];
606
- colors = solve(high);
607
- for (let index = 0; index < 20; index++) {
608
- const middle = (low + high) / 2;
609
- const attempt = solve(middle);
610
- if (attempt) [high, colors] = [middle, attempt];
611
- else low = middle;
612
- }
613
- relaxation = high;
614
- }
615
- const solved = colors ?? new Map<Surface | ThemeToken, RgbColor>();
616
- const surfacesOf = (token: ThemeToken) =>
617
- RULES.filter((rule) => rule.token === token).flatMap((rule) =>
618
- rule.on.map((surface) => solved.get(surface) ?? background),
619
- );
620
-
621
- const result = {} as Record<ThemeToken, string | number>;
622
- for (const token of Object.keys(TOKEN_FAMILIES) as ThemeToken[]) {
623
- const color = solved.get(token);
624
- result[token] = color ? hexOf(color) : "";
625
- }
626
-
627
- for (const token of FOREGROUND_TOKENS) {
628
- const surfaces = surfacesOf(token);
629
- // Body text uses the terminal's own foreground where it is clearly stronger than muted text; otherwise
630
- // the foreground's hue at just enough lightness.
631
- let text = solved.get(token);
632
- if (foreground) {
633
- const targets = surfaces.map((surface) => target(FOREGROUND_LEVEL, oklabLightness(surface), relaxation));
634
- if (targets.every((value) => value !== undefined && value >= 0 && value <= 1)) {
635
- const needed = lighter ? Math.max(...(targets as number[])) : Math.min(...(targets as number[]));
636
- const foregroundL = oklabLightness(foreground);
637
- if (lighter ? foregroundL >= needed : foregroundL <= needed) {
638
- result[token] = "";
639
- continue;
640
- }
641
- text = anchored(okhslOf(foreground), FAMILIES.neutral, oklabToOkhslLightness(needed), saturation);
642
- }
643
- }
644
- // Body text keeps at least 4.5:1 on the surfaces it is drawn on, even on relaxed mid-gray backgrounds.
645
- if (text) result[token] = hexOf(withTextContrast(text, surfaces, lighter));
646
- }
647
- return { colors: result, dim: [], appearance };
648
- }
649
-
650
- function okhslOf({ r, g, b }: RgbColor): OkhslChannels {
651
- return colorToOkhsl(rgbColor(r, g, b));
652
- }
653
-
654
- /**
655
- * A source color's hue at another OKHSL lightness. Its saturation applies at its own lightness and falls off
656
- * toward black and white along the family's saturation curve, never rising above it.
657
- */
658
- function anchored(source: OkhslChannels, family: Family, lightness: number, saturation: number): RgbColor {
659
- const anchor = saturationCurve(family, source.l);
660
- const falloff = anchor > 0 ? Math.min(1, saturationCurve(family, lightness) / anchor) : 1;
661
- return okhslColor(source.h, source.s * falloff * saturation, lightness);
662
- }
663
-
664
- /** Move a text color toward white or black until it reaches the WCAG minimum on every surface. */
665
- function withTextContrast(color: RgbColor, surfaces: RgbColor[], lighter: boolean): RgbColor {
666
- const meets = (candidate: RgbColor) =>
667
- surfaces.every((surface) => wcagContrast(candidate, surface) >= TEXT_MINIMUM_WCAG_CONTRAST);
668
- if (meets(color)) return color;
669
- const { h, s, l } = okhslOf(color);
670
- const at = (lightness: number) => okhslColor(h, s, lightness);
671
- const extreme = lighter ? 1 : 0;
672
- if (!meets(at(extreme))) return at(extreme);
673
- let [low, high] = [l, extreme];
674
- for (let index = 0; index < 20; index++) {
675
- const middle = (low + high) / 2;
676
- if (meets(at(middle))) high = middle;
677
- else low = middle;
678
- }
679
- return at(high);
680
- }
681
-
682
- /**
683
- * Colors for terminals that reported nothing: the terminal renders ANSI indices 0-15 and the default
684
- * colors with its own theme, so they fit any background. Neutral tokens below body text are faint (SGR 2)
685
- * instead of bright black, which some themes make nearly invisible. Panels have no background.
686
- */
687
- function indexedColors(saturation: number, appearance: ThemeAppearance | undefined): SystemThemeColors {
688
- const colors = {} as Record<ThemeToken, string | number>;
689
- const dim: ThemeColor[] = [];
690
- for (const [token, familyName] of Object.entries(TOKEN_FAMILIES) as [ThemeToken, FamilyName][]) {
691
- if (PANELS.includes(token as ThemeBg)) {
692
- colors[token] = "";
693
- continue;
694
- }
695
- const neutral = familyName === "neutral";
696
- colors[token] = !neutral && saturation > 0 ? (TOKEN_SLOTS[token] ?? FAMILIES[familyName].slot) : "";
697
- if (neutral && !FOREGROUND_TOKENS.includes(token as ThemeColor)) dim.push(token as ThemeColor);
698
- }
699
- return { colors, dim, appearance };
700
- }