mu-agent 0.1.7 → 0.1.10

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 (198) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +34 -29
  3. package/dist/bundle/chunks/anthropic-messages-4PAJY4R6.js +6 -0
  4. package/dist/bundle/chunks/anthropic.js +2 -2
  5. package/dist/bundle/chunks/azure-openai-responses-Y4BBWQ3N.js +2 -0
  6. package/dist/bundle/chunks/bedrock-converse-stream.js +12 -9
  7. package/dist/bundle/chunks/{chunk-TRDNDS6A.js → chunk-3V45WARH.js} +6 -3
  8. package/dist/bundle/chunks/{chunk-XDUWOHPL.js → chunk-5AGIVGSV.js} +3 -3
  9. package/dist/bundle/chunks/chunk-6QGV3IWD.js +6 -0
  10. package/dist/bundle/chunks/chunk-7CBJINYJ.js +20 -0
  11. package/dist/bundle/chunks/{chunk-DRQDKNXA.js → chunk-7DDKXS2Q.js} +1 -1
  12. package/dist/bundle/chunks/chunk-BSS6T6KQ.js +2 -0
  13. package/dist/bundle/chunks/chunk-CF5GWZHW.js +31 -0
  14. package/dist/bundle/chunks/chunk-DNNHZBQR.js +4 -0
  15. package/dist/bundle/chunks/chunk-FN6OEZYR.js +2 -0
  16. package/dist/bundle/chunks/chunk-FTJT2X6E.js +2 -0
  17. package/dist/bundle/chunks/chunk-HZNN636Z.js +130 -0
  18. package/dist/bundle/chunks/{chunk-4L3WN2XY.js → chunk-JXWJOIH4.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-4O2SCVYF.js → chunk-KIDDRTZP.js} +4 -4
  20. package/dist/bundle/chunks/chunk-LMNWTX2L.js +11 -0
  21. package/dist/bundle/chunks/chunk-LPXNMA2W.js +2 -0
  22. package/dist/bundle/chunks/{chunk-6CYCI3EJ.js → chunk-LVVZS2NO.js} +1 -1
  23. package/dist/bundle/chunks/chunk-NN4BEPYX.js +2 -0
  24. package/dist/bundle/chunks/chunk-O7IPTASE.js +1221 -0
  25. package/dist/bundle/chunks/chunk-PMRZO46Y.js +2 -0
  26. package/dist/bundle/chunks/chunk-QOEEJKS4.js +9 -0
  27. package/dist/bundle/chunks/chunk-R4VPBBDS.js +2 -0
  28. package/dist/bundle/chunks/chunk-RAQ6TU76.js +2 -0
  29. package/dist/bundle/chunks/chunk-RDZVAGPR.js +2 -0
  30. package/dist/bundle/chunks/chunk-SGOTYFMT.js +91 -0
  31. package/dist/bundle/chunks/chunk-TMTOQ7H7.js +2 -0
  32. package/dist/bundle/chunks/chunk-UKVNB6C4.js +2 -0
  33. package/dist/bundle/chunks/chunk-VJLOQZKR.js +9 -0
  34. package/dist/bundle/chunks/chunk-WCGU75WO.js +11 -0
  35. package/dist/bundle/chunks/chunk-X234222Q.js +64 -0
  36. package/dist/bundle/chunks/cli-722OL3K5.js +51 -0
  37. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-MEZHQVNP.js +2 -0
  38. package/dist/bundle/chunks/codemode-worker.js +478 -0
  39. package/dist/bundle/chunks/easter-egg-3d-6H34EDGX.js +2 -0
  40. package/dist/bundle/chunks/execute-KVSEIDWL.js +499 -0
  41. package/dist/bundle/chunks/github-copilot.js +1 -1
  42. package/dist/bundle/chunks/{google-generative-ai-C44KRRZ4.js → google-generative-ai-D2VJQBTK.js} +1 -1
  43. package/dist/bundle/chunks/{google-vertex-25DUXPTJ.js → google-vertex-SQG2BO6C.js} +1 -1
  44. package/dist/bundle/chunks/https-proxy-agent-LVOO5TPL.js +13 -0
  45. package/dist/bundle/chunks/image-resize-worker.js +1 -1
  46. package/dist/bundle/chunks/{jiti-loader-C3NU2SDH.js → jiti-loader-GL4HJFAD.js} +1 -1
  47. package/dist/bundle/chunks/{jiti-static-loader-E6ANQD5T.js → jiti-static-loader-2RCDI6B4.js} +1 -1
  48. package/dist/bundle/chunks/{lib-HGQMNYVT.js → lib-4AVRLBHM.js} +2 -2
  49. package/dist/bundle/chunks/llama-cpp-classify-5YHLY3RC.js +22 -0
  50. package/dist/bundle/chunks/mistral-conversations-BDARVJTT.js +5 -0
  51. package/dist/bundle/chunks/{multipart-parser-SQ6GDJL7.js → multipart-parser-FMGQ7342.js} +1 -1
  52. package/dist/bundle/chunks/{node-TWVVAGPO.js → node-AUOGOBCN.js} +1 -1
  53. package/dist/bundle/chunks/node-KCMANYC2.js +16 -0
  54. package/dist/bundle/chunks/openai-chatgpt.js +86 -0
  55. package/dist/bundle/chunks/openai-codex-responses-CECGTDYD.js +10 -0
  56. package/dist/bundle/chunks/openai-codex.js +2 -2
  57. package/dist/bundle/chunks/openai-completions-UOA7MELD.js +7 -0
  58. package/dist/bundle/chunks/openai-decisions-HLM372N6.js +5 -0
  59. package/dist/bundle/chunks/openai-responses-TII7ZY5X.js +3 -0
  60. package/dist/bundle/chunks/{openrouter-images-JVEJN44Q.js → openrouter-images-7T452AS5.js} +1 -1
  61. package/dist/bundle/chunks/openrouter.js +2 -2
  62. package/dist/bundle/chunks/{pi-messages-TM5SOOIW.js → pi-messages-2UDKLKOO.js} +1 -1
  63. package/dist/bundle/chunks/radius.js +2 -2
  64. package/dist/bundle/chunks/runtime-O3ELDOM7.js +2 -0
  65. package/dist/bundle/chunks/{src-UK77SYZE.js → src-QKGKDFSU.js} +1 -1
  66. package/dist/bundle/chunks/typesafe-system-one-KNCICMO5.js +2 -0
  67. package/dist/bundle/chunks/virtual-modules-IKEARUOZ.js +2 -0
  68. package/dist/bundle/cli-runtime.js +1 -1
  69. package/dist/bundle/index.js +1 -1
  70. package/dist/bundle/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  71. package/dist/bundle/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  72. package/dist/bundle/rpc-entry.js +1 -1
  73. package/dist/core/export-html/template.js +37 -16
  74. package/dist/modes/interactive/theme/dark.json +60 -73
  75. package/dist/modes/interactive/theme/light.json +58 -70
  76. package/dist/modes/interactive/theme/system-theme.d.ts +55 -0
  77. package/dist/modes/interactive/theme/system-theme.d.ts.map +1 -0
  78. package/dist/modes/interactive/theme/system-theme.js +546 -0
  79. package/dist/modes/interactive/theme/system-theme.js.map +1 -0
  80. package/dist/modes/interactive/theme/theme-controller.d.ts +43 -9
  81. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  82. package/dist/modes/interactive/theme/theme-controller.js +121 -79
  83. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  84. package/dist/modes/interactive/theme/theme-schema.d.ts +28 -0
  85. package/dist/modes/interactive/theme/theme-schema.d.ts.map +1 -0
  86. package/dist/modes/interactive/theme/theme-schema.js +105 -0
  87. package/dist/modes/interactive/theme/theme-schema.js.map +1 -0
  88. package/dist/modes/interactive/theme/theme-tokens.d.ts +257 -0
  89. package/dist/modes/interactive/theme/theme-tokens.d.ts.map +1 -0
  90. package/dist/modes/interactive/theme/theme-tokens.js +94 -0
  91. package/dist/modes/interactive/theme/theme-tokens.js.map +1 -0
  92. package/dist/modes/interactive/theme/theme.d.ts +47 -56
  93. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  94. package/dist/modes/interactive/theme/theme.js +149 -143
  95. package/dist/modes/interactive/theme/theme.js.map +1 -1
  96. package/dist/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  97. package/dist/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  98. package/docs/cli-integration.md +1 -1
  99. package/docs/cli.md +78 -13
  100. package/docs/codemode.md +230 -0
  101. package/docs/compaction.md +5 -5
  102. package/docs/configuration.md +2 -0
  103. package/docs/custom-provider.md +53 -1
  104. package/docs/docs.json +21 -1
  105. package/docs/environment-variables.md +5 -3
  106. package/docs/extensions.md +172 -10
  107. package/docs/index.md +1 -1
  108. package/docs/json.md +3 -3
  109. package/docs/keybindings.md +7 -6
  110. package/docs/llama-cpp.md +21 -1
  111. package/docs/mcp.md +272 -0
  112. package/docs/message-types.md +22 -4
  113. package/docs/models-configuration.md +187 -0
  114. package/docs/models.md +100 -2
  115. package/docs/nvidia-rtx-llama-cpp.md +96 -0
  116. package/docs/packages.md +1 -1
  117. package/docs/providers.md +66 -6
  118. package/docs/quickstart.md +16 -2
  119. package/docs/rpc-commands.md +9 -3
  120. package/docs/rpc.md +3 -1
  121. package/docs/sdk.md +8 -3
  122. package/docs/security.md +2 -0
  123. package/docs/session-format.md +5 -1
  124. package/docs/sessions.md +1 -1
  125. package/docs/settings.md +30 -8
  126. package/docs/terminal-setup.md +14 -0
  127. package/docs/themes.md +31 -7
  128. package/docs/tui.md +71 -1
  129. package/docs/usage.md +2 -2
  130. package/docs/virtual-models.md +114 -0
  131. package/docs/windows.md +2 -0
  132. package/examples/README.md +1 -4
  133. package/examples/extensions/README.md +1 -0
  134. package/examples/extensions/built-in-tool-renderer.ts +16 -40
  135. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  136. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  137. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  138. package/examples/extensions/custom-provider-gitlab-duo/test.ts +5 -5
  139. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  140. package/examples/extensions/gondolin/package-lock.json +2 -2
  141. package/examples/extensions/gondolin/package.json +1 -1
  142. package/examples/extensions/jev-router.ts +113 -0
  143. package/examples/extensions/minimal-mode.ts +28 -101
  144. package/examples/extensions/overlay-test.ts +2 -2
  145. package/examples/extensions/plan-mode/index.ts +6 -2
  146. package/examples/extensions/sandbox/package-lock.json +2 -2
  147. package/examples/extensions/sandbox/package.json +1 -1
  148. package/examples/extensions/subagent/index.ts +4 -6
  149. package/examples/extensions/with-deps/package-lock.json +2 -2
  150. package/examples/extensions/with-deps/package.json +1 -1
  151. package/examples/rpc-client.ts +1 -1
  152. package/examples/rpc-extension-ui.ts +1 -1
  153. package/examples/sdk/14-codemode-mcp.ts +61 -0
  154. package/examples/sdk/README.md +2 -1
  155. package/judge/THIRD_PARTY_NOTICES.md +63 -0
  156. package/judge/dist/auth.js +169 -123
  157. package/judge/dist/import.js +36 -30
  158. package/judge/dist/setup.js +250 -120
  159. package/judge/manifest.json +1008 -316
  160. package/judge/{dist/kyrn-judge.js → mu/index.js} +6933 -5995
  161. package/judge/themes/mu-dark.json +80 -0
  162. package/judge/themes/mu-light.json +80 -0
  163. package/kyrn/bin/kyrn-doctor +75 -25
  164. package/kyrn/bin/kyrn-judge-local +2 -2
  165. package/kyrn/bin/kyrn-ledger +1 -1
  166. package/kyrn/bin/mu.mjs +149 -107
  167. package/package.json +4 -5
  168. package/dist/bundle/chunks/anthropic-messages-VMESUHQ7.js +0 -6
  169. package/dist/bundle/chunks/azure-openai-responses-K232MQFF.js +0 -2
  170. package/dist/bundle/chunks/chunk-A3JYRWB6.js +0 -2
  171. package/dist/bundle/chunks/chunk-AYFRR3OD.js +0 -2
  172. package/dist/bundle/chunks/chunk-DTD7JQ7Y.js +0 -42
  173. package/dist/bundle/chunks/chunk-FU4XQUSL.js +0 -11
  174. package/dist/bundle/chunks/chunk-GNYZK6XN.js +0 -2
  175. package/dist/bundle/chunks/chunk-HEMXN7VJ.js +0 -1653
  176. package/dist/bundle/chunks/chunk-MVPXOVOJ.js +0 -2
  177. package/dist/bundle/chunks/chunk-NUHFSC37.js +0 -25
  178. package/dist/bundle/chunks/chunk-V7IXECST.js +0 -11
  179. package/dist/bundle/chunks/chunk-XNGRGP62.js +0 -2
  180. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-NR2TDCXU.js +0 -2
  181. package/dist/bundle/chunks/https-proxy-agent-2VXB7436.js +0 -13
  182. package/dist/bundle/chunks/mistral-conversations-6IHQOR4R.js +0 -5
  183. package/dist/bundle/chunks/node-O3RIBJRH.js +0 -16
  184. package/dist/bundle/chunks/openai-codex-responses-3UAYOSSR.js +0 -10
  185. package/dist/bundle/chunks/openai-completions-FIEQGWAH.js +0 -7
  186. package/dist/bundle/chunks/openai-responses-OZJXLHFC.js +0 -2
  187. package/dist/bundle/chunks/typesafe-system-one-VKYX4OGY.js +0 -2
  188. package/dist/bundle/chunks/virtual-modules-V7SXVVLP.js +0 -2
  189. package/dist/modes/interactive/theme/theme-json.d.ts +0 -85
  190. package/dist/modes/interactive/theme/theme-json.d.ts.map +0 -1
  191. package/dist/modes/interactive/theme/theme-json.js +0 -132
  192. package/dist/modes/interactive/theme/theme-json.js.map +0 -1
  193. package/dist/modes/interactive/theme/theme-schema.json +0 -361
  194. package/examples/plugins/pi-example-plugin/README.md +0 -38
  195. package/examples/plugins/pi-example-plugin/package.json +0 -10
  196. package/examples/plugins/pi-example-plugin/src/contract.ts +0 -13
  197. package/examples/plugins/pi-example-plugin/src/session.ts +0 -24
  198. package/examples/plugins/pi-example-plugin/src/tui.ts +0 -31
@@ -1,11 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { backgroundAnsi, colorToHex, colorToOklch, colorToRgb, foregroundAnsi, getTerminalColorMode, indexedColor, parseColor, rgbColor, styleTextWithAnsi, } from "@earendil-works/pi-tui";
3
+ import { backgroundAnsi, colorToHex, colorToOklch, foregroundAnsi, getTerminalColorMode, indexedColor, mixColors, parseColor, rgbColor, styleTextWithAnsi, } from "@earendil-works/pi-tui";
4
4
  import chalk from "chalk";
5
5
  import { getCustomThemesDir, getThemesDir } from "../../../config.js";
6
6
  import { closeWatcher, watchWithErrorHandler } from "../../../utils/fs-watch.js";
7
7
  import { highlight, supportsLanguage } from "../../../utils/syntax-highlight.js";
8
8
  import { stripBom } from "../../../utils/text.js";
9
+ import { generateSystemThemeColors, SYSTEM_THEME_NAME, terminalAppearance } from "./system-theme.js";
10
+ import { THEME_TOKENS, } from "./theme-tokens.js";
11
+ export { SYSTEM_THEME_NAME } from "./system-theme.js";
9
12
  let themeJsonValidator;
10
13
  /**
11
14
  * Install full theme validation. Without it, documents are accepted as-is, which is what built-in
@@ -19,7 +22,7 @@ export function setThemeJsonValidator(validator) {
19
22
  // Color Utilities
20
23
  // ============================================================================
21
24
  function resolveVarRefs(value, vars, visited = new Set()) {
22
- if (typeof value === "number" || value === "" || value.startsWith("#") || /^oklch\(/i.test(value)) {
25
+ if (typeof value === "number" || value === "" || value.startsWith("#") || /^ok(lch|hsl)\(/i.test(value)) {
23
26
  return value;
24
27
  }
25
28
  if (visited.has(value)) {
@@ -38,22 +41,45 @@ function resolveThemeColors(colors, vars = {}) {
38
41
  }
39
42
  return resolved;
40
43
  }
41
- function withThemeColorFallbacks(colors) {
42
- return {
43
- ...colors,
44
- scrollbarTrack: colors.scrollbarTrack ?? colors.muted,
45
- scrollbarThumb: colors.scrollbarThumb ?? colors.text,
46
- thinkingMax: colors.thinkingMax ?? colors.thinkingXhigh,
47
- searchMatchBg: colors.searchMatchBg ?? colors.selectedBg,
48
- searchMatchText: colors.searchMatchText ?? colors.text,
49
- };
44
+ function withThemeTokenFallbacks(values) {
45
+ const resolved = { ...values };
46
+ for (const [rawName, descriptor] of Object.entries(THEME_TOKENS)) {
47
+ if (!("fallback" in descriptor))
48
+ continue;
49
+ const name = rawName;
50
+ if (resolved[name] !== undefined)
51
+ continue;
52
+ const fallback = descriptor.fallback;
53
+ const fallbackValue = resolved[fallback];
54
+ if (fallbackValue === undefined) {
55
+ throw new Error(`Theme token ${rawName} has unresolved fallback ${descriptor.fallback}`);
56
+ }
57
+ resolved[name] = fallbackValue;
58
+ }
59
+ return resolved;
50
60
  }
51
- // Replaced (never mutated) on update, so themes can cache resolved colors by identity.
52
- let terminalDefaultColors = {};
53
- /** Record the terminal's reported default colors. Themes use them for tokens set to "" (terminal default). */
54
- export function setTerminalDefaultColors(colors) {
55
- const toColor = (rgb) => rgb && rgbColor(rgb.r, rgb.g, rgb.b);
56
- terminalDefaultColors = { foreground: toColor(colors.foreground), background: toColor(colors.background) };
61
+ // The terminal's reported colors. Replaced (never mutated) on update, so themes can cache resolved colors
62
+ // by identity.
63
+ let terminalColors = {};
64
+ // While the terminal color query is in flight, the system theme renders in grayscale.
65
+ let terminalColorsPending = false;
66
+ // The terminal's last light/dark report (mode 2031). Only used while it has not reported a background.
67
+ let terminalColorScheme;
68
+ /**
69
+ * Record the terminal's reported colors. Themes use the default colors for tokens set to "" (terminal
70
+ * default); the system theme is generated from all of them. Ends the pending state.
71
+ */
72
+ export function setTerminalColors(colors) {
73
+ terminalColors = { ...colors };
74
+ terminalColorsPending = false;
75
+ }
76
+ /** Record the terminal's light/dark report, the fallback for terminals that do not report their background. */
77
+ export function setTerminalColorScheme(scheme) {
78
+ terminalColorScheme = scheme;
79
+ }
80
+ /** Render the system theme in grayscale until `setTerminalColors()` reports the terminal's colors. */
81
+ export function markTerminalColorsPending() {
82
+ terminalColorsPending = true;
57
83
  }
58
84
  /** Assumed terminal default colors when the terminal does not report them. */
59
85
  const GUESSED_DEFAULT_COLORS = {
@@ -94,6 +120,8 @@ export class Theme {
94
120
  concreteColors = {};
95
121
  defaultForegroundTokens = [];
96
122
  defaultBackgroundTokens = [];
123
+ // Foreground tokens rendered faint (SGR 2) on top of their color.
124
+ dimTokens;
97
125
  ownAppearance;
98
126
  resolvedColors;
99
127
  constructor(fgColors, bgColors, mode, options = {}) {
@@ -101,14 +129,8 @@ export class Theme {
101
129
  this.sourcePath = options.sourcePath;
102
130
  this.sourceInfo = options.sourceInfo;
103
131
  this.mode = mode;
104
- const foregrounds = {
105
- ...fgColors,
106
- scrollbarTrack: fgColors.scrollbarTrack ?? fgColors.muted,
107
- scrollbarThumb: fgColors.scrollbarThumb ?? fgColors.text,
108
- thinkingMax: fgColors.thinkingMax ?? fgColors.thinkingXhigh,
109
- searchMatchText: fgColors.searchMatchText ?? fgColors.text,
110
- };
111
- const backgrounds = { ...bgColors, searchMatchBg: bgColors.searchMatchBg ?? bgColors.selectedBg };
132
+ this.dimTokens = new Set(options.dim);
133
+ const { fgColors: foregrounds, bgColors: backgrounds } = splitThemeColors(withThemeTokenFallbacks({ ...fgColors, ...bgColors }));
112
134
  const concreteForegrounds = [];
113
135
  const concreteBackgrounds = [];
114
136
  // Returns the escape sequence for the token's own slot.
@@ -132,33 +154,41 @@ export class Theme {
132
154
  }
133
155
  /**
134
156
  * The background the theme is designed for: declared in the theme JSON, detected from its colors,
135
- * or, for themes without usable colors, taken from the terminal background.
157
+ * or, for themes without usable colors, the terminal's appearance.
136
158
  */
137
159
  get appearance() {
138
- if (this.ownAppearance)
139
- return this.ownAppearance;
140
- const background = terminalDefaultColors.background;
141
- return background ? getThemeForRgbColor(colorToRgb(background)) : "dark";
160
+ return this.ownAppearance ?? getTerminalTheme();
142
161
  }
143
162
  /**
144
163
  * Concrete colors for all tokens. Tokens set to "" (terminal default) use the terminal's reported
145
- * default colors, or a guess based on `appearance` when the terminal did not report them.
164
+ * default colors, or a guess based on `appearance` when the terminal did not report them. Faint
165
+ * tokens are approximated by mixing their color toward the background.
146
166
  */
147
167
  get colors() {
148
- const terminal = terminalDefaultColors;
168
+ const terminal = terminalColors;
149
169
  if (this.resolvedColors?.terminal !== terminal) {
150
170
  const guess = GUESSED_DEFAULT_COLORS[this.appearance];
171
+ const toColor = (rgb, fallback) => rgb ? rgbColor(rgb.r, rgb.g, rgb.b) : fallback;
172
+ const foreground = toColor(terminal.foreground, guess.foreground);
173
+ const background = toColor(terminal.background, guess.background);
151
174
  const colors = { ...this.concreteColors };
152
175
  for (const token of this.defaultForegroundTokens)
153
- colors[token] = terminal.foreground ?? guess.foreground;
176
+ colors[token] = foreground;
154
177
  for (const token of this.defaultBackgroundTokens)
155
- colors[token] = terminal.background ?? guess.background;
178
+ colors[token] = background;
179
+ for (const token of this.dimTokens) {
180
+ const color = colors[token];
181
+ if (color)
182
+ colors[token] = mixColors(color, background, 0.4);
183
+ }
156
184
  this.resolvedColors = { terminal, colors: Object.freeze(colors) };
157
185
  }
158
186
  return this.resolvedColors.colors;
159
187
  }
160
188
  style(text, options) {
161
189
  const { fg, bg } = options;
190
+ if (typeof fg === "string" && this.dimTokens.has(fg))
191
+ options = { ...options, dim: true };
162
192
  return styleTextWithAnsi(text, fg === undefined
163
193
  ? undefined
164
194
  : typeof fg === "string"
@@ -171,6 +201,8 @@ export class Theme {
171
201
  }
172
202
  fg(color, text) {
173
203
  const ansi = this.tokenAnsi(this.fgAnsi, color);
204
+ if (this.dimTokens.has(color))
205
+ return `${ansi}\x1b[2m${text}\x1b[22;39m`;
174
206
  return `${ansi}${text}\x1b[39m`;
175
207
  }
176
208
  bg(color, text) {
@@ -198,8 +230,10 @@ export class Theme {
198
230
  strikethrough(text) {
199
231
  return chalk.strikethrough(text);
200
232
  }
233
+ /** Opening escape sequence for a foreground token. Faint tokens include SGR 2, which `\x1b[22m` closes. */
201
234
  getFgAnsi(color) {
202
- return this.tokenAnsi(this.fgAnsi, color);
235
+ const ansi = this.tokenAnsi(this.fgAnsi, color);
236
+ return this.dimTokens.has(color) ? `${ansi}\x1b[2m` : ansi;
203
237
  }
204
238
  getBgAnsi(color) {
205
239
  return this.tokenAnsi(this.bgAnsi, color);
@@ -262,7 +296,8 @@ export function getAvailableThemesWithPaths() {
262
296
  seen.add(themeInfo.name);
263
297
  result.push(themeInfo);
264
298
  };
265
- // Built-in themes
299
+ // Built-in themes. The system theme is generated, so it has no file.
300
+ addTheme({ name: SYSTEM_THEME_NAME, path: undefined });
266
301
  for (const name of Object.keys(getBuiltinThemes())) {
267
302
  addTheme({ name, path: path.join(themesDir, `${name}.json`) });
268
303
  }
@@ -273,7 +308,8 @@ export function getAvailableThemesWithPaths() {
273
308
  for (const [name, theme] of registeredThemes.entries()) {
274
309
  addTheme({ name, path: theme.sourcePath });
275
310
  }
276
- return result.sort((a, b) => a.name.localeCompare(b.name));
311
+ // The system theme comes first: it is the default and adapts to every terminal.
312
+ return result.sort((a, b) => a.name === SYSTEM_THEME_NAME ? -1 : b.name === SYSTEM_THEME_NAME ? 1 : a.name.localeCompare(b.name));
277
313
  }
278
314
  function getCustomThemeInfos() {
279
315
  const customThemesDir = getCustomThemesDir();
@@ -343,40 +379,52 @@ function loadThemeJson(name) {
343
379
  const content = fs.readFileSync(themePath, "utf-8");
344
380
  return parseThemeJsonContent(name, content);
345
381
  }
346
- function createTheme(themeJson, mode, sourcePath) {
347
- const colorMode = mode ?? getTerminalColorMode();
348
- const resolvedColors = resolveThemeColors(withThemeColorFallbacks(themeJson.colors), themeJson.vars);
382
+ function splitThemeColors(colors) {
349
383
  const fgColors = {};
350
384
  const bgColors = {};
351
- const bgColorKeys = new Set([
352
- "selectedBg",
353
- "searchMatchBg",
354
- "userMessageBg",
355
- "customMessageBg",
356
- "toolPendingBg",
357
- "toolSuccessBg",
358
- "toolErrorBg",
359
- ]);
360
- for (const [key, value] of Object.entries(resolvedColors)) {
361
- if (bgColorKeys.has(key)) {
385
+ for (const [key, value] of Object.entries(colors)) {
386
+ if (THEME_TOKENS[key].slot === "background") {
362
387
  bgColors[key] = value;
363
388
  }
364
389
  else {
365
390
  fgColors[key] = value;
366
391
  }
367
392
  }
393
+ return { fgColors, bgColors };
394
+ }
395
+ function createTheme(themeJson, mode, sourcePath) {
396
+ const colorMode = mode ?? getTerminalColorMode();
397
+ const resolvedColors = withThemeTokenFallbacks(resolveThemeColors(themeJson.colors, themeJson.vars));
398
+ const { fgColors, bgColors } = splitThemeColors(resolvedColors);
368
399
  return new Theme(fgColors, bgColors, colorMode, {
369
400
  name: themeJson.name,
370
401
  sourcePath,
371
402
  appearance: themeJson.appearance,
372
403
  });
373
404
  }
405
+ /** Generate the system theme from the terminal's reported colors (grayscale while they are pending). */
406
+ function createSystemTheme(mode) {
407
+ const generated = generateSystemThemeColors({
408
+ ...terminalColors,
409
+ saturation: terminalColorsPending ? 0 : 1,
410
+ appearanceHint: getTerminalTheme(),
411
+ });
412
+ const { fgColors, bgColors } = splitThemeColors(withThemeTokenFallbacks(generated.colors));
413
+ return new Theme(fgColors, bgColors, mode ?? getTerminalColorMode(), {
414
+ name: SYSTEM_THEME_NAME,
415
+ appearance: generated.appearance,
416
+ dim: generated.dim,
417
+ });
418
+ }
374
419
  export function loadThemeFromPath(themePath, mode) {
375
420
  const content = fs.readFileSync(themePath, "utf-8");
376
421
  const themeJson = parseThemeJsonContent(themePath, content);
377
422
  return createTheme(themeJson, mode, themePath);
378
423
  }
379
424
  function loadTheme(name, mode) {
425
+ // The system theme name is reserved: it takes precedence over custom themes of the same name.
426
+ if (name === SYSTEM_THEME_NAME)
427
+ return createSystemTheme(mode);
380
428
  const registeredTheme = registeredThemes.get(name);
381
429
  if (registeredTheme) {
382
430
  return registeredTheme;
@@ -417,95 +465,45 @@ export function resolveThemeSetting(themeSetting, terminalTheme) {
417
465
  return themeSetting;
418
466
  return undefined;
419
467
  }
420
- function getColorFgBgBackgroundIndex(colorfgbg) {
421
- const parts = colorfgbg.split(";");
422
- for (let i = parts.length - 1; i >= 0; i--) {
423
- const bg = parseInt(parts[i].trim(), 10);
424
- if (Number.isInteger(bg) && bg >= 0 && bg <= 255) {
425
- return bg;
426
- }
427
- }
428
- return undefined;
429
- }
430
- function getRgbColorLuminance({ r, g, b }) {
431
- const toLinear = (channel) => {
432
- const value = channel / 255;
433
- return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
434
- };
435
- return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
436
- }
437
- function getAnsiColorLuminance(index) {
438
- return getRgbColorLuminance(colorToRgb(indexedColor(index)));
439
- }
440
- export function getThemeForRgbColor(rgb) {
441
- return getRgbColorLuminance(rgb) >= 0.5 ? "light" : "dark";
442
- }
443
- export function detectTerminalBackgroundFromEnv(options = {}) {
444
- const env = options.env ?? process.env;
445
- const colorfgbg = env.COLORFGBG || "";
446
- const bg = getColorFgBgBackgroundIndex(colorfgbg);
447
- if (bg !== undefined) {
448
- return {
449
- theme: getAnsiColorLuminance(bg) >= 0.5 ? "light" : "dark",
450
- source: "COLORFGBG",
451
- detail: `background color index ${bg}`,
452
- confidence: "high",
453
- };
454
- }
455
- return {
456
- theme: "dark",
457
- source: "fallback",
458
- detail: "no terminal background hint found",
459
- confidence: "low",
460
- };
461
- }
462
- export async function detectTerminalBackgroundTheme({ ui, timeoutMs, env, }) {
463
- try {
464
- const rgb = await ui.queryTerminalBackgroundColor({ timeoutMs });
465
- if (rgb) {
466
- return {
467
- theme: getThemeForRgbColor(rgb),
468
- source: "terminal background",
469
- detail: `OSC 11 background rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
470
- confidence: "high",
471
- };
472
- }
473
- }
474
- catch {
475
- // Fall back to environment-based detection when the terminal query fails.
476
- }
477
- return detectTerminalBackgroundFromEnv({ env });
468
+ /**
469
+ * Dark or light from the `COLORFGBG` environment variable some terminals set, or undefined without a
470
+ * usable background index. The value is `fg;bg` or `fg;xpm;bg` (rxvt), where a field is an ANSI color
471
+ * index or `default` when the color is not in the palette. The index refers to the terminal's own palette,
472
+ * whose colors are unknown here, so it is classified by index like Vim does: 0-6 and 8 (bright black, e.g.
473
+ * Solarized Dark's background) are dark, 7 and 9-15 are light.
474
+ */
475
+ export function detectColorFgBgTheme(env = process.env) {
476
+ const bg = env.COLORFGBG?.split(";").at(-1)?.trim();
477
+ if (!bg || !/^\d{1,2}$/.test(bg))
478
+ return undefined;
479
+ const index = Number(bg);
480
+ if (index > 15)
481
+ return undefined;
482
+ return index <= 6 || index === 8 ? "dark" : "light";
478
483
  }
479
- export async function detectTerminalThemeForAuto({ ui, timeoutMs, env, }) {
480
- let colorSchemePromise;
481
- try {
482
- colorSchemePromise = ui.queryTerminalColorScheme?.({ timeoutMs });
483
- }
484
- catch {
485
- // Fall back to OSC 11 / COLORFGBG detection when starting the color-scheme query fails.
486
- }
487
- const backgroundThemePromise = detectTerminalBackgroundTheme({ ui, timeoutMs, env });
488
- try {
489
- const colorScheme = await colorSchemePromise;
490
- if (colorScheme)
491
- return colorScheme;
492
- }
493
- catch {
494
- // Fall back to the concurrently queried OSC 11 / COLORFGBG detection.
495
- }
496
- return (await backgroundThemePromise).theme;
484
+ /**
485
+ * Whether the terminal is dark or light. The background it renders decides, classified the same way the
486
+ * system theme does. Without a reported background: the terminal's light/dark report, then COLORFGBG,
487
+ * then dark.
488
+ */
489
+ export function detectTerminalTheme(colors = {}, reportedScheme, env = process.env) {
490
+ const { background, foreground } = colors;
491
+ if (background)
492
+ return terminalAppearance(background, foreground);
493
+ return reportedScheme ?? detectColorFgBgTheme(env) ?? "dark";
497
494
  }
498
- export function getDefaultTheme() {
499
- return detectTerminalBackgroundFromEnv().theme;
495
+ /** Whether the terminal is dark or light, from everything it reported so far. See `detectTerminalTheme()`. */
496
+ export function getTerminalTheme() {
497
+ return detectTerminalTheme(terminalColors, terminalColorScheme);
500
498
  }
501
499
  // ============================================================================
502
500
  // Global Theme Instance
503
501
  // ============================================================================
504
- // Use globalThis to share theme across module loaders (tsx + jiti in dev mode)
502
+ // Use globalThis to share theme across module loaders (node + jiti in dev mode)
505
503
  const THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme");
506
504
  const THEME_KEY_OLD = Symbol.for("@mariozechner/pi-coding-agent:theme");
507
505
  // Export theme as a getter that reads from globalThis
508
- // This ensures all module instances (tsx, jiti) see the same theme
506
+ // This ensures all module instances (node, jiti) see the same theme
509
507
  export const theme = new Proxy({}, {
510
508
  get(_target, prop) {
511
509
  const t = globalThis[THEME_KEY];
@@ -533,7 +531,7 @@ export function setRegisteredThemes(themes) {
533
531
  }
534
532
  }
535
533
  export function initTheme(themeName, enableWatcher = false) {
536
- const name = themeName ?? getDefaultTheme();
534
+ const name = themeName ?? SYSTEM_THEME_NAME;
537
535
  currentThemeName = name;
538
536
  try {
539
537
  setGlobalTheme(loadTheme(name));
@@ -542,9 +540,9 @@ export function initTheme(themeName, enableWatcher = false) {
542
540
  }
543
541
  }
544
542
  catch (_error) {
545
- // Theme is invalid - fall back to dark theme silently
546
- currentThemeName = "dark";
547
- setGlobalTheme(loadTheme("dark"));
543
+ // Theme is invalid - fall back to the system theme silently
544
+ currentThemeName = SYSTEM_THEME_NAME;
545
+ setGlobalTheme(loadTheme(SYSTEM_THEME_NAME));
548
546
  // Don't start watcher for fallback theme
549
547
  }
550
548
  }
@@ -561,9 +559,9 @@ export function setTheme(name, enableWatcher = false) {
561
559
  return { success: true };
562
560
  }
563
561
  catch (error) {
564
- // Theme is invalid - fall back to dark theme
565
- currentThemeName = "dark";
566
- setGlobalTheme(loadTheme("dark"));
562
+ // Theme is invalid - fall back to the system theme
563
+ currentThemeName = SYSTEM_THEME_NAME;
564
+ setGlobalTheme(loadTheme(SYSTEM_THEME_NAME));
567
565
  // Don't start watcher for fallback theme
568
566
  return {
569
567
  success: false,
@@ -585,7 +583,10 @@ export function onThemeChange(callback) {
585
583
  function startThemeWatcher() {
586
584
  stopThemeWatcher();
587
585
  // Only watch if it's a custom theme (not built-in)
588
- if (!currentThemeName || currentThemeName === "dark" || currentThemeName === "light") {
586
+ if (!currentThemeName ||
587
+ currentThemeName === "dark" ||
588
+ currentThemeName === "light" ||
589
+ currentThemeName === SYSTEM_THEME_NAME) {
589
590
  return;
590
591
  }
591
592
  const customThemesDir = getCustomThemesDir();
@@ -659,28 +660,30 @@ export function stopThemeWatcher() {
659
660
  * Used by HTML export to generate CSS custom properties.
660
661
  */
661
662
  export function getResolvedThemeColors(themeName) {
662
- const colors = loadTheme(themeName ?? currentThemeName ?? getDefaultTheme()).colors;
663
+ const colors = loadTheme(themeName ?? currentThemeName ?? SYSTEM_THEME_NAME).colors;
663
664
  return Object.fromEntries(Object.entries(colors).map(([token, color]) => [token, colorToHex(color)]));
664
665
  }
665
666
  /**
666
667
  * Check if a theme is a "light" theme (for CSS that needs light/dark variants).
667
668
  */
668
669
  export function isLightTheme(themeName) {
669
- return loadTheme(themeName ?? currentThemeName ?? getDefaultTheme()).appearance === "light";
670
+ return loadTheme(themeName ?? currentThemeName ?? SYSTEM_THEME_NAME).appearance === "light";
670
671
  }
671
672
  /**
672
673
  * Get explicit export colors from theme JSON, if specified.
673
674
  * Returns undefined for each color that isn't explicitly set.
674
675
  */
675
676
  export function getThemeExportColors(themeName) {
676
- const name = themeName ?? currentThemeName ?? getDefaultTheme();
677
+ const name = themeName ?? currentThemeName ?? SYSTEM_THEME_NAME;
678
+ if (name === SYSTEM_THEME_NAME)
679
+ return {};
677
680
  try {
678
681
  const themeJson = loadThemeJson(name);
679
682
  const exportSection = themeJson.export;
680
683
  if (!exportSection)
681
684
  return {};
682
685
  const vars = themeJson.vars ?? {};
683
- // Export colors end up in CSS, which understands hex, rgb() and oklch() values directly.
686
+ // Export colors end up in CSS, which understands hex and oklch() values directly but not okhsl().
684
687
  const resolve = (value) => {
685
688
  if (value === undefined)
686
689
  return undefined;
@@ -689,6 +692,8 @@ export function getThemeExportColors(themeName) {
689
692
  return colorToHex(indexedColor(resolved));
690
693
  if (resolved === "")
691
694
  return undefined;
695
+ if (/^okhsl\(/i.test(resolved))
696
+ return colorToHex(parseColor(resolved));
692
697
  return resolved;
693
698
  };
694
699
  return {
@@ -711,6 +716,7 @@ function buildCliHighlightTheme(t) {
711
716
  number: (s) => t.fg("syntaxNumber", s),
712
717
  regexp: (s) => t.fg("syntaxString", s),
713
718
  string: (s) => t.fg("syntaxString", s),
719
+ subst: (s) => t.fg("text", s),
714
720
  comment: (s) => t.fg("syntaxComment", s),
715
721
  doctag: (s) => t.fg("syntaxComment", s),
716
722
  meta: (s) => t.fg("muted", s),