@visual-z/xeno 0.1.4 → 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 (146) 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/project.ts +21 -3
  35. package/src/core/prompts.ts +33 -5
  36. package/src/core/queue.ts +18 -0
  37. package/src/core/resource-selection.ts +36 -0
  38. package/src/{runtime → core}/resume.ts +15 -22
  39. package/src/core/session-archive.ts +4 -0
  40. package/src/core/session-browser.ts +82 -0
  41. package/src/core/session-entry.ts +56 -0
  42. package/src/core/session-position.ts +26 -0
  43. package/src/core/settings.ts +169 -154
  44. package/src/core/theme.ts +15 -6
  45. package/src/core/thinking.ts +26 -0
  46. package/src/core/tokens.ts +2 -0
  47. package/src/core/transcript.ts +15 -5
  48. package/src/runtime/auth.ts +116 -11
  49. package/src/runtime/catalog.ts +54 -151
  50. package/src/runtime/clipboard.ts +114 -0
  51. package/src/runtime/extension-loader.ts +37 -0
  52. package/src/runtime/extension-ui.ts +55 -0
  53. package/src/runtime/extensions.ts +273 -0
  54. package/src/runtime/images.ts +76 -0
  55. package/src/runtime/loop.ts +213 -83
  56. package/src/runtime/model-config.ts +232 -0
  57. package/src/runtime/model-data.LICENSE +27 -0
  58. package/src/runtime/model-data.ts +21 -0
  59. package/src/runtime/model-replay.ts +10 -0
  60. package/src/runtime/model-scope.ts +20 -0
  61. package/src/runtime/model-sources.ts +151 -0
  62. package/src/runtime/model.ts +74 -11
  63. package/src/runtime/models-fetch.ts +6 -104
  64. package/src/runtime/oauth.ts +4 -3
  65. package/src/runtime/openai-responses.ts +12 -4
  66. package/src/runtime/openai.ts +13 -4
  67. package/src/runtime/package-manager.ts +125 -0
  68. package/src/runtime/pi-provider.ts +235 -0
  69. package/src/runtime/progress.ts +26 -0
  70. package/src/runtime/provider-access.ts +21 -0
  71. package/src/runtime/provider-protocols.ts +29 -0
  72. package/src/runtime/provider-request.ts +73 -0
  73. package/src/runtime/provider.ts +26 -6
  74. package/src/runtime/rich-text.ts +36 -0
  75. package/src/runtime/tools.ts +41 -5
  76. package/src/store/command.ts +7 -1
  77. package/src/store/context-files.ts +1 -1
  78. package/src/store/log.ts +65 -0
  79. package/src/store/models.ts +28 -9
  80. package/src/store/observations.ts +4 -2
  81. package/src/store/packages.ts +98 -0
  82. package/src/store/recovery.ts +8 -0
  83. package/src/store/resource-index.ts +170 -0
  84. package/src/store/resources.ts +75 -142
  85. package/src/store/session-archive.ts +162 -0
  86. package/src/store/session-browser.ts +68 -0
  87. package/src/store/session-position.ts +27 -0
  88. package/src/store/sessions.ts +87 -66
  89. package/src/store/trust.ts +13 -0
  90. package/src/tui/chat-viewport.ts +47 -0
  91. package/src/tui/editor.ts +62 -15
  92. package/src/tui/host.ts +50 -9
  93. package/src/tui/input.ts +8 -2
  94. package/src/tui/select.ts +24 -6
  95. package/src/tui/settings.ts +4 -0
  96. package/src/tui/system-theme.ts +700 -0
  97. package/src/tui/transcript.ts +63 -11
  98. package/src/tui/tree.ts +161 -95
  99. package/src/vendor/pi-rich/LICENSE.upstream +21 -0
  100. package/src/vendor/pi-rich/UPSTREAM.json +17 -0
  101. package/src/vendor/pi-rich/clipboard-command.ts +44 -0
  102. package/src/vendor/pi-rich/exif-orientation.ts +182 -0
  103. package/src/vendor/pi-rich/html.ts +51 -0
  104. package/src/vendor/pi-rich/image-convert.ts +49 -0
  105. package/src/vendor/pi-rich/image-resize-core.ts +164 -0
  106. package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
  107. package/src/vendor/pi-rich/image-resize.ts +123 -0
  108. package/src/vendor/pi-rich/mime.ts +116 -0
  109. package/src/vendor/pi-rich/photon.ts +139 -0
  110. package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
  111. package/src/vendor/pi-tui/UPSTREAM.json +50 -0
  112. package/src/vendor/pi-tui/VENDOR.md +7 -5
  113. package/src/vendor/pi-tui/autocomplete.ts +35 -10
  114. package/src/vendor/pi-tui/colors.ts +367 -0
  115. package/src/vendor/pi-tui/components/box.ts +8 -9
  116. package/src/vendor/pi-tui/components/editor.ts +4 -2
  117. package/src/vendor/pi-tui/components/markdown.ts +8 -2
  118. package/src/vendor/pi-tui/index.ts +31 -1
  119. package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
  120. package/src/vendor/pi-tui/native/clipboard.h +208 -0
  121. package/src/vendor/pi-tui/native/darwin/README.md +22 -0
  122. package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
  123. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  124. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  125. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
  126. package/src/vendor/pi-tui/native/linux/README.md +23 -0
  127. package/src/vendor/pi-tui/native/linux/build.sh +65 -0
  128. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  129. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  130. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
  131. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
  132. package/src/vendor/pi-tui/native/napi.h +91 -0
  133. package/src/vendor/pi-tui/native/win32/README.md +31 -0
  134. package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
  135. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  136. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  137. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
  138. package/src/vendor/pi-tui/native-platform.ts +2 -0
  139. package/src/vendor/pi-tui/oklab.ts +233 -0
  140. package/src/vendor/pi-tui/terminal-colors.ts +26 -8
  141. package/src/vendor/pi-tui/terminal-image.ts +43 -8
  142. package/src/vendor/pi-tui/terminal.ts +16 -9
  143. package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
  144. package/src/vendor/pi-tui/tui.ts +112 -75
  145. package/src/vendor/pi-tui/utils.ts +89 -49
  146. package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
@@ -180,8 +180,10 @@ function finalizeTruncatedResult(
180
180
  * check to avoid running the RGI_Emoji regex unnecessarily.
181
181
  */
182
182
  function graphemeWidth(segment: string): number {
183
- if (segment === "\t") {
184
- return 3;
183
+ if (segment.length === 1) {
184
+ const code = segment.charCodeAt(0);
185
+ if (code >= 0x20 && code <= 0x7e) return 1;
186
+ if (code === 0x09) return 3;
185
187
  }
186
188
 
187
189
  // Some marks occupy cells even without a base character.
@@ -250,9 +252,11 @@ export function visibleWidth(str: string): number {
250
252
  return 0;
251
253
  }
252
254
 
253
- // Fast path: pure ASCII printable
254
- if (isPrintableAscii(str)) {
255
- return str.length;
255
+ // Fast path: printable ASCII, tabs, and ANSI escape sequences. Styled lines take this path, so
256
+ // re-rendering after a theme change does not run grapheme segmentation on every line.
257
+ const asciiWidth = asciiVisibleWidth(str);
258
+ if (asciiWidth !== -1) {
259
+ return asciiWidth;
256
260
  }
257
261
 
258
262
  // Check cache
@@ -266,22 +270,25 @@ export function visibleWidth(str: string): number {
266
270
  if (str.includes("\t")) {
267
271
  clean = clean.replace(/\t/g, " ");
268
272
  }
269
- if (clean.includes("\x1b")) {
273
+ let escapeIndex = clean.indexOf("\x1b");
274
+ if (escapeIndex !== -1) {
270
275
  // Strip supported ANSI/OSC/APC escape sequences in one pass.
271
276
  // This covers CSI styling/cursor codes, OSC hyperlinks and prompt markers,
272
277
  // and APC sequences like CURSOR_MARKER.
273
278
  let stripped = "";
274
- let i = 0;
275
- while (i < clean.length) {
276
- const ansi = extractAnsiCode(clean, i);
277
- if (ansi) {
278
- i += ansi.length;
279
- continue;
279
+ let copyFrom = 0;
280
+ while (escapeIndex !== -1) {
281
+ const length = ansiCodeLength(clean, escapeIndex);
282
+ if (length > 0) {
283
+ stripped += clean.slice(copyFrom, escapeIndex);
284
+ escapeIndex += length;
285
+ copyFrom = escapeIndex;
286
+ } else {
287
+ escapeIndex++;
280
288
  }
281
- stripped += clean[i];
282
- i++;
289
+ escapeIndex = clean.indexOf("\x1b", escapeIndex);
283
290
  }
284
- clean = stripped;
291
+ clean = stripped + clean.slice(copyFrom);
285
292
  }
286
293
 
287
294
  // Calculate width
@@ -412,43 +419,66 @@ export function normalizeTerminalOutput(str: string): string {
412
419
  * Extract ANSI escape sequences from a string at the given position.
413
420
  */
414
421
  export function extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {
415
- if (pos >= str.length || str[pos] !== "\x1b") return null;
422
+ const length = ansiCodeLength(str, pos);
423
+ return length > 0 ? { code: str.substring(pos, pos + length), length } : null;
424
+ }
425
+
426
+ /**
427
+ * Width of a string made of printable ASCII, tabs, and ANSI escape sequences, or -1 if it contains
428
+ * anything else. Matches `visibleWidth` for those strings without allocating.
429
+ */
430
+ function asciiVisibleWidth(str: string): number {
431
+ let width = 0;
432
+ let i = 0;
433
+ while (i < str.length) {
434
+ const code = str.charCodeAt(i);
435
+ if (code >= 0x20 && code <= 0x7e) {
436
+ width++;
437
+ i++;
438
+ } else if (code === 0x09) {
439
+ width += 3;
440
+ i++;
441
+ } else if (code === 0x1b) {
442
+ const length = ansiCodeLength(str, i);
443
+ if (length === 0) return -1;
444
+ i += length;
445
+ } else {
446
+ return -1;
447
+ }
448
+ }
449
+ return width;
450
+ }
451
+
452
+ /** Length of the ANSI/OSC/APC escape sequence starting at `pos`, or 0 if there is none. */
453
+ function ansiCodeLength(str: string, pos: number): number {
454
+ if (pos >= str.length || str.charCodeAt(pos) !== 0x1b) return 0;
416
455
 
417
456
  const next = str[pos + 1];
418
457
 
419
458
  // CSI sequence: ESC [ ... m/G/K/H/J
420
459
  if (next === "[") {
421
- let j = pos + 2;
422
- while (j < str.length && !/[mGKHJ]/.test(str[j]!)) j++;
423
- if (j < str.length) return { code: str.substring(pos, j + 1), length: j + 1 - pos };
424
- return null;
460
+ for (let j = pos + 2; j < str.length; j++) {
461
+ const c = str.charCodeAt(j);
462
+ // m, G, K, H, J
463
+ if (c === 0x6d || c === 0x47 || c === 0x4b || c === 0x48 || c === 0x4a) return j + 1 - pos;
464
+ }
465
+ return 0;
425
466
  }
426
467
 
427
468
  // OSC sequence: ESC ] ... BEL or ESC ] ... ST (ESC \)
428
469
  // Used for hyperlinks (OSC 8), window titles, etc.
429
- if (next === "]") {
430
- let j = pos + 2;
431
- while (j < str.length) {
432
- if (str[j] === "\x07") return { code: str.substring(pos, j + 1), length: j + 1 - pos };
433
- if (str[j] === "\x1b" && str[j + 1] === "\\") return { code: str.substring(pos, j + 2), length: j + 2 - pos };
434
- j++;
435
- }
436
- return null;
437
- }
438
-
439
470
  // APC sequence: ESC _ ... BEL or ESC _ ... ST (ESC \)
440
471
  // Used for cursor marker and application-specific commands
441
- if (next === "_") {
442
- let j = pos + 2;
443
- while (j < str.length) {
444
- if (str[j] === "\x07") return { code: str.substring(pos, j + 1), length: j + 1 - pos };
445
- if (str[j] === "\x1b" && str[j + 1] === "\\") return { code: str.substring(pos, j + 2), length: j + 2 - pos };
446
- j++;
472
+ if (next === "]" || next === "_") {
473
+ for (let j = pos + 2; j < str.length; j++) {
474
+ const c = str.charCodeAt(j);
475
+ if (c === 0x07) return j + 1 - pos;
476
+ if (c === 0x1b && str[j + 1] === "\\") return j + 2 - pos;
447
477
  }
448
- return null;
478
+ return 0;
449
479
  }
450
480
 
451
- return null;
481
+ return 0;
452
482
  }
453
483
 
454
484
  type Osc8Terminator = "\x07" | "\x1b\\";
@@ -739,15 +769,16 @@ class AnsiCodeTracker {
739
769
  }
740
770
 
741
771
  function updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {
742
- let i = 0;
743
- while (i < text.length) {
744
- const ansiResult = extractAnsiCode(text, i);
745
- if (ansiResult) {
746
- tracker.process(ansiResult.code);
747
- i += ansiResult.length;
772
+ let i = text.indexOf("\x1b");
773
+ while (i !== -1) {
774
+ const length = ansiCodeLength(text, i);
775
+ if (length > 0) {
776
+ tracker.process(text.substring(i, i + length));
777
+ i += length;
748
778
  } else {
749
779
  i++;
750
780
  }
781
+ i = text.indexOf("\x1b", i);
751
782
  }
752
783
  }
753
784
 
@@ -758,6 +789,10 @@ export function getActiveBackgroundAnsi(text: string): string {
758
789
  return tracker.getActiveBackgroundCode();
759
790
  }
760
791
 
792
+ function* graphemeSegments(text: string): Generator<string> {
793
+ for (const { segment } of graphemeSegmenter.segment(text)) yield segment;
794
+ }
795
+
761
796
  /**
762
797
  * Split text into words while keeping ANSI codes attached.
763
798
  */
@@ -786,14 +821,19 @@ function splitIntoTokensWithAnsi(text: string): string[] {
786
821
  continue;
787
822
  }
788
823
 
789
- let end = i;
790
- while (end < text.length && !extractAnsiCode(text, end)) {
791
- end++;
824
+ // Visible text runs up to the next escape sequence.
825
+ let end = text.indexOf("\x1b", i + 1);
826
+ while (end !== -1 && ansiCodeLength(text, end) === 0) {
827
+ end = text.indexOf("\x1b", end + 1);
792
828
  }
829
+ if (end === -1) end = text.length;
793
830
 
794
- for (const { segment } of graphemeSegmenter.segment(text.slice(i, end))) {
831
+ const chunk = text.slice(i, end);
832
+ // Printable ASCII characters are single graphemes, so skip the segmenter for them.
833
+ const ascii = isPrintableAscii(chunk);
834
+ for (const segment of ascii ? chunk : graphemeSegments(chunk)) {
795
835
  const segmentIsSpace = segment === " ";
796
- if (!segmentIsSpace && cjkBreakRegex.test(segment)) {
836
+ if (!ascii && !segmentIsSpace && cjkBreakRegex.test(segment)) {
797
837
  flushCurrent();
798
838
  const token = pendingAnsi + segment;
799
839
  pendingAnsi = "";
@@ -0,0 +1,82 @@
1
+ /** Lines moved per mouse-wheel event, or `"auto"` to accelerate fast wheel spins. */
2
+ export type WheelScrollLines = number | "auto";
3
+
4
+ // Several events closer than this belong to one physical notch (Ghostty emits them ~4 ms apart)
5
+ // or come from a high-resolution source. They move one line each and do not accelerate.
6
+ const BURST_GAP_MS = 5;
7
+ // A pause longer than this ends a scroll gesture.
8
+ const GESTURE_GAP_MS = 200;
9
+ // Average event gap that maps to one line per event. Faster events scale up proportionally.
10
+ const REFERENCE_GAP_MS = 100;
11
+ const MAX_AUTO_LINES = 6;
12
+
13
+ /**
14
+ * Local macOS terminals receive wheel and trackpad deltas that the OS has already accelerated,
15
+ * and they emit one event per line. Other platforms, and SSH sessions where the client platform
16
+ * is unknown, usually send one event per wheel notch.
17
+ */
18
+ function terminalAcceleratesWheel(): boolean {
19
+ const env = process.env;
20
+ return (
21
+ process.platform === "darwin" &&
22
+ env.SSH_CONNECTION === undefined &&
23
+ env.SSH_CLIENT === undefined &&
24
+ env.SSH_TTY === undefined
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Converts wheel events into line counts.
30
+ *
31
+ * In `"auto"` mode on terminals that do not accelerate wheel input, the count follows event
32
+ * velocity: an isolated notch moves one line, while a fast spin moves up to six lines per event.
33
+ * For example, notches 100 ms apart move 1 line each, 50 ms apart move 2, and 20 ms apart move 5.
34
+ */
35
+ export class WheelScrollAccelerator {
36
+ private lines: WheelScrollLines;
37
+ private readonly accelerate: boolean;
38
+ private lastTime = Number.NEGATIVE_INFINITY;
39
+ private lastDirection = 0;
40
+ private averageGap: number | undefined;
41
+ private carry = 0;
42
+
43
+ constructor(lines: WheelScrollLines = "auto", accelerate = !terminalAcceleratesWheel()) {
44
+ this.lines = lines;
45
+ this.accelerate = accelerate;
46
+ }
47
+
48
+ setLines(lines: WheelScrollLines): void {
49
+ this.lines = lines;
50
+ this.reset();
51
+ }
52
+
53
+ /** Return the positive line count for a wheel event in `direction` at time `now` (milliseconds). */
54
+ next(direction: -1 | 1, now: number): number {
55
+ if (this.lines !== "auto") return Number.isFinite(this.lines) ? Math.max(1, Math.floor(this.lines)) : 1;
56
+ if (!this.accelerate) return 1;
57
+
58
+ const gap = now - this.lastTime;
59
+ const sameGesture = direction === this.lastDirection && gap <= GESTURE_GAP_MS;
60
+ this.lastTime = now;
61
+ this.lastDirection = direction;
62
+ if (!sameGesture) {
63
+ this.averageGap = undefined;
64
+ this.carry = 0;
65
+ return 1;
66
+ }
67
+ if (gap < BURST_GAP_MS) return 1;
68
+
69
+ this.averageGap = this.averageGap === undefined ? gap : (this.averageGap + gap) / 2;
70
+ const lines = Math.min(MAX_AUTO_LINES, Math.max(1, REFERENCE_GAP_MS / this.averageGap)) + this.carry;
71
+ const whole = Math.floor(lines);
72
+ this.carry = lines - whole;
73
+ return whole;
74
+ }
75
+
76
+ private reset(): void {
77
+ this.lastTime = Number.NEGATIVE_INFINITY;
78
+ this.lastDirection = 0;
79
+ this.averageGap = undefined;
80
+ this.carry = 0;
81
+ }
82
+ }