@visual-z/xeno 0.1.4 → 0.2.1

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 (147) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +143 -33
  3. package/docs/install.md +23 -6
  4. package/docs/providers.md +147 -0
  5. package/package.json +8 -2
  6. package/src/apps/cli.ts +45 -18
  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-input.ts +94 -0
  21. package/src/apps/startup.ts +413 -295
  22. package/src/core/action-defaults.ts +306 -0
  23. package/src/core/attachment-input.ts +17 -0
  24. package/src/core/catalog-merge.ts +19 -0
  25. package/src/core/event-validation.ts +19 -2
  26. package/src/core/events.ts +45 -2
  27. package/src/core/export.ts +6 -2
  28. package/src/core/extension-flags.ts +14 -0
  29. package/src/core/images.ts +46 -0
  30. package/src/core/json-event.ts +2 -0
  31. package/src/core/keymap.ts +43 -119
  32. package/src/core/model-replay.ts +22 -0
  33. package/src/core/model-scope.ts +53 -0
  34. package/src/core/packages.ts +47 -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/tui/chat-viewport.ts +47 -0
  92. package/src/tui/editor.ts +62 -15
  93. package/src/tui/host.ts +50 -9
  94. package/src/tui/input.ts +8 -2
  95. package/src/tui/select.ts +24 -6
  96. package/src/tui/settings.ts +4 -0
  97. package/src/tui/system-theme.ts +700 -0
  98. package/src/tui/transcript.ts +63 -11
  99. package/src/tui/tree.ts +161 -95
  100. package/src/vendor/pi-rich/LICENSE.upstream +21 -0
  101. package/src/vendor/pi-rich/UPSTREAM.json +17 -0
  102. package/src/vendor/pi-rich/clipboard-command.ts +44 -0
  103. package/src/vendor/pi-rich/exif-orientation.ts +182 -0
  104. package/src/vendor/pi-rich/html.ts +51 -0
  105. package/src/vendor/pi-rich/image-convert.ts +49 -0
  106. package/src/vendor/pi-rich/image-resize-core.ts +164 -0
  107. package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
  108. package/src/vendor/pi-rich/image-resize.ts +123 -0
  109. package/src/vendor/pi-rich/mime.ts +116 -0
  110. package/src/vendor/pi-rich/photon.ts +139 -0
  111. package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
  112. package/src/vendor/pi-tui/UPSTREAM.json +50 -0
  113. package/src/vendor/pi-tui/VENDOR.md +7 -5
  114. package/src/vendor/pi-tui/autocomplete.ts +35 -10
  115. package/src/vendor/pi-tui/colors.ts +367 -0
  116. package/src/vendor/pi-tui/components/box.ts +8 -9
  117. package/src/vendor/pi-tui/components/editor.ts +4 -2
  118. package/src/vendor/pi-tui/components/markdown.ts +8 -2
  119. package/src/vendor/pi-tui/index.ts +31 -1
  120. package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
  121. package/src/vendor/pi-tui/native/clipboard.h +208 -0
  122. package/src/vendor/pi-tui/native/darwin/README.md +22 -0
  123. package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
  124. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  125. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  126. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
  127. package/src/vendor/pi-tui/native/linux/README.md +23 -0
  128. package/src/vendor/pi-tui/native/linux/build.sh +65 -0
  129. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  130. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  131. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
  132. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
  133. package/src/vendor/pi-tui/native/napi.h +91 -0
  134. package/src/vendor/pi-tui/native/win32/README.md +31 -0
  135. package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
  136. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  137. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  138. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
  139. package/src/vendor/pi-tui/native-platform.ts +2 -0
  140. package/src/vendor/pi-tui/oklab.ts +233 -0
  141. package/src/vendor/pi-tui/terminal-colors.ts +26 -8
  142. package/src/vendor/pi-tui/terminal-image.ts +43 -8
  143. package/src/vendor/pi-tui/terminal.ts +16 -9
  144. package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
  145. package/src/vendor/pi-tui/tui.ts +112 -75
  146. package/src/vendor/pi-tui/utils.ts +89 -49
  147. package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
@@ -141,6 +141,8 @@ export class ProcessTerminal implements Terminal {
141
141
  private _kittyProtocolActive = false;
142
142
  private _modifyOtherKeysActive = false;
143
143
  private keyboardProtocolPushed = false;
144
+ /** DA1 replies owed to keyboard protocol queries. Later DA1 replies answer other queries and are forwarded. */
145
+ private pendingKeyboardProtocolDeviceAttributes = 0;
144
146
  private keyboardProtocolNegotiationBuffer = "";
145
147
  private keyboardProtocolBufferFlushTimer?: ReturnType<typeof setTimeout>;
146
148
  private stdinBuffer?: StdinBuffer;
@@ -215,16 +217,16 @@ export class ProcessTerminal implements Terminal {
215
217
 
216
218
  // Forward individual sequences to the input handler
217
219
  this.stdinBuffer.on("data", (sequence) => {
218
- const negotiationSequence = this.readKeyboardProtocolNegotiationSequence(sequence);
219
- if (negotiationSequence === "pending") {
220
+ const negotiation = this.readKeyboardProtocolNegotiationSequence(sequence);
221
+ if (negotiation === "pending") {
220
222
  this.scheduleKeyboardProtocolNegotiationBufferFlush();
221
223
  return; // Wait briefly for the rest of a split Kitty response.
222
224
  }
223
- if (this.handleKeyboardProtocolNegotiationSequence(negotiationSequence)) {
225
+ if (negotiation && this.handleKeyboardProtocolNegotiationSequence(negotiation.parsed)) {
224
226
  return;
225
227
  }
226
228
 
227
- this.forwardInputSequence(sequence);
229
+ this.forwardInputSequence(negotiation?.sequence ?? sequence);
228
230
  });
229
231
 
230
232
  // Re-wrap paste content with bracketed paste markers for existing editor handling
@@ -257,15 +259,19 @@ export class ProcessTerminal implements Terminal {
257
259
  this.setupStdinBuffer();
258
260
  process.stdin.on("data", this.stdinDataHandler!);
259
261
  this.keyboardProtocolPushed = true;
262
+ this.pendingKeyboardProtocolDeviceAttributes += 1;
260
263
  this.clearKeyboardProtocolNegotiationBuffer();
261
264
  process.stdout.write(KITTY_KEYBOARD_PROTOCOL_QUERY);
262
265
  }
263
266
 
264
267
  private handleKeyboardProtocolNegotiationSequence(
265
- negotiationSequence: KeyboardProtocolNegotiationSequence | undefined,
268
+ negotiationSequence: KeyboardProtocolNegotiationSequence,
266
269
  ): boolean {
267
- if (!negotiationSequence) return false;
268
270
  this.clearKeyboardProtocolNegotiationBuffer();
271
+ if (negotiationSequence.type === "device-attributes") {
272
+ if (this.pendingKeyboardProtocolDeviceAttributes === 0) return false;
273
+ this.pendingKeyboardProtocolDeviceAttributes -= 1;
274
+ }
269
275
  if (negotiationSequence.type === "kitty-flags") {
270
276
  if (negotiationSequence.flags !== 0) {
271
277
  this.disableModifyOtherKeys();
@@ -285,15 +291,16 @@ export class ProcessTerminal implements Terminal {
285
291
  return true;
286
292
  }
287
293
 
294
+ /** Returns the parsed negotiation reply with its full (possibly reassembled) sequence. */
288
295
  private readKeyboardProtocolNegotiationSequence(
289
296
  sequence: string,
290
- ): KeyboardProtocolNegotiationSequence | "pending" | undefined {
297
+ ): { parsed: KeyboardProtocolNegotiationSequence; sequence: string } | "pending" | undefined {
291
298
  if (this.keyboardProtocolNegotiationBuffer) {
292
299
  const bufferedSequence = this.keyboardProtocolNegotiationBuffer + sequence;
293
300
  const negotiationSequence = parseKeyboardProtocolNegotiationSequence(bufferedSequence);
294
301
  if (negotiationSequence) {
295
302
  this.clearKeyboardProtocolNegotiationBuffer();
296
- return negotiationSequence;
303
+ return { parsed: negotiationSequence, sequence: bufferedSequence };
297
304
  }
298
305
  if (isKeyboardProtocolNegotiationSequencePrefix(bufferedSequence)) {
299
306
  this.setKeyboardProtocolNegotiationBuffer(bufferedSequence);
@@ -303,7 +310,7 @@ export class ProcessTerminal implements Terminal {
303
310
  }
304
311
 
305
312
  const negotiationSequence = parseKeyboardProtocolNegotiationSequence(sequence);
306
- if (negotiationSequence) return negotiationSequence;
313
+ if (negotiationSequence) return { parsed: negotiationSequence, sequence };
307
314
  if (isKeyboardProtocolNegotiationSequencePrefix(sequence)) {
308
315
  this.setKeyboardProtocolNegotiationBuffer(sequence);
309
316
  return "pending";
@@ -57,6 +57,7 @@ import {
57
57
  truncateToWidth,
58
58
  visibleWidth,
59
59
  } from "./utils.ts";
60
+ import { WheelScrollAccelerator, type WheelScrollLines } from "./wheel-scroll.ts";
60
61
 
61
62
  const ENTER_ALT_SCREEN = "\x1b[?1049h";
62
63
  const EXIT_ALT_SCREEN = "\x1b[?1049l";
@@ -164,8 +165,11 @@ interface SearchHighlightRange {
164
165
  }
165
166
 
166
167
  export interface TuiAltScreenOptions {
167
- /** Number of logical lines moved for each mouse-wheel event. */
168
- wheelScrollLines?: number;
168
+ /**
169
+ * Logical lines moved for each mouse-wheel event (default: 1). `"auto"` accelerates fast wheel
170
+ * spins on terminals that send one event per notch. Alt+wheel moves five times as far.
171
+ */
172
+ wheelScrollLines?: WheelScrollLines;
169
173
  /** Capture mouse events for viewport scrolling and application-owned text selection. */
170
174
  mouse?: boolean;
171
175
  /** Style a non-current transcript search match. */
@@ -236,7 +240,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
236
240
  x: number;
237
241
  y: number;
238
242
  };
239
- private readonly wheelScrollLines: number;
243
+ private readonly wheelScroll: WheelScrollAccelerator;
240
244
  private readonly mouseEnabled: boolean;
241
245
  private readonly searchMatchStyle: (text: string) => string;
242
246
  private readonly searchCurrentMatchStyle: (text: string) => string;
@@ -263,7 +267,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
263
267
  };
264
268
  this.implicitScrollView = new ScrollView(this.implicitDocument, { follow: "end", primary: true });
265
269
  this.flashes = new AltScreenFlashContainer(() => this.requestRender());
266
- this.wheelScrollLines = Math.max(1, Math.floor(options.wheelScrollLines ?? 1));
270
+ this.wheelScroll = new WheelScrollAccelerator(options.wheelScrollLines ?? 1);
267
271
  this.mouseEnabled = options.mouse ?? true;
268
272
  this.searchMatchStyle = options.searchMatchStyle ?? ((text) => `\x1b[4m${text}\x1b[24m`);
269
273
  this.searchCurrentMatchStyle = options.searchCurrentMatchStyle ?? ((text) => `\x1b[1;7m${text}\x1b[22;27m`);
@@ -284,6 +288,10 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
284
288
  return this.getPrimaryScrollView().isFollowingEnd;
285
289
  }
286
290
 
291
+ setWheelScrollLines(lines: WheelScrollLines): void {
292
+ this.wheelScroll.setLines(lines);
293
+ }
294
+
287
295
  getCopyOnSelect(): boolean {
288
296
  return this.copyOnSelect;
289
297
  }
@@ -682,9 +690,11 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
682
690
 
683
691
  const wheelEvent = this.parseWheelEvent(data);
684
692
  if (wheelEvent) {
685
- const event = this.createMouseEvent("wheel", wheelEvent.button, wheelEvent.x, wheelEvent.y, {
686
- wheelDelta: wheelEvent.direction * this.getWheelScrollLines(wheelEvent.button),
687
- });
693
+ const lines = this.wheelScroll.next(wheelEvent.direction, performance.now());
694
+ // SGR mouse button codes use bit 3 (value 8) for the Alt modifier.
695
+ const wheelDelta =
696
+ wheelEvent.direction * ((wheelEvent.button & 8) !== 0 ? lines * ALT_WHEEL_SCROLL_MULTIPLIER : lines);
697
+ const event = this.createMouseEvent("wheel", wheelEvent.button, wheelEvent.x, wheelEvent.y, { wheelDelta });
688
698
  const overlay = this.dispatchMouseToOverlay(event);
689
699
  const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
690
700
  if (result) {
@@ -692,7 +702,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
692
702
  return { consume: true };
693
703
  }
694
704
  if (this.shouldDeferViewportInputToOverlay()) return undefined;
695
- this.routeWheel(wheelEvent);
705
+ this.routeWheel(wheelEvent, wheelDelta);
696
706
  return { consume: true };
697
707
  }
698
708
  const mouseEvent = this.parseSgrMouseEvent(data);
@@ -967,13 +977,8 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
967
977
  return undefined;
968
978
  }
969
979
 
970
- private getWheelScrollLines(button: number): number {
971
- // SGR mouse button codes use bit 3 (value 8) for the Alt modifier.
972
- return (button & 8) !== 0 ? this.wheelScrollLines * ALT_WHEEL_SCROLL_MULTIPLIER : this.wheelScrollLines;
973
- }
974
-
975
- private routeWheel(event: WheelEvent): void {
976
- let remaining = event.direction * this.getWheelScrollLines(event.button);
980
+ private routeWheel(event: WheelEvent, delta: number): void {
981
+ let remaining = delta;
977
982
  const seen = new Set<ScrollView>();
978
983
  for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
979
984
  seen.add(scrollView);
@@ -1630,11 +1635,14 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
1630
1635
  const row = clip.y + clip.height - 1;
1631
1636
  if (row >= screen.length || isImageLine(screen[row] ?? "")) return screen;
1632
1637
  const scrollbarColumn = box ? getScrollbarGeometry(box)?.column : undefined;
1633
- const availableWidth = Math.max(0, (scrollbarColumn ?? clip.x + clip.width) - clip.x);
1634
- const text = truncateToWidth(this.scrollToEndIndicator(), availableWidth, "");
1638
+ const label = truncateToWidth(this.scrollToEndIndicator(), clip.width, "");
1639
+ const labelWidth = visibleWidth(label);
1640
+ const column = clip.x + Math.floor((clip.width - labelWidth) / 2);
1641
+ const rightEdge = scrollbarColumn ?? clip.x + clip.width;
1642
+ const availableWidth = Math.max(0, rightEdge - column);
1643
+ const text = truncateToWidth(label, availableWidth, "");
1635
1644
  const textWidth = visibleWidth(text);
1636
1645
  if (textWidth === 0) return screen;
1637
- const column = clip.x + Math.floor((availableWidth - textWidth) / 2);
1638
1646
  const result = [...screen];
1639
1647
  result[row] = compositeTuiLine(result[row] ?? "", text, column, textWidth, width);
1640
1648
  this.scrollToEndIndicatorRect = { row, column, width: textWidth };
@@ -6,11 +6,11 @@ import { performance } from "node:perf_hooks";
6
6
  import { isKeyRelease, matchesKey } from "./keys.ts";
7
7
  import type { Terminal } from "./terminal.ts";
8
8
  import {
9
- isOsc11BackgroundColorResponse,
10
- parseOsc11BackgroundColor,
9
+ parseOscColorResponse,
11
10
  parseTerminalColorSchemeReport,
12
11
  type RgbColor,
13
12
  type TerminalColorScheme,
13
+ type TerminalColors,
14
14
  } from "./terminal-colors.ts";
15
15
  import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.ts";
16
16
  import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.ts";
@@ -81,7 +81,13 @@ export interface TuiMouseDispatchResult extends TuiMouseEventResult {
81
81
  export function dispatchMouseEvent(component: Component, event: TuiMouseEvent): TuiMouseDispatchResult | undefined {
82
82
  const result = component.handleMouse?.(event);
83
83
  if (!result) return undefined;
84
- if ("target" in result) return result as TuiMouseDispatchResult;
84
+ if ("target" in result) {
85
+ // The component forwarded the event to a child it hosts. Like a delegating container, it routes
86
+ // keys to that child itself, so it keeps keyboard focus. Focusing the child directly would leave
87
+ // focus on a detached component once the host removes it, e.g. a closed settings submenu.
88
+ const forwarded = result as TuiMouseDispatchResult;
89
+ return forwarded.focus && component.handleInput ? { ...forwarded, focusTarget: component } : forwarded;
90
+ }
85
91
  if (!result.handled && !result.capture && !result.focus) return undefined;
86
92
  return {
87
93
  ...result,
@@ -137,12 +143,34 @@ export interface Component {
137
143
 
138
144
  export type TuiInputListenerResult = { consume?: boolean; data?: string } | undefined;
139
145
  export type TuiInputListener = (data: string) => TuiInputListenerResult;
140
- type PendingOsc11BackgroundQuery = {
141
- settled: boolean;
142
- resolve: ((rgb: RgbColor | undefined) => void) | undefined;
146
+ type PendingTerminalColorQuery = {
147
+ foreground?: RgbColor;
148
+ background?: RgbColor;
149
+ palette: Array<RgbColor | undefined>;
150
+ /** Targets that already replied, so duplicates do not count twice. */
151
+ replied: Set<string>;
152
+ /**
153
+ * Receives the result: the promise's resolve until the timeout, then `onLateReply`. Unset once the
154
+ * query completed (on the DA1 reply or once every color replied); later replies are ignored.
155
+ */
156
+ deliver: ((colors: TerminalColors) => void) | undefined;
143
157
  timer: NodeJS.Timeout | undefined;
144
158
  };
145
159
 
160
+ const TERMINAL_PALETTE_SIZE = 16;
161
+ /** OSC 10 and 11 plus OSC 4 for every palette color. */
162
+ const TERMINAL_COLOR_REPLY_COUNT = 2 + TERMINAL_PALETTE_SIZE;
163
+ /**
164
+ * Default colors, palette colors 0-15, and a trailing primary device attributes (DA1) request.
165
+ * Every terminal answers DA1 and terminals answer in order, so the DA1 reply marks the end of
166
+ * the color replies, including for terminals that ignore the color queries.
167
+ */
168
+ const TERMINAL_COLOR_QUERY = `\x1b]10;?\x07\x1b]11;?\x07${Array.from(
169
+ { length: TERMINAL_PALETTE_SIZE },
170
+ (_, index) => `\x1b]4;${index};?\x07`,
171
+ ).join("")}\x1b[c`;
172
+ const DEVICE_ATTRIBUTES_RESPONSE_PATTERN = /^\x1b\[\?[\d;]*c$/;
173
+
146
174
  /**
147
175
  * Interface for components that can receive focus and display a hardware cursor.
148
176
  * When focused, the component should emit CURSOR_MARKER at the cursor position
@@ -447,8 +475,10 @@ export interface TUI extends Component {
447
475
  removeInputListener(listener: TuiInputListener): void;
448
476
  onTerminalColorSchemeChange(listener: (scheme: TerminalColorScheme) => void): () => void;
449
477
  setTerminalColorSchemeNotifications(enabled: boolean): void;
450
- queryTerminalBackgroundColor(options: { timeoutMs: number }): Promise<RgbColor | undefined>;
451
- queryTerminalColorScheme(options: { timeoutMs: number }): Promise<TerminalColorScheme | undefined>;
478
+ queryTerminalColors(options: {
479
+ timeoutMs: number;
480
+ onLateReply?: (colors: TerminalColors) => void;
481
+ }): Promise<TerminalColors>;
452
482
  }
453
483
 
454
484
  export const VIEWPORT_TUI = Symbol.for("@earendil-works/pi-tui/viewport");
@@ -479,8 +509,11 @@ export abstract class TuiBase extends Container implements TUI {
479
509
  private clearOnShrink = false;
480
510
  protected fullRedrawCount = 0;
481
511
  protected stopped = false;
482
- private pendingOsc11BackgroundReplies = 0;
483
- private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = [];
512
+ /**
513
+ * Color queries waiting for their DA1 reply, oldest first. Terminals answer in order, so color
514
+ * replies belong to the oldest one. Queries stay here after a timeout to collect late replies.
515
+ */
516
+ private pendingTerminalColorQueries: PendingTerminalColorQuery[] = [];
484
517
  private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>();
485
518
  private terminalColorSchemeNotificationsEnabled = false;
486
519
  /** Directory for debug/crash logs. When undefined, debug logging is disabled and crash dumps fall back to the OS temp directory. */
@@ -529,7 +562,7 @@ export abstract class TuiBase extends Container implements TUI {
529
562
  if (this.showHardwareCursor === enabled) return;
530
563
  this.showHardwareCursor = enabled;
531
564
  if (!enabled) {
532
- this.terminal.hideCursor();
565
+ this.hideTerminalCursor();
533
566
  }
534
567
  this.requestRender();
535
568
  }
@@ -695,7 +728,7 @@ export abstract class TuiBase extends Container implements TUI {
695
728
  if (!options?.nonCapturing && this.isOverlayVisible(entry)) {
696
729
  this.setFocus(component);
697
730
  }
698
- this.terminal.hideCursor();
731
+ this.hideTerminalCursor();
699
732
  this.requestRender();
700
733
 
701
734
  // Return handle for controlling this overlay
@@ -711,7 +744,7 @@ export abstract class TuiBase extends Container implements TUI {
711
744
  const topVisible = this.getTopmostVisibleOverlay();
712
745
  this.setFocus(topVisible?.component ?? entry.preFocus);
713
746
  }
714
- if (this.overlayStack.length === 0) this.terminal.hideCursor();
747
+ if (this.overlayStack.length === 0) this.hideTerminalCursor();
715
748
  this.requestRender();
716
749
  }
717
750
  },
@@ -793,10 +826,15 @@ export abstract class TuiBase extends Container implements TUI {
793
826
  const topVisible = this.getTopmostVisibleOverlay();
794
827
  this.setFocus(topVisible?.component ?? overlay.preFocus);
795
828
  }
796
- if (this.overlayStack.length === 0) this.terminal.hideCursor();
829
+ if (this.overlayStack.length === 0) this.hideTerminalCursor();
797
830
  this.requestRender();
798
831
  }
799
832
 
833
+ /** Hide the cursor while running. After stop(), the shell owns the cursor and it must stay visible. */
834
+ private hideTerminalCursor(): void {
835
+ if (!this.stopped) this.terminal.hideCursor();
836
+ }
837
+
800
838
  /** Check if there are any visible overlays */
801
839
  hasOverlay(): boolean {
802
840
  return this.overlayStack.some((o) => this.isOverlayVisible(o));
@@ -1004,7 +1042,7 @@ export abstract class TuiBase extends Container implements TUI {
1004
1042
  }
1005
1043
 
1006
1044
  private handleTerminalInput(data: string): void {
1007
- if (this.consumeOsc11BackgroundResponse(data)) {
1045
+ if (this.consumeTerminalColorResponse(data)) {
1008
1046
  return;
1009
1047
  }
1010
1048
  if (this.consumeTerminalColorSchemeReport(data)) {
@@ -1081,30 +1119,52 @@ export abstract class TuiBase extends Container implements TUI {
1081
1119
  }
1082
1120
  }
1083
1121
 
1084
- private consumeOsc11BackgroundResponse(data: string): boolean {
1085
- if (this.pendingOsc11BackgroundReplies <= 0) {
1122
+ private consumeTerminalColorResponse(data: string): boolean {
1123
+ const query = this.pendingTerminalColorQueries[0];
1124
+ if (!query) {
1086
1125
  return false;
1087
1126
  }
1127
+ if (DEVICE_ATTRIBUTES_RESPONSE_PATTERN.test(data)) {
1128
+ this.pendingTerminalColorQueries.shift();
1129
+ this.completeTerminalColorQuery(query);
1130
+ return true;
1131
+ }
1088
1132
 
1089
- if (!isOsc11BackgroundColorResponse(data)) {
1133
+ const response = parseOscColorResponse(data);
1134
+ if (!response) {
1090
1135
  return false;
1091
1136
  }
1092
-
1093
- const rgb = parseOsc11BackgroundColor(data);
1094
- this.pendingOsc11BackgroundReplies -= 1;
1095
- const query = this.pendingOsc11BackgroundQueries.shift();
1096
- if (query && !query.settled) {
1097
- query.settled = true;
1098
- if (query.timer) {
1099
- clearTimeout(query.timer);
1100
- query.timer = undefined;
1101
- }
1102
- query.resolve?.(rgb);
1103
- query.resolve = undefined;
1137
+ const { target, rgb } = response;
1138
+ const key = String(target);
1139
+ if (!query.deliver || query.replied.has(key)) {
1140
+ return true;
1141
+ }
1142
+ query.replied.add(key);
1143
+ if (target === "foreground") {
1144
+ query.foreground = rgb;
1145
+ } else if (target === "background") {
1146
+ query.background = rgb;
1147
+ } else if (target < TERMINAL_PALETTE_SIZE) {
1148
+ query.palette[target] = rgb;
1149
+ }
1150
+ if (query.replied.size === TERMINAL_COLOR_REPLY_COUNT) {
1151
+ this.completeTerminalColorQuery(query);
1104
1152
  }
1105
1153
  return true;
1106
1154
  }
1107
1155
 
1156
+ private terminalColorQueryResult(query: PendingTerminalColorQuery): TerminalColors {
1157
+ const palette = query.palette.every((color) => color !== undefined) ? (query.palette as RgbColor[]) : undefined;
1158
+ return { foreground: query.foreground, background: query.background, palette };
1159
+ }
1160
+
1161
+ private completeTerminalColorQuery(query: PendingTerminalColorQuery): void {
1162
+ const deliver = query.deliver;
1163
+ query.deliver = undefined;
1164
+ clearTimeout(query.timer);
1165
+ deliver?.(this.terminalColorQueryResult(query));
1166
+ }
1167
+
1108
1168
  private consumeTerminalColorSchemeReport(data: string): boolean {
1109
1169
  const scheme = parseTerminalColorSchemeReport(data);
1110
1170
  if (!scheme) {
@@ -1400,57 +1460,34 @@ export abstract class TuiBase extends Container implements TUI {
1400
1460
  }
1401
1461
 
1402
1462
  /**
1403
- * Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`).
1404
- * @param timeoutMs Query timeout in milliseconds.
1405
- * @returns Promise containing the parsed RGB color, or undefined if it times out or fails to parse.
1463
+ * Query the terminal's theme colors: the default foreground (OSC 10), the default background
1464
+ * (OSC 11), and ANSI colors 0-15 (OSC 4), followed by a DA1 request that marks the end of the
1465
+ * replies. Resolves when the DA1 reply or all color replies arrive, or when the timeout expires.
1466
+ * Colors the terminal did not report are undefined; the palette is only set when all 16 arrived.
1467
+ * @param timeoutMs Query timeout in milliseconds, for terminals that do not answer DA1 either.
1468
+ * @param onLateReply Receives the replies if the query completes after the timeout, e.g. over slow links.
1406
1469
  */
1407
- queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise<RgbColor | undefined> {
1470
+ queryTerminalColors({
1471
+ timeoutMs,
1472
+ onLateReply,
1473
+ }: {
1474
+ timeoutMs: number;
1475
+ onLateReply?: (colors: TerminalColors) => void;
1476
+ }): Promise<TerminalColors> {
1408
1477
  return new Promise((resolve) => {
1409
- const query: PendingOsc11BackgroundQuery = {
1410
- settled: false,
1411
- resolve,
1478
+ const query: PendingTerminalColorQuery = {
1479
+ palette: Array.from({ length: TERMINAL_PALETTE_SIZE }, () => undefined),
1480
+ replied: new Set(),
1481
+ deliver: resolve,
1412
1482
  timer: undefined,
1413
1483
  };
1414
-
1484
+ // Resolve with the replies so far, and keep collecting late replies for `onLateReply`.
1415
1485
  query.timer = setTimeout(() => {
1416
- if (query.settled) {
1417
- return;
1418
- }
1419
- query.settled = true;
1420
- query.timer = undefined;
1421
- query.resolve?.(undefined);
1422
- query.resolve = undefined;
1486
+ query.deliver = onLateReply;
1487
+ resolve(this.terminalColorQueryResult(query));
1423
1488
  }, timeoutMs);
1424
- this.pendingOsc11BackgroundQueries.push(query);
1425
- this.pendingOsc11BackgroundReplies += 1;
1426
- this.terminal.write("\x1b]11;?\x07");
1427
- });
1428
- }
1429
-
1430
- /**
1431
- * Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`).
1432
- * Terminals that support the color palette notification protocol reply with
1433
- * `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light.
1434
- */
1435
- queryTerminalColorScheme({ timeoutMs }: { timeoutMs: number }): Promise<TerminalColorScheme | undefined> {
1436
- return new Promise((resolve) => {
1437
- let settled = false;
1438
- let timer: NodeJS.Timeout | undefined;
1439
- let unsubscribe: () => void = () => {};
1440
- const settle = (scheme: TerminalColorScheme | undefined) => {
1441
- if (settled) return;
1442
- settled = true;
1443
- if (timer) {
1444
- clearTimeout(timer);
1445
- timer = undefined;
1446
- }
1447
- unsubscribe();
1448
- resolve(scheme);
1449
- };
1450
-
1451
- unsubscribe = this.onTerminalColorSchemeChange(settle);
1452
- timer = setTimeout(() => settle(undefined), timeoutMs);
1453
- this.terminal.write("\x1b[?996n");
1489
+ this.pendingTerminalColorQueries.push(query);
1490
+ this.terminal.write(TERMINAL_COLOR_QUERY);
1454
1491
  });
1455
1492
  }
1456
1493
  }