@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.
- package/README.md +40 -11
- package/dist/composition/owned-ui.d.ts +2 -0
- package/dist/composition/owned-ui.js +20 -3
- package/dist/contracts/owned-ui/index.d.ts +2 -0
- package/dist/contracts/owned-ui/index.js +2 -0
- package/dist/contracts/owned-ui/model.d.ts +38 -3
- package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
- package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
- package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
- package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
- package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
- package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
- package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
- package/dist/contracts/owned-ui/validation.js +45 -2
- package/dist/contracts/presentation/index.d.ts +8 -0
- package/dist/features/owned-ui/settings-app.js +1 -1
- package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
- package/dist/features/prompt-history/image-sidecar.js +130 -0
- package/dist/features/prompt-history/index.d.ts +2 -0
- package/dist/features/prompt-history/index.js +1 -0
- package/dist/features/prompt-history/paths.d.ts +1 -0
- package/dist/features/prompt-history/paths.js +2 -1
- package/dist/features/prompt-history/store.d.ts +1 -1
- package/dist/features/prompt-history/store.js +46 -2
- package/dist/features/prompt-history/worker.js +1 -1
- package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
- package/dist/features/prompt-suggestions/diagnostics.js +82 -0
- package/dist/features/prompt-suggestions/index.d.ts +1 -0
- package/dist/features/prompt-suggestions/index.js +1 -0
- package/dist/foundation/release/bootstrap.d.ts +2 -0
- package/dist/foundation/release/bootstrap.js +54 -18
- package/dist/foundation/release/index.d.ts +1 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/update-launch.d.ts +6 -0
- package/dist/foundation/release/update-launch.js +17 -0
- package/dist/foundation/release/update.d.ts +2 -0
- package/dist/foundation/release/update.js +20 -21
- package/dist/foundation/supervision/main.js +5 -2
- package/dist/foundation/supervision/server.js +40 -13
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
- package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
- package/dist/integrations/pi/components/prompt-input-port.js +1 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
- package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
- package/dist/integrations/pi/components/shell-footer-status.js +9 -5
- package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
- package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
- package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
- package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
- package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
- package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
- package/dist/integrations/pi/engine/adapter.d.ts +2 -1
- package/dist/integrations/pi/engine/adapter.js +181 -84
- package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
- package/dist/integrations/pi/engine/tool-rendering.js +122 -0
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
- package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
- package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
- package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
- package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
- package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
- package/dist/integrations/pi/session-ui/session-shell.js +51 -31
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
- package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
- package/dist/integrations/pi/tui-runtime/index.js +1 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
- package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
- package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +1 -0
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/line-input.d.ts +2 -3
- package/dist/ui/components/line-input.js +4 -20
- package/dist/ui/components/mouse.d.ts +1 -1
- package/dist/ui/components/mouse.js +4 -3
- package/dist/ui/components/prompt-input.d.ts +32 -0
- package/dist/ui/components/prompt-input.js +54 -0
- package/docs/architecture/boundaries.md +1 -0
- package/docs/architecture/history-editor-provenance.md +5 -1
- package/docs/architecture/project-structure.md +1 -0
- package/docs/architecture/prompt-suggestions.md +68 -0
- package/docs/architecture/toolchain.md +1 -1
- package/docs/architecture/ui-reference-provenance.md +23 -0
- package/docs/ci-release-runbook.md +45 -7
- package/docs/features/modal-content-interaction.md +37 -0
- 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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
684
|
+
const deliver = (data) => {
|
|
666
685
|
const routed = route(data);
|
|
667
686
|
if (routed.length > 0)
|
|
668
687
|
onInput(routed);
|
|
669
|
-
}
|
|
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;
|
|
@@ -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;
|