@timurproko/a1 0.1.8-dev.332 → 0.1.8-dev.368

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 (110) hide show
  1. package/README.md +40 -11
  2. package/dist/composition/owned-ui.d.ts +2 -0
  3. package/dist/composition/owned-ui.js +20 -3
  4. package/dist/contracts/owned-ui/index.d.ts +2 -0
  5. package/dist/contracts/owned-ui/index.js +2 -0
  6. package/dist/contracts/owned-ui/model.d.ts +38 -3
  7. package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
  8. package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
  9. package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
  10. package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
  11. package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
  12. package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
  13. package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
  14. package/dist/contracts/owned-ui/validation.js +45 -2
  15. package/dist/contracts/presentation/index.d.ts +8 -0
  16. package/dist/features/owned-ui/settings-app.js +1 -1
  17. package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
  18. package/dist/features/prompt-history/image-sidecar.js +130 -0
  19. package/dist/features/prompt-history/index.d.ts +2 -0
  20. package/dist/features/prompt-history/index.js +1 -0
  21. package/dist/features/prompt-history/paths.d.ts +1 -0
  22. package/dist/features/prompt-history/paths.js +2 -1
  23. package/dist/features/prompt-history/store.d.ts +1 -1
  24. package/dist/features/prompt-history/store.js +46 -2
  25. package/dist/features/prompt-history/worker.js +1 -1
  26. package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
  27. package/dist/features/prompt-suggestions/diagnostics.js +82 -0
  28. package/dist/features/prompt-suggestions/index.d.ts +1 -0
  29. package/dist/features/prompt-suggestions/index.js +1 -0
  30. package/dist/foundation/release/bootstrap.d.ts +2 -0
  31. package/dist/foundation/release/bootstrap.js +54 -18
  32. package/dist/foundation/release/index.d.ts +1 -0
  33. package/dist/foundation/release/index.js +1 -0
  34. package/dist/foundation/release/update-launch.d.ts +6 -0
  35. package/dist/foundation/release/update-launch.js +17 -0
  36. package/dist/foundation/release/update.d.ts +2 -0
  37. package/dist/foundation/release/update.js +20 -21
  38. package/dist/foundation/supervision/main.js +5 -2
  39. package/dist/foundation/supervision/server.js +40 -13
  40. package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
  41. package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
  42. package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
  43. package/dist/integrations/pi/components/prompt-input-port.js +1 -0
  44. package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
  45. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  46. package/dist/integrations/pi/components/shell-footer-status.js +9 -5
  47. package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
  48. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
  49. package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
  51. package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
  52. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
  53. package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
  54. package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
  55. package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
  56. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
  57. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
  60. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  61. package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
  62. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  63. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  64. package/dist/integrations/pi/engine/adapter.js +181 -84
  65. package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
  66. package/dist/integrations/pi/engine/tool-rendering.js +122 -0
  67. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
  68. package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
  69. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
  70. package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
  71. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
  72. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
  73. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
  74. package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
  75. package/dist/integrations/pi/session-ui/session-shell.js +51 -31
  76. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
  77. package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
  78. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  79. package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
  80. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
  81. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
  82. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  83. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
  84. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
  85. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
  86. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/linux-x64/manifest.json +1 -1
  89. package/dist/native/win32-x64/manifest.json +2 -2
  90. package/dist/native/win32-x64/process-guardian.exe +0 -0
  91. package/dist/product-identity.d.ts +1 -1
  92. package/dist/product-identity.js +1 -1
  93. package/dist/product-identity.json +1 -0
  94. package/dist/ui/components/index.d.ts +1 -0
  95. package/dist/ui/components/index.js +1 -0
  96. package/dist/ui/components/line-input.d.ts +2 -3
  97. package/dist/ui/components/line-input.js +4 -20
  98. package/dist/ui/components/mouse.d.ts +1 -1
  99. package/dist/ui/components/mouse.js +4 -3
  100. package/dist/ui/components/prompt-input.d.ts +32 -0
  101. package/dist/ui/components/prompt-input.js +54 -0
  102. package/docs/architecture/boundaries.md +1 -0
  103. package/docs/architecture/history-editor-provenance.md +5 -1
  104. package/docs/architecture/project-structure.md +1 -0
  105. package/docs/architecture/prompt-suggestions.md +68 -0
  106. package/docs/architecture/toolchain.md +1 -1
  107. package/docs/architecture/ui-reference-provenance.md +23 -0
  108. package/docs/ci-release-runbook.md +45 -7
  109. package/docs/features/modal-content-interaction.md +37 -0
  110. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { samePointerSurfaces } from "../tui-runtime/index.js";
1
2
  import { TranscriptViewport, readVisibleHyperlinks, routeMouseInput, scrollForTrackPage, scrollForThumbRow, scrollbarSelectionRows, scrollbarWheelRows, } from "../../../ui/components/index.js";
2
3
  /**
3
4
  * Owns the stateful interaction policy of the bare-A1 session viewport. The
@@ -17,6 +18,10 @@ export class SessionViewportController {
17
18
  scrollbarSpeed: "normal",
18
19
  };
19
20
  #dragGrabOffset = null;
21
+ #inputSurface;
22
+ #inputSurfacePending = false;
23
+ #overlays = [];
24
+ #pointerOwner;
20
25
  // Invariant: a left-button sequence begun in dock chrome remains owned by the dock.
21
26
  #dockPointerSuppressed = false;
22
27
  // Invariant: a left-button sequence begun in non-selectable transcript tail chrome is held there.
@@ -70,7 +75,33 @@ export class SessionViewportController {
70
75
  setEditorPointerFrame(frame) {
71
76
  this.#editorPointerFrame = frame;
72
77
  }
78
+ /** Replacement input changed before its new bounds have been painted. */
79
+ invalidateInputSurface() {
80
+ this.#inputSurfacePending = true;
81
+ this.#cancelGesture();
82
+ }
83
+ setInputSurfaceFrame(surface) {
84
+ if (!samePointerSurfaces(this.#inputSurface === undefined ? [] : [this.#inputSurface], surface === undefined ? [] : [surface])) {
85
+ this.#cancelGesture();
86
+ }
87
+ this.#inputSurface = surface;
88
+ this.#inputSurfacePending = false;
89
+ }
90
+ setOverlaySurfaces(surfaces) {
91
+ if (surfaces === this.#overlays || surfaces !== null && this.#overlays !== null && samePointerSurfaces(surfaces, this.#overlays))
92
+ return;
93
+ this.#cancelGesture();
94
+ this.#overlays = surfaces;
95
+ }
96
+ get inputGeometryReady() {
97
+ return !this.#inputSurfacePending && this.#overlays !== null;
98
+ }
73
99
  compose(input) {
100
+ const previous = this.#viewport.frame;
101
+ if ((this.#inputSurface !== undefined || this.#overlays === null || this.#overlays.length > 0)
102
+ && previous !== null && (previous.rows.length !== input.height || previous.descriptor.width !== input.width
103
+ || previous.hits.viewportHeight !== Math.max(0, input.height - input.dockRows.length)))
104
+ this.#cancelGesture();
74
105
  this.#viewport.setConfig(this.#config);
75
106
  const selectionRevision = this.#viewport.selectionRevision;
76
107
  const frame = this.#viewport.compose({
@@ -132,6 +163,10 @@ export class SessionViewportController {
132
163
  this.#pointerPosition = undefined;
133
164
  this.#hoveredHyperlinkKey = undefined;
134
165
  this.#viewport.reset();
166
+ this.#pointerOwner = undefined;
167
+ this.#dragGrabOffset = null;
168
+ this.#dockPointerSuppressed = false;
169
+ this.#tailPointerSuppressed = false;
135
170
  }
136
171
  clearPointerState() {
137
172
  this.#presentationRevision += 1;
@@ -146,33 +181,51 @@ export class SessionViewportController {
146
181
  this.#stopSelectionAutoScroll();
147
182
  this.#viewport.clearSelection();
148
183
  this.#viewport.clearTransient();
184
+ this.#pointerOwner = undefined;
185
+ }
186
+ #cancelGesture() {
187
+ const held = this.#pointerOwner !== undefined || this.#viewport.selectionActive || this.#editorPointerSelecting;
188
+ const repaint = this.#viewport.hasSelection || held;
189
+ if (!repaint && this.#pointerPosition === undefined && this.#hoveredHyperlinkKey === undefined)
190
+ return;
191
+ const editorFrame = this.#editorPointerFrame;
192
+ this.clearPointerState();
193
+ this.#editorPointerFrame = editorFrame;
194
+ if (held)
195
+ this.#pointerOwner = "drain";
196
+ if (repaint)
197
+ this.#requestRender();
149
198
  }
150
- handlePreInput(data, allowWheel = true, now = Date.now()) {
199
+ handlePreInput(data, allowWheel = true, now = Date.now(), editorActive = true) {
151
200
  if (!this.#enabled)
152
201
  return { data, consumed: false };
153
202
  // Compatibility: Pi components share one keybinding manager. Restore bare A1's aliases
154
203
  // before the focused vanilla editor handles this input.
155
- this.#editor.activateKeybindings();
204
+ if (editorActive)
205
+ this.#editor.activateKeybindings();
206
+ // Protocol: bracketed paste is opaque input, even if its text resembles mouse reports.
207
+ if (data.startsWith("\u001b[200~"))
208
+ return { data, consumed: false };
156
209
  // Platform: handle the physical paste chord at the pre-input boundary. Windows
157
210
  // terminals vary between forwarding Ctrl+V and terminal-owned bracketed paste.
158
- if (this.#editor.matchesTerminalKey(data, "ctrl+v") && this.#editor.pasteClipboard()) {
211
+ if (editorActive && this.#editor.matchesTerminalKey(data, "ctrl+v") && this.#editor.pasteClipboard()) {
159
212
  return { data: "", consumed: true };
160
213
  }
161
214
  // Platform: URL chip deletion must overwrite terminal link cells in the same frame.
162
- if (EDITOR_LINK_DELETION_INPUTS.has(data) && this.#hasEditorLinks()) {
215
+ if (editorActive && EDITOR_LINK_DELETION_INPUTS.has(data) && this.#hasEditorLinks()) {
163
216
  this.#requestHyperlinkCleanup();
164
217
  this.#requestRender();
165
218
  }
166
219
  // Invariant: content shortcuts never reach the prompt, even before the first frame
167
220
  // or when scrolling is a no-op. Plain Home/End remain editor line navigation.
168
- if (allowWheel && this.#editor.matchesTerminalKey(data, "ctrl+home")) {
221
+ if (editorActive && allowWheel && this.#editor.matchesTerminalKey(data, "ctrl+home")) {
169
222
  if (this.#viewport.scrollTo(0, now)) {
170
223
  this.#scheduleActivityExpiry();
171
224
  this.#requestRender();
172
225
  }
173
226
  return { data: "", consumed: true };
174
227
  }
175
- if (allowWheel && this.#editor.matchesTerminalKey(data, "ctrl+end")) {
228
+ if (editorActive && allowWheel && this.#editor.matchesTerminalKey(data, "ctrl+end")) {
176
229
  if (!this.#viewport.followingEnd) {
177
230
  this.#viewport.scrollToEnd(now);
178
231
  this.#scheduleActivityExpiry();
@@ -191,7 +244,7 @@ export class SessionViewportController {
191
244
  });
192
245
  }
193
246
  if (data === "\u0003") {
194
- if (this.#editor.hasSelection()) {
247
+ if (editorActive && this.#editor.hasSelection()) {
195
248
  if (this.#viewport.clearSelection())
196
249
  this.#requestRender();
197
250
  }
@@ -204,8 +257,9 @@ export class SessionViewportController {
204
257
  }
205
258
  }
206
259
  }
207
- if (allowWheel && (SHIFT_UP_INPUTS.has(data) || SHIFT_DOWN_INPUTS.has(data))) {
208
- const scrolled = SHIFT_UP_INPUTS.has(data)
260
+ const previousPrompt = SHIFT_UP_INPUTS.has(data) || this.#editor.matchesTerminalKey(data, "alt+home");
261
+ if (editorActive && allowWheel && (previousPrompt || SHIFT_DOWN_INPUTS.has(data))) {
262
+ const scrolled = previousPrompt
209
263
  ? this.#viewport.scrollToPreviousPrompt(now)
210
264
  : this.#viewport.scrollToNextPrompt(now);
211
265
  if (scrolled) {
@@ -221,14 +275,44 @@ export class SessionViewportController {
221
275
  let repaint = false;
222
276
  let forceRepaint = false;
223
277
  let activity = false;
224
- const routed = routeMouseInput(data, event => {
278
+ const routed = routeMouseInput(data, (event, report) => {
225
279
  const hits = frame.hits;
226
- const overRail = hits.rail !== null
280
+ const overModal = this.#modalAt(event.column, event.row);
281
+ const wheel = event.kind === "wheel-up" || event.kind === "wheel-down";
282
+ // Invariant: ownership is latched for a complete gesture. Modal reports bypass the outer
283
+ // fullscreen selection layer without ever invoking the hidden ordinary editor.
284
+ if (this.#pointerOwner === "drain" || !this.inputGeometryReady) {
285
+ if (event.kind === "release")
286
+ this.#pointerOwner = undefined;
287
+ else if (event.kind === "press")
288
+ this.#pointerOwner = "drain";
289
+ return true;
290
+ }
291
+ const owner = !wheel && this.#pointerOwner !== undefined ? this.#pointerOwner : overModal;
292
+ if (owner !== undefined && owner !== "viewport") {
293
+ if (event.kind === "press")
294
+ this.#pointerOwner = owner;
295
+ if (event.kind === "release")
296
+ this.#pointerOwner = undefined;
297
+ if (event.kind === "press" && this.#viewport.clearSelection())
298
+ repaint = true;
299
+ this.#viewport.setRailHovered(false);
300
+ this.#viewport.setStickyHovered(false);
301
+ this.#pointerPosition = undefined;
302
+ owner.component.handleInput?.(report);
303
+ repaint = true;
304
+ return true;
305
+ }
306
+ if (event.kind === "press")
307
+ this.#pointerOwner = "viewport";
308
+ if (event.kind === "release")
309
+ this.#pointerOwner = undefined;
310
+ const overRail = overModal === undefined && hits.rail !== null
227
311
  && event.column === hits.rail.column
228
312
  && event.row >= hits.rail.rowStart
229
313
  && event.row < hits.rail.rowStart + hits.rail.trackHeight;
230
- const overSticky = hits.sticky !== null && event.row === hits.sticky.row && event.column <= hits.sticky.width;
231
- const overBottom = hits.bottom !== null && event.row === hits.bottom.row
314
+ const overSticky = overModal === undefined && hits.sticky !== null && event.row === hits.sticky.row && event.column <= hits.sticky.width;
315
+ const overBottom = overModal === undefined && hits.bottom !== null && event.row === hits.bottom.row
232
316
  && event.column >= hits.bottom.columnStart && event.column <= hits.bottom.columnEnd;
233
317
  const previousPointer = this.#pointerPosition;
234
318
  const wasOverBottom = hits.bottom !== null && previousPointer !== undefined
@@ -251,7 +335,7 @@ export class SessionViewportController {
251
335
  this.#viewport.setRailHovered(overRail);
252
336
  this.#viewport.setStickyHovered(overSticky);
253
337
  repaint = true;
254
- if (this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
338
+ if (editorActive && this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
255
339
  this.#editor.handlePointer({
256
340
  kind: "motion",
257
341
  button: event.button,
@@ -287,7 +371,7 @@ export class SessionViewportController {
287
371
  return true;
288
372
  }
289
373
  if (event.kind === "press") {
290
- const editorFrame = this.#editorPointerFrame;
374
+ const editorFrame = editorActive ? this.#editorPointerFrame : undefined;
291
375
  if (event.button === 2 && editorFrame !== undefined
292
376
  && event.row >= editorFrame.rowStart && event.row <= editorFrame.rowEnd) {
293
377
  this.#stopSelectionAutoScroll();
@@ -362,7 +446,7 @@ export class SessionViewportController {
362
446
  return true;
363
447
  }
364
448
  if (event.kind === "release") {
365
- if (this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
449
+ if (editorActive && this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
366
450
  const wasEditorSelecting = this.#editorPointerSelecting;
367
451
  this.#editor.handlePointer({
368
452
  kind: "release",
@@ -408,6 +492,12 @@ export class SessionViewportController {
408
492
  }
409
493
  return routed;
410
494
  }
495
+ #modalAt(column, row) {
496
+ const contains = (surface) => column >= surface.columnStart && column <= surface.columnEnd
497
+ && row >= surface.rowStart && row <= surface.rowEnd;
498
+ const overlay = this.#overlays?.findLast(contains);
499
+ return overlay ?? (this.#inputSurface !== undefined && contains(this.#inputSurface) ? this.#inputSurface : undefined);
500
+ }
411
501
  #hyperlinkKeyAt(frame, column, row) {
412
502
  if (row < 1 || row > frame.rows.length || column < 1)
413
503
  return undefined;
@@ -22,6 +22,8 @@ export declare class PiTuiRuntimeAdapter {
22
22
  setFocus(component: PiTuiComponentPort | null): void;
23
23
  showOverlay(component: PiTuiComponentPort, options?: PiTuiOverlayOptions): PiTuiOverlayHandle;
24
24
  hasOverlay(): boolean;
25
+ /** Keyboard capture is independent of a passive overlay's visible pointer region. */
26
+ hasFocusedOverlay(): boolean;
25
27
  /**
26
28
  * Writes a terminal control sequence. Used to enable and disable mouse
27
29
  * reporting while an A1-owned application is presented, and for nothing else:
@@ -1,4 +1,6 @@
1
1
  import { HStack, ProcessTerminal, ScrollView, TuiAltScreen, TuiMainScreen, VStack, visibleWidth, } from "#pi-tui";
2
+ import { MouseReportInput } from "./mouse-report-input.js";
3
+ import { GeometryObservedAltScreen, OverlayGeometryTracker } from "./overlay-geometry.js";
2
4
  import { boundedCleanup, EMERGENCY_TERMINAL_RESET } from "../../../foundation/terminal-cleanup/index.js";
3
5
  import { InputPresentationCoordinator, } from "./input-presentation-coordinator.js";
4
6
  /** Identifies the Pi TUI lifecycle stage that failed while preserving the original cause. */
@@ -103,6 +105,7 @@ export class PiTuiRuntimeAdapter {
103
105
  #bridges = new WeakMap();
104
106
  #mountedComponents = new Set();
105
107
  #scrollViews = new Map();
108
+ #overlayGeometry;
106
109
  #overlayDisposers = new Set();
107
110
  #overlayInputCoordination = new Map();
108
111
  #inputListeners = new Map();
@@ -117,6 +120,7 @@ export class PiTuiRuntimeAdapter {
117
120
  #terminalProgress = false;
118
121
  constructor(options) {
119
122
  this.#root = options.root;
123
+ this.#overlayGeometry = options.onOverlayGeometry === undefined ? undefined : new OverlayGeometryTracker(options.onOverlayGeometry);
120
124
  this.#terminal = options.terminal ?? new ProcessTerminal();
121
125
  this.#inputDiagnostics = options.inputDiagnostics;
122
126
  this.#diagnosticNow = options.inputDiagnostics?.now ?? (() => performance.now());
@@ -144,8 +148,8 @@ export class PiTuiRuntimeAdapter {
144
148
  }),
145
149
  });
146
150
  this.#tuiTerminal = this.#inputCoordinator === undefined
147
- ? preInputTerminal(decoratedTerminal, data => this.#routePreInput(data))
148
- : coordinatedInputTerminal(decoratedTerminal, this.#inputCoordinator, sink => { this.#inputSink = sink; });
151
+ ? preInputTerminal(decoratedTerminal, data => this.#routePreInput(data), this.#overlayGeometry !== undefined)
152
+ : coordinatedInputTerminal(decoratedTerminal, this.#inputCoordinator, sink => { this.#inputSink = sink; }, this.#overlayGeometry !== undefined);
149
153
  this.#layoutRoot = options.layoutRoot;
150
154
  this.#logDirectory = options.logDirectory;
151
155
  this.#rootBridge = new ComponentBridge(options.root, options.inputDiagnostics === undefined
@@ -256,6 +260,8 @@ export class PiTuiRuntimeAdapter {
256
260
  }
257
261
  setFocus(component) {
258
262
  this.#assertRunning("focus");
263
+ if (this.#overlayInputCoordination.size > 0)
264
+ this.#overlayGeometry?.invalidate();
259
265
  this.#tui.setFocus(this.#bridgeFor(component));
260
266
  }
261
267
  showOverlay(component, options) {
@@ -264,6 +270,7 @@ export class PiTuiRuntimeAdapter {
264
270
  throw new TypeError("Pi TUI component is already mounted");
265
271
  const bridge = new ComponentBridge(component);
266
272
  this.#bridges.set(component, bridge);
273
+ this.#overlayGeometry?.ports.set(bridge, component);
267
274
  let hidden = false;
268
275
  const dispose = () => {
269
276
  if (hidden)
@@ -282,6 +289,10 @@ export class PiTuiRuntimeAdapter {
282
289
  hasOverlay() {
283
290
  return this.#tui.hasOverlay();
284
291
  }
292
+ /** Keyboard capture is independent of a passive overlay's visible pointer region. */
293
+ hasFocusedOverlay() {
294
+ return this.#tui instanceof GeometryObservedAltScreen ? this.#tui.overlayFocused : this.#focusedOverlayInputCoordination() !== undefined;
295
+ }
285
296
  /**
286
297
  * Writes a terminal control sequence. Used to enable and disable mouse
287
298
  * reporting while an A1-owned application is presented, and for nothing else:
@@ -435,9 +446,13 @@ export class PiTuiRuntimeAdapter {
435
446
  throw failure;
436
447
  }
437
448
  #createTui(mode, hardwareCursor) {
438
- return mode === "fullscreen"
439
- ? new TuiAltScreen(this.#tuiTerminal, hardwareCursor, this.#logDirectory, this.#tuiOptions)
440
- : new TuiMainScreen(this.#tuiTerminal, hardwareCursor, this.#logDirectory);
449
+ if (mode !== "fullscreen")
450
+ return new TuiMainScreen(this.#tuiTerminal, hardwareCursor, this.#logDirectory);
451
+ if (this.#overlayGeometry === undefined)
452
+ return new TuiAltScreen(this.#tuiTerminal, hardwareCursor, this.#logDirectory, this.#tuiOptions);
453
+ const screen = new GeometryObservedAltScreen(this.#tuiTerminal, hardwareCursor, this.#logDirectory, this.#tuiOptions);
454
+ screen.geometry = this.#overlayGeometry;
455
+ return screen;
441
456
  }
442
457
  #routePreInput(data) {
443
458
  let current = data;
@@ -600,20 +615,23 @@ export class PiTuiRuntimeAdapter {
600
615
  this.#disposeRoot();
601
616
  }
602
617
  }
603
- function coordinatedInputTerminal(terminal, coordinator, setSink) {
618
+ function coordinatedInputTerminal(terminal, coordinator, setSink, frameMouse) {
619
+ const mouse = frameMouse ? new MouseReportInput(data => coordinator.accept(data)) : undefined;
604
620
  return {
605
621
  get columns() { return terminal.columns; },
606
622
  get rows() { return terminal.rows; },
607
623
  get kittyProtocolActive() { return terminal.kittyProtocolActive; },
608
624
  start(onInput, onResize) {
609
625
  setSink(onInput);
610
- terminal.start(data => coordinator.accept(data), () => {
626
+ terminal.start(data => mouse === undefined ? coordinator.accept(data) : mouse.accept(data), () => {
611
627
  coordinator.flush();
628
+ mouse?.reset();
612
629
  onResize();
613
630
  });
614
631
  },
615
632
  stop() {
616
633
  coordinator.flush();
634
+ mouse?.reset();
617
635
  setSink(undefined);
618
636
  terminal.stop();
619
637
  },
@@ -656,19 +674,25 @@ function diagnosticTerminal(terminal, trace) {
656
674
  setProgress: active => terminal.setProgress(active),
657
675
  };
658
676
  }
659
- function preInputTerminal(terminal, route) {
677
+ function preInputTerminal(terminal, route, frameMouse) {
678
+ let mouse;
660
679
  return {
661
680
  get columns() { return terminal.columns; },
662
681
  get rows() { return terminal.rows; },
663
682
  get kittyProtocolActive() { return terminal.kittyProtocolActive; },
664
683
  start(onInput, onResize) {
665
- terminal.start(data => {
684
+ const deliver = (data) => {
666
685
  const routed = route(data);
667
686
  if (routed.length > 0)
668
687
  onInput(routed);
669
- }, onResize);
688
+ };
689
+ mouse = frameMouse ? new MouseReportInput(deliver) : undefined;
690
+ terminal.start(data => mouse === undefined ? deliver(data) : mouse.accept(data), () => {
691
+ mouse?.reset();
692
+ onResize();
693
+ });
670
694
  },
671
- stop: () => terminal.stop(),
695
+ stop: () => { mouse?.reset(); terminal.stop(); },
672
696
  drainInput: (maxMs, idleMs) => terminal.drainInput(maxMs, idleMs),
673
697
  write: data => terminal.write(data),
674
698
  moveBy: lines => terminal.moveBy(lines),
@@ -1,4 +1,4 @@
1
- import type { PresentationComponentPort } from "../../../contracts/presentation/index.js";
1
+ import type { PresentationComponentPort, PresentationPointerSurface } from "../../../contracts/presentation/index.js";
2
2
  import type { PiTuiInputCoordinationDecision, PiTuiInputCoordinationScheduler, PiTuiInputSurfaceKind } from "./input-presentation-coordinator.js";
3
3
  export type PiTuiRuntimeState = "idle" | "running" | "stopping" | "stopped" | "failed";
4
4
  export interface PiTuiViewport {
@@ -11,6 +11,7 @@ export interface PiTuiScrollState {
11
11
  readonly followingEnd: boolean;
12
12
  readonly scrollbarVisible: boolean;
13
13
  }
14
+ export type PiTuiPointerSurface = PresentationPointerSurface;
14
15
  export interface PiTuiComponentPort extends PresentationComponentPort {
15
16
  readonly wantsKeyRelease?: boolean;
16
17
  }
@@ -112,6 +113,8 @@ export interface PiTuiInputDiagnosticsEvent {
112
113
  }
113
114
  export interface PiTuiRuntimeAdapterOptions {
114
115
  readonly root: PiTuiComponentPort;
116
+ /** Bare viewport only: painted overlays in back-to-front order; null while geometry is stale. */
117
+ readonly onOverlayGeometry?: (surfaces: readonly PiTuiPointerSurface[] | null) => void;
115
118
  readonly mode?: "regular" | "fullscreen";
116
119
  readonly layoutRoot?: PiTuiLayoutNode;
117
120
  readonly terminal?: PiTuiTerminalPort;
@@ -1,4 +1,5 @@
1
1
  export * from "./contracts.js";
2
+ export { samePointerSurfaces } from "./overlay-geometry.js";
2
3
  export * from "./damage-aware-terminal.js";
3
4
  export * from "./input-presentation-coordinator.js";
4
5
  export * from "./adapter.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./contracts.js";
2
+ export { samePointerSurfaces } from "./overlay-geometry.js";
2
3
  export * from "./damage-aware-terminal.js";
3
4
  export * from "./input-presentation-coordinator.js";
4
5
  export * from "./adapter.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Keeps mouse reports and adjacent keyboard input in physical delivery order.
3
+ * ProcessTerminal already frames escape/paste sequences; this also handles hosts
4
+ * that batch reports or split an SGR report after its introducer. Paste payloads
5
+ * are opaque and never interpreted as pointer input.
6
+ */
7
+ export declare class MouseReportInput {
8
+ #private;
9
+ readonly deliver: (data: string) => void;
10
+ constructor(deliver: (data: string) => void);
11
+ accept(chunk: string): void;
12
+ reset(): void;
13
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Keeps mouse reports and adjacent keyboard input in physical delivery order.
3
+ * ProcessTerminal already frames escape/paste sequences; this also handles hosts
4
+ * that batch reports or split an SGR report after its introducer. Paste payloads
5
+ * are opaque and never interpreted as pointer input.
6
+ */
7
+ export class MouseReportInput {
8
+ deliver;
9
+ #pending = "";
10
+ #paste = false;
11
+ constructor(deliver) {
12
+ this.deliver = deliver;
13
+ }
14
+ accept(chunk) {
15
+ const data = this.#pending + chunk;
16
+ this.#pending = "";
17
+ if (this.#paste) {
18
+ const end = data.indexOf("\u001b[201~");
19
+ if (end < 0) {
20
+ this.deliver(data);
21
+ return;
22
+ }
23
+ this.#paste = false;
24
+ this.deliver(data.slice(0, end + 6));
25
+ if (end + 6 < data.length)
26
+ this.accept(data.slice(end + 6));
27
+ return;
28
+ }
29
+ const pattern = /\u001b\[200~|\u001b\[<\d+;\d+;\d+[Mm]/g;
30
+ let from = 0;
31
+ for (const match of data.matchAll(pattern)) {
32
+ if (match.index > from)
33
+ this.deliver(data.slice(from, match.index));
34
+ if (match[0] === "\u001b[200~") {
35
+ const end = data.indexOf("\u001b[201~", match.index + 6);
36
+ this.#paste = end < 0;
37
+ this.deliver(data.slice(match.index, end < 0 ? undefined : end + 6));
38
+ if (end >= 0 && end + 6 < data.length)
39
+ this.accept(data.slice(end + 6));
40
+ return;
41
+ }
42
+ this.deliver(match[0]);
43
+ from = match.index + match[0].length;
44
+ }
45
+ const rest = data.slice(from);
46
+ const partial = /\u001b\[<[\d;]*$/.exec(rest);
47
+ if (partial !== null && partial[0].length <= 64) {
48
+ this.#pending = partial[0];
49
+ if (partial.index > 0)
50
+ this.deliver(rest.slice(0, partial.index));
51
+ }
52
+ else if (rest.length > 0)
53
+ this.deliver(rest);
54
+ }
55
+ reset() {
56
+ this.#pending = "";
57
+ this.#paste = false;
58
+ }
59
+ }
@@ -0,0 +1,32 @@
1
+ import { TuiAltScreen, type Component, type OverlayHandle, type OverlayOptions } from "#pi-tui";
2
+ import type { PresentationComponentPort, PresentationPointerSurface } from "../../../contracts/presentation/index.js";
3
+ import type { PiTuiOverlayOptions } from "./contracts.js";
4
+ /** Observes public component renders in compositor order, without accessing renderer state. */
5
+ export declare class OverlayGeometryTracker {
6
+ #private;
7
+ readonly publish: (surfaces: readonly PresentationPointerSurface[] | null) => void;
8
+ readonly ports: WeakMap<Component, PresentationComponentPort>;
9
+ constructor(publish: (surfaces: readonly PresentationPointerSurface[] | null) => void);
10
+ invalidate(): void;
11
+ begin(columns: number, rows: number): void;
12
+ record(component: PresentationComponentPort, options: PiTuiOverlayOptions | undefined, width: number, height: number): void;
13
+ end(): void;
14
+ }
15
+ /** The protected composition hook brackets the actual visible, focus-ordered overlay renders. */
16
+ export declare class GeometryObservedAltScreen extends TuiAltScreen {
17
+ #private;
18
+ geometry: OverlayGeometryTracker | undefined;
19
+ get overlayFocused(): boolean;
20
+ setFocus(component: Component | null): void;
21
+ showOverlay(component: Component, options?: OverlayOptions): OverlayHandle;
22
+ hideOverlay(): void;
23
+ protected compositeOverlays(lines: string[], width: number, height: number): string[];
24
+ }
25
+ /** Compare only geometry and ownership, never animation content or transcript position. */
26
+ export declare function samePointerSurfaces(a: readonly PresentationPointerSurface[], b: readonly PresentationPointerSurface[]): boolean;
27
+ /**
28
+ * Resolve the pinned public overlay options against the actual render width/height.
29
+ * The runtime still owns layout and painting. Independent terminal-cell tests certify
30
+ * this option-to-hit-region projection against its compositor, including clipping.
31
+ */
32
+ export declare function overlayPointerSurface(component: PresentationComponentPort, options: PiTuiOverlayOptions | undefined, width: number, renderedHeight: number, columns: number, rows: number): PresentationPointerSurface | undefined;