@visual-z/xeno 0.1.3 → 0.2.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 (148) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +143 -33
  3. package/docs/install.md +18 -6
  4. package/docs/providers.md +147 -0
  5. package/package.json +8 -2
  6. package/src/apps/cli.ts +42 -14
  7. package/src/apps/commands.ts +32 -4
  8. package/src/apps/completions.ts +9 -6
  9. package/src/apps/export-cli.ts +24 -0
  10. package/src/apps/extension-ui.ts +330 -0
  11. package/src/apps/input.ts +16 -1
  12. package/src/apps/model-catalog.ts +42 -0
  13. package/src/apps/project-trust.ts +49 -0
  14. package/src/apps/render.ts +1 -0
  15. package/src/apps/resource-browser.ts +75 -0
  16. package/src/apps/resources.ts +25 -0
  17. package/src/apps/session-browser.ts +113 -0
  18. package/src/apps/session-picker.ts +55 -0
  19. package/src/apps/shell.ts +1177 -378
  20. package/src/apps/startup.ts +400 -268
  21. package/src/core/action-defaults.ts +306 -0
  22. package/src/core/attachment-input.ts +17 -0
  23. package/src/core/catalog-merge.ts +19 -0
  24. package/src/core/event-validation.ts +19 -2
  25. package/src/core/events.ts +45 -2
  26. package/src/core/export.ts +6 -2
  27. package/src/core/extension-flags.ts +14 -0
  28. package/src/core/images.ts +46 -0
  29. package/src/core/json-event.ts +2 -0
  30. package/src/core/keymap.ts +43 -119
  31. package/src/core/model-replay.ts +22 -0
  32. package/src/core/model-scope.ts +53 -0
  33. package/src/core/packages.ts +47 -0
  34. package/src/core/progress.ts +9 -0
  35. package/src/core/project.ts +21 -3
  36. package/src/core/prompts.ts +33 -5
  37. package/src/core/queue.ts +18 -0
  38. package/src/core/resource-selection.ts +36 -0
  39. package/src/{runtime → core}/resume.ts +15 -22
  40. package/src/core/session-archive.ts +4 -0
  41. package/src/core/session-browser.ts +82 -0
  42. package/src/core/session-entry.ts +56 -0
  43. package/src/core/session-position.ts +26 -0
  44. package/src/core/settings.ts +169 -154
  45. package/src/core/theme.ts +15 -6
  46. package/src/core/thinking.ts +26 -0
  47. package/src/core/tokens.ts +2 -0
  48. package/src/core/transcript.ts +15 -5
  49. package/src/runtime/auth.ts +116 -11
  50. package/src/runtime/catalog.ts +54 -151
  51. package/src/runtime/clipboard.ts +114 -0
  52. package/src/runtime/extension-loader.ts +37 -0
  53. package/src/runtime/extension-ui.ts +55 -0
  54. package/src/runtime/extensions.ts +273 -0
  55. package/src/runtime/images.ts +76 -0
  56. package/src/runtime/loop.ts +213 -83
  57. package/src/runtime/model-config.ts +232 -0
  58. package/src/runtime/model-data.LICENSE +27 -0
  59. package/src/runtime/model-data.ts +21 -0
  60. package/src/runtime/model-replay.ts +10 -0
  61. package/src/runtime/model-scope.ts +20 -0
  62. package/src/runtime/model-sources.ts +151 -0
  63. package/src/runtime/model.ts +74 -11
  64. package/src/runtime/models-fetch.ts +6 -104
  65. package/src/runtime/oauth.ts +4 -3
  66. package/src/runtime/openai-responses.ts +12 -4
  67. package/src/runtime/openai.ts +13 -4
  68. package/src/runtime/package-manager.ts +125 -0
  69. package/src/runtime/pi-provider.ts +235 -0
  70. package/src/runtime/progress.ts +26 -0
  71. package/src/runtime/provider-access.ts +21 -0
  72. package/src/runtime/provider-protocols.ts +29 -0
  73. package/src/runtime/provider-request.ts +73 -0
  74. package/src/runtime/provider.ts +26 -6
  75. package/src/runtime/rich-text.ts +36 -0
  76. package/src/runtime/tools.ts +41 -5
  77. package/src/store/command.ts +7 -1
  78. package/src/store/context-files.ts +1 -1
  79. package/src/store/log.ts +65 -0
  80. package/src/store/models.ts +28 -9
  81. package/src/store/observations.ts +4 -2
  82. package/src/store/packages.ts +98 -0
  83. package/src/store/recovery.ts +8 -0
  84. package/src/store/resource-index.ts +170 -0
  85. package/src/store/resources.ts +75 -142
  86. package/src/store/session-archive.ts +162 -0
  87. package/src/store/session-browser.ts +68 -0
  88. package/src/store/session-position.ts +27 -0
  89. package/src/store/sessions.ts +87 -66
  90. package/src/store/trust.ts +13 -0
  91. package/src/store/workspace.ts +6 -0
  92. package/src/tui/chat-viewport.ts +47 -0
  93. package/src/tui/editor.ts +62 -15
  94. package/src/tui/host.ts +50 -9
  95. package/src/tui/input.ts +8 -2
  96. package/src/tui/select.ts +24 -6
  97. package/src/tui/settings.ts +4 -0
  98. package/src/tui/system-theme.ts +700 -0
  99. package/src/tui/transcript.ts +63 -11
  100. package/src/tui/tree.ts +161 -95
  101. package/src/vendor/pi-rich/LICENSE.upstream +21 -0
  102. package/src/vendor/pi-rich/UPSTREAM.json +17 -0
  103. package/src/vendor/pi-rich/clipboard-command.ts +44 -0
  104. package/src/vendor/pi-rich/exif-orientation.ts +182 -0
  105. package/src/vendor/pi-rich/html.ts +51 -0
  106. package/src/vendor/pi-rich/image-convert.ts +49 -0
  107. package/src/vendor/pi-rich/image-resize-core.ts +164 -0
  108. package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
  109. package/src/vendor/pi-rich/image-resize.ts +123 -0
  110. package/src/vendor/pi-rich/mime.ts +116 -0
  111. package/src/vendor/pi-rich/photon.ts +139 -0
  112. package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
  113. package/src/vendor/pi-tui/UPSTREAM.json +50 -0
  114. package/src/vendor/pi-tui/VENDOR.md +7 -5
  115. package/src/vendor/pi-tui/autocomplete.ts +35 -10
  116. package/src/vendor/pi-tui/colors.ts +367 -0
  117. package/src/vendor/pi-tui/components/box.ts +8 -9
  118. package/src/vendor/pi-tui/components/editor.ts +4 -2
  119. package/src/vendor/pi-tui/components/markdown.ts +8 -2
  120. package/src/vendor/pi-tui/index.ts +31 -1
  121. package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
  122. package/src/vendor/pi-tui/native/clipboard.h +208 -0
  123. package/src/vendor/pi-tui/native/darwin/README.md +22 -0
  124. package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
  125. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  126. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  127. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
  128. package/src/vendor/pi-tui/native/linux/README.md +23 -0
  129. package/src/vendor/pi-tui/native/linux/build.sh +65 -0
  130. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  131. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  132. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
  133. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
  134. package/src/vendor/pi-tui/native/napi.h +91 -0
  135. package/src/vendor/pi-tui/native/win32/README.md +31 -0
  136. package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
  137. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  138. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  139. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
  140. package/src/vendor/pi-tui/native-platform.ts +2 -0
  141. package/src/vendor/pi-tui/oklab.ts +233 -0
  142. package/src/vendor/pi-tui/terminal-colors.ts +26 -8
  143. package/src/vendor/pi-tui/terminal-image.ts +43 -8
  144. package/src/vendor/pi-tui/terminal.ts +16 -9
  145. package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
  146. package/src/vendor/pi-tui/tui.ts +112 -75
  147. package/src/vendor/pi-tui/utils.ts +89 -49
  148. package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
@@ -0,0 +1,367 @@
1
+ import { linearSrgbToRgb, okhslToRgb, oklabToLinearSrgb, rgbToOkhsl, rgbToOklab } from "./oklab.ts";
2
+ import type { RgbColor } from "./terminal-colors.ts";
3
+
4
+ export interface IndexedColor {
5
+ readonly kind: "indexed";
6
+ readonly index: number;
7
+ }
8
+
9
+ export interface RgbColorValue {
10
+ readonly kind: "rgb";
11
+ readonly r: number;
12
+ readonly g: number;
13
+ readonly b: number;
14
+ }
15
+
16
+ export interface OklchColorValue {
17
+ readonly kind: "oklch";
18
+ readonly l: number;
19
+ readonly c: number;
20
+ readonly h: number;
21
+ }
22
+
23
+ /** A concrete color. Every color can be converted to sRGB, so color math never fails. */
24
+ export type Color = IndexedColor | RgbColorValue | OklchColorValue;
25
+ export type TerminalColorMode = "256color" | "truecolor";
26
+ export type ColorMixSpace = "oklch" | "srgb";
27
+
28
+ export interface OklchChannels {
29
+ l: number;
30
+ c: number;
31
+ h: number;
32
+ }
33
+
34
+ /**
35
+ * OKHSL channels: hue in degrees, saturation and lightness 0-1. Saturation is relative to the most the
36
+ * sRGB gamut allows at that hue and lightness, so every value is in gamut.
37
+ */
38
+ export interface OkhslChannels {
39
+ h: number;
40
+ s: number;
41
+ l: number;
42
+ }
43
+
44
+ export interface TextAttributes {
45
+ bold?: boolean;
46
+ dim?: boolean;
47
+ italic?: boolean;
48
+ underline?: boolean;
49
+ inverse?: boolean;
50
+ strikethrough?: boolean;
51
+ }
52
+
53
+ export interface TextStyle extends TextAttributes {
54
+ fg?: Color;
55
+ bg?: Color;
56
+ }
57
+
58
+ function requireFinite(value: number, name: string): void {
59
+ if (!Number.isFinite(value)) throw new Error(`${name} must be finite`);
60
+ }
61
+
62
+ export function indexedColor(index: number): IndexedColor {
63
+ if (!Number.isInteger(index) || index < 0 || index > 255) {
64
+ throw new Error(`ANSI color index must be an integer from 0 to 255: ${index}`);
65
+ }
66
+ return Object.freeze({ kind: "indexed", index });
67
+ }
68
+
69
+ export function rgbColor(r: number, g: number, b: number): RgbColorValue {
70
+ for (const [name, value] of [
71
+ ["r", r],
72
+ ["g", g],
73
+ ["b", b],
74
+ ] as const) {
75
+ requireFinite(value, name);
76
+ if (value < 0 || value > 255) throw new Error(`${name} must be between 0 and 255: ${value}`);
77
+ }
78
+ return Object.freeze({ kind: "rgb", r, g, b });
79
+ }
80
+
81
+ export function oklchColor(l: number, c: number, h: number): OklchColorValue {
82
+ requireFinite(l, "l");
83
+ requireFinite(c, "c");
84
+ requireFinite(h, "h");
85
+ if (l < 0 || l > 1) throw new Error(`l must be between 0 and 1: ${l}`);
86
+ if (c < 0) throw new Error(`c must not be negative: ${c}`);
87
+ return Object.freeze({ kind: "oklch", l, c, h: ((h % 360) + 360) % 360 });
88
+ }
89
+
90
+ const NUMBER_PATTERN = String.raw`[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?`;
91
+ const OKLCH_PATTERN = new RegExp(
92
+ `^oklch\\(\\s*(${NUMBER_PATTERN})(%)?\\s+(${NUMBER_PATTERN})\\s+(${NUMBER_PATTERN})(?:deg)?\\s*\\)$`,
93
+ "i",
94
+ );
95
+ const OKHSL_PATTERN = new RegExp(
96
+ `^okhsl\\(\\s*(${NUMBER_PATTERN})(?:deg)?\\s+(${NUMBER_PATTERN})(%)?\\s+(${NUMBER_PATTERN})(%)?\\s*\\)$`,
97
+ "i",
98
+ );
99
+
100
+ /**
101
+ * An OKHSL color, converted to sRGB. Saturation is relative to the sRGB gamut at the hue and lightness,
102
+ * so equal saturation looks equally colorful across hues and lightness.
103
+ * @param h Hue in degrees.
104
+ * @param s Saturation, 0-1.
105
+ * @param l Lightness, 0-1.
106
+ */
107
+ export function okhslColor(h: number, s: number, l: number): RgbColorValue {
108
+ requireFinite(h, "h");
109
+ requireFinite(s, "s");
110
+ requireFinite(l, "l");
111
+ if (s < 0 || s > 1) throw new Error(`s must be between 0 and 1: ${s}`);
112
+ if (l < 0 || l > 1) throw new Error(`l must be between 0 and 1: ${l}`);
113
+ const { r, g, b } = okhslToRgb(h, s, l);
114
+ return rgbColor(r, g, b);
115
+ }
116
+
117
+ export function colorToOkhsl(color: Color): OkhslChannels {
118
+ return rgbToOkhsl(colorToRgb(color));
119
+ }
120
+
121
+ export function parseColor(value: string | number): Color {
122
+ if (typeof value === "number") return indexedColor(value);
123
+
124
+ const hex = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(value);
125
+ if (hex) {
126
+ const digits = hex[1].length === 3 ? [...hex[1]].map((digit) => digit + digit).join("") : hex[1];
127
+ return rgbColor(
128
+ Number.parseInt(digits.slice(0, 2), 16),
129
+ Number.parseInt(digits.slice(2, 4), 16),
130
+ Number.parseInt(digits.slice(4, 6), 16),
131
+ );
132
+ }
133
+
134
+ const oklch = OKLCH_PATTERN.exec(value);
135
+ if (oklch) {
136
+ const lightness = Number.parseFloat(oklch[1]) / (oklch[2] ? 100 : 1);
137
+ return oklchColor(lightness, Number.parseFloat(oklch[3]), Number.parseFloat(oklch[4]));
138
+ }
139
+
140
+ const okhsl = OKHSL_PATTERN.exec(value);
141
+ if (okhsl) {
142
+ const saturation = Number.parseFloat(okhsl[2]) / (okhsl[3] ? 100 : 1);
143
+ const lightness = Number.parseFloat(okhsl[4]) / (okhsl[5] ? 100 : 1);
144
+ return okhslColor(Number.parseFloat(okhsl[1]), saturation, lightness);
145
+ }
146
+
147
+ throw new Error(`Invalid color value: ${value}`);
148
+ }
149
+
150
+ const BASIC_COLORS: readonly RgbColor[] = [
151
+ { r: 0, g: 0, b: 0 },
152
+ { r: 128, g: 0, b: 0 },
153
+ { r: 0, g: 128, b: 0 },
154
+ { r: 128, g: 128, b: 0 },
155
+ { r: 0, g: 0, b: 128 },
156
+ { r: 128, g: 0, b: 128 },
157
+ { r: 0, g: 128, b: 128 },
158
+ { r: 192, g: 192, b: 192 },
159
+ { r: 128, g: 128, b: 128 },
160
+ { r: 255, g: 0, b: 0 },
161
+ { r: 0, g: 255, b: 0 },
162
+ { r: 255, g: 255, b: 0 },
163
+ { r: 0, g: 0, b: 255 },
164
+ { r: 255, g: 0, b: 255 },
165
+ { r: 0, g: 255, b: 255 },
166
+ { r: 255, g: 255, b: 255 },
167
+ ];
168
+ const CUBE_VALUES = [0, 95, 135, 175, 215, 255] as const;
169
+ const GRAY_VALUES = Array.from({ length: 24 }, (_, index) => 8 + index * 10);
170
+
171
+ function indexedToRgb(index: number): RgbColor {
172
+ if (index < 16) return { ...BASIC_COLORS[index] };
173
+ if (index < 232) {
174
+ const cubeIndex = index - 16;
175
+ return {
176
+ r: CUBE_VALUES[Math.floor(cubeIndex / 36)],
177
+ g: CUBE_VALUES[Math.floor((cubeIndex % 36) / 6)],
178
+ b: CUBE_VALUES[cubeIndex % 6],
179
+ };
180
+ }
181
+ const gray = 8 + (index - 232) * 10;
182
+ return { r: gray, g: gray, b: gray };
183
+ }
184
+
185
+ function isInSrgbGamut(linear: number[]): boolean {
186
+ const epsilon = 1e-7;
187
+ return linear.every((channel) => channel >= -epsilon && channel <= 1 + epsilon);
188
+ }
189
+
190
+ function oklchToRgb({ l, c, h }: OklchChannels): RgbColor {
191
+ // Gamut mapping keeps the hue fixed, so its direction is computed once and scaled by chroma.
192
+ const radians = (h * Math.PI) / 180;
193
+ const cos = Math.cos(radians);
194
+ const sin = Math.sin(radians);
195
+ const atChroma = (chroma: number) => oklabToLinearSrgb([l, chroma * cos, chroma * sin]);
196
+
197
+ const direct = atChroma(c);
198
+ if (isInSrgbGamut(direct)) return linearSrgbToRgb(direct);
199
+
200
+ // Reduce chroma until the color fits. The achromatic color is always in gamut, so it is the
201
+ // fallback when no bisection step fits, e.g. `oklch(100% 0.3 150)` must map to white.
202
+ let linear = atChroma(0);
203
+ let low = 0;
204
+ let high = c;
205
+ for (let index = 0; index < 20; index++) {
206
+ const chroma = (low + high) / 2;
207
+ const candidate = atChroma(chroma);
208
+ if (isInSrgbGamut(candidate)) {
209
+ low = chroma;
210
+ linear = candidate;
211
+ } else {
212
+ high = chroma;
213
+ }
214
+ }
215
+ return linearSrgbToRgb(linear);
216
+ }
217
+
218
+ export function colorToRgb(color: Color): RgbColor {
219
+ switch (color.kind) {
220
+ case "indexed":
221
+ return indexedToRgb(color.index);
222
+ case "rgb":
223
+ return { r: color.r, g: color.g, b: color.b };
224
+ case "oklch":
225
+ return oklchToRgb(color);
226
+ }
227
+ }
228
+
229
+ export function colorToOklch(color: Color): OklchChannels {
230
+ if (color.kind === "oklch") return { l: color.l, c: color.c, h: color.h };
231
+ const [l, a, b] = rgbToOklab(colorToRgb(color));
232
+ return { l, c: Math.hypot(a, b), h: ((Math.atan2(b, a) * 180) / Math.PI + 360) % 360 };
233
+ }
234
+
235
+ export function colorToHex(color: Color): string {
236
+ const { r, g, b } = colorToRgb(color);
237
+ const channel = (value: number) => Math.round(value).toString(16).padStart(2, "0");
238
+ return `#${channel(r)}${channel(g)}${channel(b)}`;
239
+ }
240
+
241
+ export function mixColors(first: Color, second: Color, amount: number, space: ColorMixSpace = "oklch"): Color {
242
+ requireFinite(amount, "amount");
243
+ if (amount < 0 || amount > 1) throw new Error(`amount must be between 0 and 1: ${amount}`);
244
+
245
+ if (space === "srgb") {
246
+ const a = colorToRgb(first);
247
+ const b = colorToRgb(second);
248
+ return rgbColor(a.r + (b.r - a.r) * amount, a.g + (b.g - a.g) * amount, a.b + (b.b - a.b) * amount);
249
+ }
250
+
251
+ const a = colorToOklch(first);
252
+ const b = colorToOklch(second);
253
+ const firstHue = a.c < 1e-7 ? b.h : a.h;
254
+ const secondHue = b.c < 1e-7 ? firstHue : b.h;
255
+ const hueDelta = ((secondHue - firstHue + 540) % 360) - 180;
256
+ return oklchColor(a.l + (b.l - a.l) * amount, a.c + (b.c - a.c) * amount, firstHue + hueDelta * amount);
257
+ }
258
+
259
+ function findClosest(values: readonly number[], target: number): number {
260
+ let closestIndex = 0;
261
+ let closestDistance = Infinity;
262
+ for (let index = 0; index < values.length; index++) {
263
+ const distance = Math.abs(target - values[index]);
264
+ if (distance < closestDistance) {
265
+ closestIndex = index;
266
+ closestDistance = distance;
267
+ }
268
+ }
269
+ return closestIndex;
270
+ }
271
+
272
+ function colorDistance(first: RgbColor, second: RgbColor): number {
273
+ const dr = first.r - second.r;
274
+ const dg = first.g - second.g;
275
+ const db = first.b - second.b;
276
+ return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;
277
+ }
278
+
279
+ function rgbToAnsi256(color: RgbColor): number {
280
+ const rIndex = findClosest(CUBE_VALUES, color.r);
281
+ const gIndex = findClosest(CUBE_VALUES, color.g);
282
+ const bIndex = findClosest(CUBE_VALUES, color.b);
283
+ const cubeColor = { r: CUBE_VALUES[rIndex], g: CUBE_VALUES[gIndex], b: CUBE_VALUES[bIndex] };
284
+ const cubeIndex = 16 + 36 * rIndex + 6 * gIndex + bIndex;
285
+
286
+ const gray = Math.round(0.299 * color.r + 0.587 * color.g + 0.114 * color.b);
287
+ const grayOffset = findClosest(GRAY_VALUES, gray);
288
+ const grayValue = GRAY_VALUES[grayOffset];
289
+ const spread = Math.max(color.r, color.g, color.b) - Math.min(color.r, color.g, color.b);
290
+ if (
291
+ spread < 10 &&
292
+ colorDistance(color, { r: grayValue, g: grayValue, b: grayValue }) < colorDistance(color, cubeColor)
293
+ ) {
294
+ return 232 + grayOffset;
295
+ }
296
+ return cubeIndex;
297
+ }
298
+
299
+ function colorAnsi(color: Color, mode: TerminalColorMode, background: boolean): string {
300
+ if (color.kind === "indexed") return `\x1b[${background ? 48 : 38};5;${color.index}m`;
301
+
302
+ const rgb = colorToRgb(color);
303
+ if (mode === "truecolor") {
304
+ return `\x1b[${background ? 48 : 38};2;${Math.round(rgb.r)};${Math.round(rgb.g)};${Math.round(rgb.b)}m`;
305
+ }
306
+ return `\x1b[${background ? 48 : 38};5;${rgbToAnsi256(rgb)}m`;
307
+ }
308
+
309
+ export function foregroundAnsi(color: Color, mode: TerminalColorMode): string {
310
+ return colorAnsi(color, mode, false);
311
+ }
312
+
313
+ export function backgroundAnsi(color: Color, mode: TerminalColorMode): string {
314
+ return colorAnsi(color, mode, true);
315
+ }
316
+
317
+ export function styleText(text: string, options: TextStyle, mode: TerminalColorMode): string {
318
+ return styleTextWithAnsi(
319
+ text,
320
+ options.fg && foregroundAnsi(options.fg, mode),
321
+ options.bg && backgroundAnsi(options.bg, mode),
322
+ options,
323
+ );
324
+ }
325
+
326
+ /**
327
+ * Like `styleText()`, but with precomputed color escape sequences, e.g. cached theme colors.
328
+ * Colors in `options` are ignored.
329
+ */
330
+ export function styleTextWithAnsi(
331
+ text: string,
332
+ fgAnsi: string | undefined,
333
+ bgAnsi: string | undefined,
334
+ options: TextAttributes,
335
+ ): string {
336
+ // Resets are prepended so they close in reverse order of the opening sequences.
337
+ let prefix = "";
338
+ let suffix = "";
339
+ if (fgAnsi) {
340
+ prefix += fgAnsi;
341
+ suffix = "\x1b[39m";
342
+ }
343
+ if (bgAnsi) {
344
+ prefix += bgAnsi;
345
+ suffix = `\x1b[49m${suffix}`;
346
+ }
347
+ if (options.bold) prefix += "\x1b[1m";
348
+ if (options.dim) prefix += "\x1b[2m";
349
+ if (options.bold || options.dim) suffix = `\x1b[22m${suffix}`;
350
+ if (options.italic) {
351
+ prefix += "\x1b[3m";
352
+ suffix = `\x1b[23m${suffix}`;
353
+ }
354
+ if (options.underline) {
355
+ prefix += "\x1b[4m";
356
+ suffix = `\x1b[24m${suffix}`;
357
+ }
358
+ if (options.inverse) {
359
+ prefix += "\x1b[7m";
360
+ suffix = `\x1b[27m${suffix}`;
361
+ }
362
+ if (options.strikethrough) {
363
+ prefix += "\x1b[9m";
364
+ suffix = `\x1b[29m${suffix}`;
365
+ }
366
+ return `${prefix}${text}${suffix}`;
367
+ }
@@ -1,5 +1,5 @@
1
1
  import { type Component, dispatchMouseEvent, type TuiMouseDispatchResult, type TuiMouseEvent } from "../tui.ts";
2
- import { applyBackgroundToLine, visibleWidth } from "../utils.ts";
2
+ import { visibleWidth } from "../utils.ts";
3
3
 
4
4
  type RenderCache = {
5
5
  childLines: string[];
@@ -106,14 +106,16 @@ export class Box implements Component {
106
106
  const contentWidth = Math.max(1, width - this.paddingX * 2);
107
107
  const leftPad = " ".repeat(this.paddingX);
108
108
 
109
- // Render all children
109
+ // Render all children. Keep the child lines unpadded: children usually return the same string
110
+ // objects every frame, so the cache check below is a cheap identity comparison per line.
111
+ // Padding here would create new strings that must be compared character by character.
110
112
  const childLines: string[] = [];
111
113
  const mouseChildren: Array<{ component: Component; height: number }> = [];
112
114
  for (const child of this.children) {
113
115
  const lines = child.render(contentWidth);
114
116
  mouseChildren.push({ component: child, height: lines.length });
115
117
  for (const line of lines) {
116
- childLines.push(leftPad + line);
118
+ childLines.push(line);
117
119
  }
118
120
  }
119
121
  this.mouseLayout = { width: contentWidth, children: mouseChildren };
@@ -140,7 +142,7 @@ export class Box implements Component {
140
142
 
141
143
  // Content
142
144
  for (const line of childLines) {
143
- result.push(this.applyBg(line, width));
145
+ result.push(this.applyBg(leftPad + line, width));
144
146
  }
145
147
 
146
148
  // Bottom padding
@@ -158,10 +160,7 @@ export class Box implements Component {
158
160
  const visLen = visibleWidth(line);
159
161
  const padNeeded = Math.max(0, width - visLen);
160
162
  const padded = line + " ".repeat(padNeeded);
161
-
162
- if (this.bgFn) {
163
- return applyBackgroundToLine(padded, width, this.bgFn);
164
- }
165
- return padded;
163
+ // Already padded to width, so apply the background directly instead of measuring the line again.
164
+ return this.bgFn ? this.bgFn(padded) : padded;
166
165
  }
167
166
  }
@@ -253,6 +253,8 @@ const ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS = 20;
253
253
  const DEFAULT_AUTOCOMPLETE_TRIGGER_CHARACTERS = ["@", "#"];
254
254
  // Unquoted completions end at whitespace or CJK punctuation; quoted paths may contain either.
255
255
  const unquotedAutocompleteSuffixRegex = new RegExp(`(?:(?!${autocompleteSeparatorRegex.source}).)*`, "u");
256
+ // Trigger tokens may be wrapped in prose, e.g. "(@src/foo" or "`@src/foo".
257
+ const autocompleteTokenStartSource = `${autocompleteBoundaryRegex.source}[([{<\`]*`;
256
258
 
257
259
  function escapeCharacterClass(value: string): string {
258
260
  return value.replace(/[\\^$.*+?()[\]{}|-]/g, "\\$&");
@@ -260,7 +262,7 @@ function escapeCharacterClass(value: string): string {
260
262
 
261
263
  function buildTriggerPattern(triggerCharacters: string[]): RegExp {
262
264
  return new RegExp(
263
- `${autocompleteBoundaryRegex.source}(?:@"[^"]*|[${triggerCharacters.map(escapeCharacterClass).join("")}]${unquotedAutocompleteSuffixRegex.source})$`,
265
+ `${autocompleteTokenStartSource}(?:@"[^"]*|[${triggerCharacters.map(escapeCharacterClass).join("")}]${unquotedAutocompleteSuffixRegex.source})$`,
264
266
  "u",
265
267
  );
266
268
  }
@@ -268,7 +270,7 @@ function buildTriggerPattern(triggerCharacters: string[]): RegExp {
268
270
  function buildDebouncePattern(triggerCharacters: string[]): RegExp {
269
271
  const escapedWithoutAt = triggerCharacters.filter((character) => character !== "@").map(escapeCharacterClass);
270
272
  return new RegExp(
271
- `${autocompleteBoundaryRegex.source}(?:@(?:"[^"]*|${unquotedAutocompleteSuffixRegex.source})|[${escapedWithoutAt.join("")}]${unquotedAutocompleteSuffixRegex.source})$`,
273
+ `${autocompleteTokenStartSource}(?:@(?:"[^"]*|${unquotedAutocompleteSuffixRegex.source})|[${escapedWithoutAt.join("")}]${unquotedAutocompleteSuffixRegex.source})$`,
272
274
  "u",
273
275
  );
274
276
  }
@@ -246,6 +246,8 @@ export class Markdown implements Component {
246
246
  private cachedText?: string;
247
247
  private cachedWidth?: number;
248
248
  private cachedLines?: string[];
249
+ // Parsed tokens depend only on the source, so they survive theme and width invalidation.
250
+ private cachedTokens?: { source: string; tokens: Token[] };
249
251
 
250
252
  constructor(
251
253
  text: string,
@@ -298,8 +300,12 @@ export class Markdown implements Component {
298
300
  const normalizedText = text.replace(/\t/g, " ");
299
301
 
300
302
  // Parse markdown to HTML-like tokens
301
- const tokens = markdownParser.lexer(normalizedText);
302
- trimPartialClosingFences(tokens);
303
+ let tokens = this.cachedTokens?.source === normalizedText ? this.cachedTokens.tokens : undefined;
304
+ if (!tokens) {
305
+ tokens = markdownParser.lexer(normalizedText);
306
+ trimPartialClosingFences(tokens);
307
+ this.cachedTokens = { source: normalizedText, tokens };
308
+ }
303
309
 
304
310
  // Convert tokens to styled terminal output
305
311
  const renderedLines: string[] = [];
@@ -9,6 +9,33 @@ export {
9
9
  CombinedAutocompleteProvider,
10
10
  type SlashCommand,
11
11
  } from "./autocomplete.ts";
12
+ // Colors and styling
13
+ export {
14
+ backgroundAnsi,
15
+ type Color,
16
+ type ColorMixSpace,
17
+ colorToHex,
18
+ colorToOkhsl,
19
+ colorToOklch,
20
+ colorToRgb,
21
+ foregroundAnsi,
22
+ type IndexedColor,
23
+ indexedColor,
24
+ mixColors,
25
+ type OkhslChannels,
26
+ type OklchChannels,
27
+ type OklchColorValue,
28
+ okhslColor,
29
+ oklchColor,
30
+ parseColor,
31
+ type RgbColorValue,
32
+ rgbColor,
33
+ styleText,
34
+ styleTextWithAnsi,
35
+ type TerminalColorMode,
36
+ type TextAttributes,
37
+ type TextStyle,
38
+ } from "./colors.ts";
12
39
  // Components
13
40
  export { Box } from "./components/box.ts";
14
41
  export { CancellableLoader } from "./components/cancellable-loader.ts";
@@ -77,16 +104,17 @@ export {
77
104
  export { type RenderLatexOptions, renderLatex } from "./latex.ts";
78
105
  // Native platform integration
79
106
  export { getNativeClipboard, type NativeClipboard } from "./native-platform.ts";
107
+ export { oklabToOkhslLightness } from "./oklab.ts";
80
108
  // Input buffering for batch splitting
81
109
  export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "./stdin-buffer.ts";
82
110
  // Terminal interface and implementations
83
111
  export { ProcessTerminal, type Terminal } from "./terminal.ts";
84
112
  // Terminal colors
85
113
  export {
86
- parseOsc11BackgroundColor,
87
114
  parseTerminalColorSchemeReport,
88
115
  type RgbColor,
89
116
  type TerminalColorScheme,
117
+ type TerminalColors,
90
118
  } from "./terminal-colors.ts";
91
119
  // Terminal image support
92
120
  export {
@@ -104,6 +132,7 @@ export {
104
132
  getImageDimensions,
105
133
  getJpegDimensions,
106
134
  getPngDimensions,
135
+ getTerminalColorMode,
107
136
  getWebpDimensions,
108
137
  hyperlink,
109
138
  type ImageDimensions,
@@ -154,3 +183,4 @@ export {
154
183
  visibleWidth,
155
184
  wrapTextWithAnsi,
156
185
  } from "./utils.ts";
186
+ export type { WheelScrollLines } from "./wheel-scroll.ts";
@@ -0,0 +1,23 @@
1
+ {
2
+ "commit": "02eed88fd8912e54a804ddebd409e2e4c08ac5ef",
3
+ "files": {
4
+ "clipboard.h": "4e7d0a70f8e530017307816f097d355b94227c4d",
5
+ "darwin/README.md": "74c5c5911b480e3cffa23b18ec39a00a68fb71bd",
6
+ "darwin/build.sh": "65f7fa4f8cdc19be744b9519e95687bbb046df0e",
7
+ "darwin/prebuilds/darwin-arm64/darwin-platform.node": "3294b6bffd36eaab70d61134e9955992f31f9a8f",
8
+ "darwin/prebuilds/darwin-x64/darwin-platform.node": "014383575508654d9f898ee25631b88db8d9740c",
9
+ "darwin/src/darwin-platform.m": "5cdd8f9122309f2e4ed0aa107fe1f556b9f8934d",
10
+ "linux/README.md": "5efdb5cb69af75d5b596423a11a8411138a43ccd",
11
+ "linux/build.sh": "121b6c45d56020fb2bb78aa6f1a5627f0c094986",
12
+ "linux/prebuilds/linux-arm64/linux-platform-x11.node": "6d3abc4e33fe4fc8f9029cda1201a3a2aee5c050",
13
+ "linux/prebuilds/linux-x64/linux-platform-x11.node": "54fcd629b88533ee6c788a830038aa46cf78c1d7",
14
+ "linux/src/clipboard-worker.h": "93c79d0e7112096b86f38d6fdb1e21c0a72293d4",
15
+ "linux/src/linux-platform-x11.c": "578a850e43de0c4a00cff4f46c8d1cd31dc74a5f",
16
+ "napi.h": "816a8202e2fd4d69d3d1f834fbe353b7ef71744a",
17
+ "win32/README.md": "f3b58607cbaa893d1cdff1b0321ed39e793677ea",
18
+ "win32/build.mjs": "df1ed206131af71e22d042f5924649e7d742c09b",
19
+ "win32/prebuilds/win32-arm64/win32-platform.node": "ec330840e70b863b2b0f295437b78378fb8ac84b",
20
+ "win32/prebuilds/win32-x64/win32-platform.node": "2d0b749b53a17d6049f3e1629c73a847d3851d04",
21
+ "win32/src/win32-platform.c": "a637dda892b695a525ca2cb6b22ee9a8a55cae59"
22
+ }
23
+ }