@timurproko/a1 0.1.8-dev.137 → 0.1.8-dev.151

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 (105) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/capabilities.d.ts +2 -11
  3. package/dist/cli/capabilities.js +1 -1
  4. package/dist/cli/dispatch.js +3 -6
  5. package/dist/composition/owned-ui.js +26 -5
  6. package/dist/composition/settings-route-host.d.ts +1 -1
  7. package/dist/composition/settings-route-host.js +1 -1
  8. package/dist/contracts/owned-ui/model.d.ts +28 -0
  9. package/dist/contracts/owned-ui/validation.js +14 -0
  10. package/dist/features/launch/development-launch.js +11 -3
  11. package/dist/features/launch/index.d.ts +0 -1
  12. package/dist/features/launch/index.js +0 -1
  13. package/dist/features/launch/intent.d.ts +3 -2
  14. package/dist/features/launch/intent.js +1 -2
  15. package/dist/features/launch/prepare-launch.d.ts +0 -2
  16. package/dist/features/launch/prepare-launch.js +2 -9
  17. package/dist/features/launch/profile-paths.d.ts +1 -2
  18. package/dist/features/launch/profile-paths.js +2 -8
  19. package/dist/features/launch/runtime-selection.d.ts +2 -5
  20. package/dist/features/owned-ui/settings-app.js +38 -14
  21. package/dist/foundation/lifecycle/model.d.ts +1 -1
  22. package/dist/foundation/lifecycle/model.js +1 -1
  23. package/dist/foundation/release/bootstrap.d.ts +1 -3
  24. package/dist/foundation/release/bootstrap.js +1 -1
  25. package/dist/foundation/storage/control-store.js +37 -3
  26. package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
  27. package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
  28. package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
  29. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  30. package/dist/integrations/pi/components/shell-footer-status.js +14 -5
  31. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -2
  32. package/dist/integrations/pi/components/shell-presenters-transcript.js +9 -8
  33. package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
  34. package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
  35. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +13 -0
  36. package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
  37. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
  38. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
  39. package/dist/integrations/pi/engine/adapter.js +21 -3
  40. package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
  41. package/dist/integrations/pi/engine/session-integration.js +17 -3
  42. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
  43. package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
  44. package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
  45. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +34 -5
  46. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +294 -45
  47. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
  48. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +167 -24
  49. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
  50. package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
  51. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
  52. package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
  53. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
  54. package/dist/integrations/pi/tui-runtime/adapter.js +51 -2
  55. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -6
  56. package/dist/native/darwin-arm64/manifest.json +1 -1
  57. package/dist/native/linux-x64/manifest.json +1 -1
  58. package/dist/native/win32-x64/manifest.json +3 -3
  59. package/dist/native/win32-x64/process-guardian.exe +0 -0
  60. package/dist/product-identity.d.ts +2 -2
  61. package/dist/product-identity.js +3 -4
  62. package/dist/product-identity.json +3 -5
  63. package/dist/ui/apps/contracts.d.ts +23 -1
  64. package/dist/ui/apps/host.js +9 -1
  65. package/dist/ui/components/index.d.ts +3 -0
  66. package/dist/ui/components/index.js +3 -0
  67. package/dist/ui/components/mouse.d.ts +10 -0
  68. package/dist/ui/components/mouse.js +23 -0
  69. package/dist/ui/components/prompt-row.d.ts +14 -0
  70. package/dist/ui/components/prompt-row.js +20 -0
  71. package/dist/ui/components/scrollbar.d.ts +26 -0
  72. package/dist/ui/components/scrollbar.js +35 -0
  73. package/dist/ui/components/spans.d.ts +21 -0
  74. package/dist/ui/components/spans.js +178 -0
  75. package/dist/ui/components/submitted-prompt.d.ts +23 -0
  76. package/dist/ui/components/submitted-prompt.js +50 -0
  77. package/dist/ui/components/text-selection.d.ts +53 -0
  78. package/dist/ui/components/text-selection.js +168 -0
  79. package/dist/ui/components/text.d.ts +14 -0
  80. package/dist/ui/components/text.js +40 -0
  81. package/dist/ui/components/transcript-viewport.d.ts +104 -0
  82. package/dist/ui/components/transcript-viewport.js +381 -0
  83. package/dist/ui/settings/declarations.d.ts +8 -1
  84. package/dist/ui/settings/declarations.js +33 -5
  85. package/dist/ui/settings/migrations.d.ts +1 -1
  86. package/dist/ui/settings/migrations.js +21 -2
  87. package/dist/ui/settings/sections.js +15 -8
  88. package/dist/ui/settings/session.d.ts +2 -0
  89. package/dist/ui/settings/session.js +7 -5
  90. package/docs/architecture/boundaries.md +3 -3
  91. package/docs/architecture/project-structure.md +2 -2
  92. package/docs/architecture/resource-and-data-policy.md +1 -1
  93. package/docs/architecture/toolchain.md +5 -5
  94. package/docs/architecture/ui-reference-provenance.md +11 -2
  95. package/docs/features/launch-profiles.md +7 -14
  96. package/docs/manual-launch-instance-acceptance.md +12 -35
  97. package/docs/manual-owned-ui-checkpoint.md +2 -3
  98. package/docs/manual-pi-boundary-candidate.md +4 -5
  99. package/package.json +4 -2
  100. package/dist/features/launch/profiles.d.ts +0 -12
  101. package/dist/features/launch/profiles.js +0 -26
  102. package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
  103. /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
  104. /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
  105. /package/dist/integrations/pi/{owned-ui → session-ui}/route-host.js +0 -0
@@ -0,0 +1,369 @@
1
+ import { TranscriptViewport, routeMouseInput, scrollForTrackPage, scrollForThumbRow, scrollbarSelectionRows, scrollbarWheelRows, } from "../../../ui/components/index.js";
2
+ /**
3
+ * Owns the stateful interaction policy of the bare-A1 session viewport. The
4
+ * shell root supplies semantic rows and dock geometry; this controller owns
5
+ * follow state, selection, pointer latches, timers, and input routing.
6
+ */
7
+ export class SessionViewportController {
8
+ #enabled;
9
+ #editor;
10
+ #requestRender;
11
+ #hasEditorLinks;
12
+ #viewport = new TranscriptViewport();
13
+ #config = {
14
+ scrollbarAppearance: "auto",
15
+ scrollbarStyle: "thin",
16
+ scrollbarSpeed: "normal",
17
+ };
18
+ #dragGrabOffset = null;
19
+ /** A left-button sequence begun in status/editor/footer chrome is swallowed. */
20
+ #dockPointerSuppressed = false;
21
+ /** True only while the editor owns a held left mouse button. */
22
+ #editorPointerSelecting = false;
23
+ /** Last composed terminal rows occupied by the default prompt editor. */
24
+ #editorPointerFrame;
25
+ #selectionAutoScrollTimer;
26
+ #selectionAutoScrollPointer;
27
+ #activityTimer;
28
+ constructor(options) {
29
+ this.#enabled = options.enabled;
30
+ this.#editor = options.editor;
31
+ this.#requestRender = options.requestRender;
32
+ this.#hasEditorLinks = options.hasEditorLinks ?? (() => false);
33
+ }
34
+ get config() {
35
+ return this.#config;
36
+ }
37
+ get editorPointerSelecting() {
38
+ return this.#editorPointerSelecting;
39
+ }
40
+ get transcriptPointerSelecting() {
41
+ return this.#viewport.selectionActive;
42
+ }
43
+ setEditorPointerFrame(frame) {
44
+ this.#editorPointerFrame = frame;
45
+ }
46
+ compose(input) {
47
+ this.#viewport.setConfig(this.#config);
48
+ return this.#viewport.compose(input);
49
+ }
50
+ /** Returns true when document wrapping may have changed. */
51
+ setConfig(config) {
52
+ const appearanceChanged = config.scrollbarAppearance !== this.#config.scrollbarAppearance;
53
+ this.#config = config;
54
+ this.#viewport.setConfig(config);
55
+ this.#requestRender();
56
+ return appearanceChanged;
57
+ }
58
+ resumeFollowing() {
59
+ if (!this.#enabled)
60
+ return;
61
+ this.#stopSelectionAutoScroll();
62
+ const selectionChanged = this.#viewport.clearSelection();
63
+ const scrolled = this.#viewport.scrollToEnd();
64
+ if (scrolled)
65
+ this.#scheduleActivityExpiry();
66
+ if (scrolled || selectionChanged)
67
+ this.#requestRender();
68
+ }
69
+ noteCompletedAssistantMessage() {
70
+ if (!this.#enabled || !this.#viewport.noteNewMessage())
71
+ return;
72
+ this.#requestRender();
73
+ }
74
+ reset() {
75
+ this.#clearActivityTimer();
76
+ this.#stopSelectionAutoScroll();
77
+ this.#editorPointerSelecting = false;
78
+ this.#editorPointerFrame = undefined;
79
+ this.#viewport.reset();
80
+ }
81
+ clearPointerState() {
82
+ this.#clearActivityTimer();
83
+ this.#dragGrabOffset = null;
84
+ this.#dockPointerSuppressed = false;
85
+ this.#editorPointerSelecting = false;
86
+ this.#editorPointerFrame = undefined;
87
+ this.#stopSelectionAutoScroll();
88
+ this.#viewport.clearSelection();
89
+ this.#viewport.clearTransient();
90
+ }
91
+ handlePreInput(data, allowWheel = true, now = Date.now()) {
92
+ if (!this.#enabled)
93
+ return { data, consumed: false };
94
+ // Pi components share one keybinding manager. Restore bare A1's aliases
95
+ // before the focused vanilla editor handles this input.
96
+ this.#editor.activateKeybindings();
97
+ // Handle the physical paste chord at the pre-input boundary. Windows
98
+ // terminals vary between forwarding Ctrl+V and terminal-owned bracketed paste.
99
+ if (this.#editor.matchesTerminalKey(data, "ctrl+v") && this.#editor.pasteClipboard()) {
100
+ return { data: "", consumed: true };
101
+ }
102
+ // URL chip deletion must overwrite terminal link cells in the same frame.
103
+ if (EDITOR_LINK_DELETION_INPUTS.has(data) && this.#hasEditorLinks())
104
+ this.#requestRender(true);
105
+ if (this.#viewport.frame === null)
106
+ return { data, consumed: false };
107
+ if (data === "\u0003") {
108
+ if (this.#editor.hasSelection()) {
109
+ if (this.#viewport.clearSelection())
110
+ this.#requestRender();
111
+ }
112
+ else {
113
+ const copyText = this.#viewport.selectedText();
114
+ if (copyText !== null && copyText.length > 0) {
115
+ this.#viewport.clearSelection();
116
+ this.#requestRender();
117
+ return { data: "", consumed: true, copyText };
118
+ }
119
+ }
120
+ }
121
+ if (allowWheel && this.#editor.matchesTerminalKey(data, "home")) {
122
+ if (this.#viewport.scrollTo(0, now)) {
123
+ this.#scheduleActivityExpiry();
124
+ this.#requestRender();
125
+ }
126
+ return { data: "", consumed: true };
127
+ }
128
+ if (allowWheel && this.#editor.matchesTerminalKey(data, "end")) {
129
+ if (!this.#viewport.followingEnd) {
130
+ this.#viewport.scrollToEnd(now);
131
+ this.#scheduleActivityExpiry();
132
+ this.#requestRender();
133
+ }
134
+ return { data: "", consumed: true };
135
+ }
136
+ if (allowWheel && (SHIFT_UP_INPUTS.has(data) || SHIFT_DOWN_INPUTS.has(data))) {
137
+ const scrolled = SHIFT_UP_INPUTS.has(data)
138
+ ? this.#viewport.scrollToPreviousPrompt(now)
139
+ : this.#viewport.scrollToNextPrompt(now);
140
+ if (scrolled) {
141
+ this.#scheduleActivityExpiry();
142
+ this.#requestRender();
143
+ }
144
+ return { data: "", consumed: true };
145
+ }
146
+ if (!data.includes("\u001b[<") && data !== "\u0003" && this.#viewport.clearSelection()) {
147
+ this.#requestRender();
148
+ }
149
+ const frame = this.#viewport.frame;
150
+ let repaint = false;
151
+ let forceRepaint = false;
152
+ let activity = false;
153
+ const routed = routeMouseInput(data, event => {
154
+ const hits = frame.hits;
155
+ const overRail = hits.rail !== null
156
+ && event.column === hits.rail.column
157
+ && event.row >= hits.rail.rowStart
158
+ && event.row < hits.rail.rowStart + hits.rail.trackHeight;
159
+ const overSticky = hits.sticky !== null && event.row === hits.sticky.row && event.column <= hits.sticky.width;
160
+ const overBottom = hits.bottom !== null && event.row === hits.bottom.row
161
+ && event.column >= hits.bottom.columnStart && event.column <= hits.bottom.columnEnd;
162
+ if (event.kind === "motion") {
163
+ this.#viewport.setRailHovered(overRail);
164
+ this.#viewport.setStickyHovered(overSticky);
165
+ this.#viewport.setBottomHovered(overBottom);
166
+ repaint = true;
167
+ if (this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
168
+ this.#editor.handlePointer({
169
+ kind: "motion",
170
+ button: event.button,
171
+ column: event.column,
172
+ row: event.row - this.#editorPointerFrame.rowStart + 1,
173
+ });
174
+ return true;
175
+ }
176
+ if (this.#dockPointerSuppressed)
177
+ return true;
178
+ if (this.#viewport.selectionActive) {
179
+ this.#viewport.extendSelection(event.column, event.row, now, false);
180
+ this.#updateSelectionAutoScroll(event.column, event.row, hits.viewportHeight);
181
+ activity = true;
182
+ return true;
183
+ }
184
+ if (this.#dragGrabOffset !== null && hits.rail !== null) {
185
+ const target = scrollForThumbRow(hits.rail.geometry, event.row - hits.rail.rowStart - this.#dragGrabOffset);
186
+ this.#viewport.scrollTo(target, now);
187
+ activity = true;
188
+ return true;
189
+ }
190
+ return overRail || overSticky || overBottom;
191
+ }
192
+ if (event.kind === "wheel-up" || event.kind === "wheel-down") {
193
+ if (!allowWheel || event.row < 1 || event.row > hits.viewportHeight)
194
+ return false;
195
+ const distance = scrollbarWheelRows(this.#config.scrollbarSpeed);
196
+ this.#viewport.scrollBy(event.kind === "wheel-up" ? -distance : distance, now);
197
+ activity = true;
198
+ repaint = true;
199
+ forceRepaint = true;
200
+ return true;
201
+ }
202
+ if (event.kind === "press") {
203
+ const editorFrame = this.#editorPointerFrame;
204
+ if (event.button === 2 && editorFrame !== undefined
205
+ && event.row >= editorFrame.rowStart && event.row <= editorFrame.rowEnd) {
206
+ this.#stopSelectionAutoScroll();
207
+ if (this.#viewport.clearSelection())
208
+ repaint = true;
209
+ this.#editor.pasteClipboard();
210
+ this.#dockPointerSuppressed = true;
211
+ repaint = true;
212
+ return true;
213
+ }
214
+ if (event.button !== 0)
215
+ return false;
216
+ this.#stopSelectionAutoScroll();
217
+ if (this.#viewport.clearSelection())
218
+ repaint = true;
219
+ if (overBottom) {
220
+ this.#viewport.scrollToEnd(now);
221
+ activity = true;
222
+ repaint = true;
223
+ return true;
224
+ }
225
+ if (overSticky && hits.sticky !== null) {
226
+ this.#viewport.scrollTo(hits.sticky.target, now);
227
+ activity = true;
228
+ repaint = true;
229
+ return true;
230
+ }
231
+ if (overRail && hits.rail !== null) {
232
+ const trackRow = event.row - hits.rail.rowStart;
233
+ const geometry = hits.rail.geometry;
234
+ if (trackRow >= geometry.thumbTop && trackRow < geometry.thumbTop + geometry.thumbHeight) {
235
+ this.#dragGrabOffset = trackRow - geometry.thumbTop;
236
+ this.#viewport.setRailDragging(true);
237
+ }
238
+ else {
239
+ this.#viewport.scrollTo(scrollForTrackPage(geometry, trackRow, frame.scrollTop, hits.viewportHeight), now);
240
+ }
241
+ activity = true;
242
+ repaint = true;
243
+ return true;
244
+ }
245
+ if (editorFrame !== undefined && event.row >= editorFrame.rowStart && event.row <= editorFrame.rowEnd) {
246
+ const handled = this.#editor.handlePointer({
247
+ kind: "press",
248
+ button: event.button,
249
+ column: event.column,
250
+ row: event.row - editorFrame.rowStart + 1,
251
+ });
252
+ if (!handled)
253
+ this.#dockPointerSuppressed = true;
254
+ this.#editorPointerSelecting = handled;
255
+ repaint = true;
256
+ return true;
257
+ }
258
+ if (event.row > hits.viewportHeight) {
259
+ this.#dockPointerSuppressed = true;
260
+ return true;
261
+ }
262
+ if (event.row >= 1 && event.row <= hits.viewportHeight && event.column <= frame.contentWidth) {
263
+ this.#viewport.pressSelection(event.column, event.row, now);
264
+ repaint = true;
265
+ return true;
266
+ }
267
+ return false;
268
+ }
269
+ if (event.kind === "release") {
270
+ if (this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
271
+ const wasEditorSelecting = this.#editorPointerSelecting;
272
+ this.#editor.handlePointer({
273
+ kind: "release",
274
+ button: event.button,
275
+ column: event.column,
276
+ row: event.row - this.#editorPointerFrame.rowStart + 1,
277
+ });
278
+ this.#editorPointerSelecting = false;
279
+ forceRepaint ||= wasEditorSelecting;
280
+ repaint = true;
281
+ return true;
282
+ }
283
+ if (this.#viewport.releaseSelection()) {
284
+ this.#stopSelectionAutoScroll();
285
+ // Restore OSC 8 links only after the held-button selection paint has
286
+ // ended, then overwrite any terminal-cached hover cells immediately.
287
+ forceRepaint = true;
288
+ repaint = true;
289
+ return true;
290
+ }
291
+ if (this.#dragGrabOffset !== null) {
292
+ this.#dragGrabOffset = null;
293
+ this.#viewport.setRailDragging(false);
294
+ repaint = true;
295
+ return true;
296
+ }
297
+ if (this.#dockPointerSuppressed) {
298
+ this.#dockPointerSuppressed = false;
299
+ return true;
300
+ }
301
+ }
302
+ return false;
303
+ });
304
+ if (activity)
305
+ this.#scheduleActivityExpiry();
306
+ if (repaint)
307
+ this.#requestRender(forceRepaint);
308
+ return routed;
309
+ }
310
+ #updateSelectionAutoScroll(column, row, viewportHeight) {
311
+ const beyondEdge = row <= 1 || row > viewportHeight;
312
+ if (!beyondEdge) {
313
+ this.#stopSelectionAutoScroll();
314
+ return;
315
+ }
316
+ this.#selectionAutoScrollPointer = { column, row };
317
+ if (this.#selectionAutoScrollTimer !== undefined)
318
+ return;
319
+ this.#selectionAutoScrollTimer = setTimeout(() => this.#selectionAutoScrollTick(), SELECTION_AUTO_SCROLL_INTERVAL_MS);
320
+ this.#selectionAutoScrollTimer.unref?.();
321
+ }
322
+ #selectionAutoScrollTick() {
323
+ this.#selectionAutoScrollTimer = undefined;
324
+ const pointer = this.#selectionAutoScrollPointer;
325
+ if (pointer === undefined || !this.#viewport.selectionActive) {
326
+ this.#stopSelectionAutoScroll();
327
+ return;
328
+ }
329
+ const before = this.#viewport.scrollTop;
330
+ const rowsPerTick = scrollbarSelectionRows(this.#config.scrollbarSpeed);
331
+ for (let row = 0; row < rowsPerTick; row += 1) {
332
+ const previous = this.#viewport.scrollTop;
333
+ this.#viewport.extendSelection(pointer.column, pointer.row);
334
+ if (this.#viewport.scrollTop === previous)
335
+ break;
336
+ }
337
+ if (this.#viewport.scrollTop === before) {
338
+ this.#stopSelectionAutoScroll();
339
+ return;
340
+ }
341
+ this.#requestRender();
342
+ this.#scheduleActivityExpiry();
343
+ this.#selectionAutoScrollTimer = setTimeout(() => this.#selectionAutoScrollTick(), SELECTION_AUTO_SCROLL_INTERVAL_MS);
344
+ this.#selectionAutoScrollTimer.unref?.();
345
+ }
346
+ #stopSelectionAutoScroll() {
347
+ if (this.#selectionAutoScrollTimer !== undefined)
348
+ clearTimeout(this.#selectionAutoScrollTimer);
349
+ this.#selectionAutoScrollTimer = undefined;
350
+ this.#selectionAutoScrollPointer = undefined;
351
+ }
352
+ #clearActivityTimer() {
353
+ if (this.#activityTimer !== undefined)
354
+ clearTimeout(this.#activityTimer);
355
+ this.#activityTimer = undefined;
356
+ }
357
+ #scheduleActivityExpiry() {
358
+ this.#clearActivityTimer();
359
+ this.#activityTimer = setTimeout(() => {
360
+ this.#activityTimer = undefined;
361
+ this.#requestRender();
362
+ }, 925);
363
+ this.#activityTimer.unref?.();
364
+ }
365
+ }
366
+ const SELECTION_AUTO_SCROLL_INTERVAL_MS = 30;
367
+ const EDITOR_LINK_DELETION_INPUTS = new Set(["\b", "\u007f", "\u001b[3~"]);
368
+ const SHIFT_UP_INPUTS = new Set(["\u001b[1;2A"]);
369
+ const SHIFT_DOWN_INPUTS = new Set(["\u001b[1;2B"]);
@@ -0,0 +1,11 @@
1
+ import type { PiShellClipboardContent } from "../components/index.js";
2
+ /** Load the native adapter during shell startup so the first paste is warm. */
3
+ export declare function preloadSystemClipboard(): void;
4
+ /**
5
+ * Reads plain text through platform clipboard commands without importing Pi's
6
+ * private clipboard module. Failures are deliberately non-fatal: a denied or
7
+ * unavailable clipboard simply makes the paste action a no-op.
8
+ */
9
+ export declare function readSystemClipboardContent(): Promise<PiShellClipboardContent | null>;
10
+ export declare function readSystemClipboardText(): Promise<string | null>;
11
+ export declare function writeSystemClipboardText(text: string): Promise<void>;
@@ -0,0 +1,101 @@
1
+ import { execFile } from "node:child_process";
2
+ const MAX_CLIPBOARD_BYTES = 16 * 1024 * 1024;
3
+ let nativeClipboardPromise;
4
+ let pendingClipboardWrite = Promise.resolve();
5
+ /** Load the native adapter during shell startup so the first paste is warm. */
6
+ export function preloadSystemClipboard() {
7
+ if (process.platform === "win32" || process.platform === "darwin")
8
+ void nativeClipboard();
9
+ }
10
+ /**
11
+ * Reads plain text through platform clipboard commands without importing Pi's
12
+ * private clipboard module. Failures are deliberately non-fatal: a denied or
13
+ * unavailable clipboard simply makes the paste action a no-op.
14
+ */
15
+ export async function readSystemClipboardContent() {
16
+ await pendingClipboardWrite;
17
+ const native = await nativeClipboard();
18
+ if (native !== null) {
19
+ try {
20
+ if (process.platform === "win32" && native.availableFormats().some(isFileDropFormat)) {
21
+ const files = await readSystemClipboardText();
22
+ if (files !== null)
23
+ return { kind: "text", text: files };
24
+ }
25
+ if (native.hasImage()) {
26
+ const data = await native.getImageBase64();
27
+ if (data.length > 0)
28
+ return { kind: "image", data, mimeType: "image/png" };
29
+ }
30
+ }
31
+ catch {
32
+ // Fall through to text and platform readers.
33
+ }
34
+ }
35
+ const text = await readSystemClipboardText();
36
+ return text === null ? null : { kind: "text", text };
37
+ }
38
+ export async function readSystemClipboardText() {
39
+ // Ctrl+C/Ctrl+X and Ctrl+V can arrive in adjacent input turns. Serialize the
40
+ // read behind our native write so paste never observes the previous value.
41
+ await pendingClipboardWrite;
42
+ if (process.platform === "win32" || process.platform === "darwin") {
43
+ const native = await nativeClipboard();
44
+ if (native !== null) {
45
+ try {
46
+ const text = await native.getText();
47
+ if (text.length > 0)
48
+ return text;
49
+ }
50
+ catch {
51
+ // Fall through to the platform command.
52
+ }
53
+ }
54
+ }
55
+ if (process.platform === "win32") {
56
+ const script = [
57
+ "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8",
58
+ "$t=Get-Clipboard -Raw -ErrorAction SilentlyContinue",
59
+ "if ($null -ne $t -and $t.Length -gt 0) {[Console]::Out.Write($t)} else {$f=Get-Clipboard -Format FileDropList -ErrorAction SilentlyContinue; if ($f) {[Console]::Out.Write(($f | ForEach-Object {$_.FullName}) -join [Environment]::NewLine)}}",
60
+ ].join("; ");
61
+ return execClipboard("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script]);
62
+ }
63
+ if (process.platform === "darwin")
64
+ return execClipboard("pbpaste", []);
65
+ return (await execClipboard("wl-paste", ["--no-newline", "--type", "text"]))
66
+ ?? execClipboard("xclip", ["-selection", "clipboard", "-o"]);
67
+ }
68
+ export function writeSystemClipboardText(text) {
69
+ const write = async () => {
70
+ const native = await nativeClipboard();
71
+ if (native !== null) {
72
+ try {
73
+ await native.setText(text);
74
+ return;
75
+ }
76
+ catch {
77
+ // The caller still emits its terminal clipboard fallback.
78
+ }
79
+ }
80
+ };
81
+ const operation = pendingClipboardWrite.then(write, write);
82
+ pendingClipboardWrite = operation.catch(() => { });
83
+ return operation;
84
+ }
85
+ function nativeClipboard() {
86
+ nativeClipboardPromise ??= import("@mariozechner/clipboard").catch(() => null);
87
+ return nativeClipboardPromise;
88
+ }
89
+ function isFileDropFormat(format) {
90
+ return /(?:filedrop|hdrop|shell idlist)/iu.test(format);
91
+ }
92
+ function execClipboard(command, args) {
93
+ return new Promise(resolve => {
94
+ execFile(command, args, {
95
+ encoding: "utf8",
96
+ maxBuffer: MAX_CLIPBOARD_BYTES,
97
+ timeout: 5_000,
98
+ windowsHide: true,
99
+ }, (error, stdout) => resolve(error || stdout.length === 0 ? null : stdout));
100
+ });
101
+ }
@@ -1,4 +1,4 @@
1
- import type { PiTuiComponentPort, PiTuiInputListener, PiTuiOverlayHandle, PiTuiOverlayOptions, PiTuiRuntimeAdapterOptions, PiTuiRuntimeState, PiTuiScrollState, PiTuiStopOptions, PiTuiViewport } from "./contracts.js";
1
+ import type { PiTuiComponentPort, PiTuiInputListener, PiTuiOverlayHandle, PiTuiOverlayOptions, PiTuiPreInputListener, PiTuiRuntimeAdapterOptions, PiTuiRuntimeState, PiTuiScrollState, PiTuiStopOptions, PiTuiViewport } from "./contracts.js";
2
2
  export type PiTuiRuntimeErrorStage = "construction" | "start" | "input-drain" | "restoration";
3
3
  export declare class PiTuiRuntimeError extends Error {
4
4
  readonly stage: PiTuiRuntimeErrorStage;
@@ -26,6 +26,7 @@ export declare class PiTuiRuntimeAdapter {
26
26
  * the transparent and pinned paths never call it.
27
27
  */
28
28
  writeControl(data: string): void;
29
+ addPreInputListener(listener: PiTuiPreInputListener): () => void;
29
30
  addInputListener(listener: PiTuiInputListener): () => void;
30
31
  switchMode(mode: "regular" | "fullscreen"): boolean;
31
32
  setHardwareCursor(enabled: boolean): void;
@@ -81,6 +81,7 @@ class OverlayHandleBridge {
81
81
  }
82
82
  export class PiTuiRuntimeAdapter {
83
83
  #terminal;
84
+ #tuiTerminal;
84
85
  #tui;
85
86
  #root;
86
87
  #layoutRoot;
@@ -92,12 +93,14 @@ export class PiTuiRuntimeAdapter {
92
93
  #scrollViews = new Map();
93
94
  #overlayDisposers = new Set();
94
95
  #inputListeners = new Map();
96
+ #preInputListeners = new Set();
95
97
  #state = "idle";
96
98
  #stopPromise;
97
99
  #rootDisposed = false;
98
100
  constructor(options) {
99
101
  this.#root = options.root;
100
102
  this.#terminal = options.terminal ?? new ProcessTerminal();
103
+ this.#tuiTerminal = preInputTerminal(this.#terminal, data => this.#routePreInput(data));
101
104
  this.#layoutRoot = options.layoutRoot;
102
105
  this.#logDirectory = options.logDirectory;
103
106
  this.#rootBridge = new ComponentBridge(options.root);
@@ -239,6 +242,12 @@ export class PiTuiRuntimeAdapter {
239
242
  this.#assertRunning("control sequence");
240
243
  this.#terminal.write(data);
241
244
  }
245
+ addPreInputListener(listener) {
246
+ if (this.#preInputListeners.has(listener))
247
+ throw new TypeError("Pi TUI pre-input listener is already registered");
248
+ this.#preInputListeners.add(listener);
249
+ return () => this.#preInputListeners.delete(listener);
250
+ }
242
251
  addInputListener(listener) {
243
252
  this.#assertRunning("input listener");
244
253
  if (this.#inputListeners.has(listener))
@@ -313,6 +322,7 @@ export class PiTuiRuntimeAdapter {
313
322
  return this.#stopPromise;
314
323
  if (this.#state === "idle") {
315
324
  this.#state = "stopped";
325
+ this.#preInputListeners.clear();
316
326
  this.#disposeRoot();
317
327
  return Promise.resolve();
318
328
  }
@@ -352,8 +362,21 @@ export class PiTuiRuntimeAdapter {
352
362
  }
353
363
  #createTui(mode, hardwareCursor) {
354
364
  return mode === "fullscreen"
355
- ? new TuiAltScreen(this.#terminal, hardwareCursor, this.#logDirectory, this.#tuiOptions)
356
- : new TuiMainScreen(this.#terminal, hardwareCursor, this.#logDirectory);
365
+ ? new TuiAltScreen(this.#tuiTerminal, hardwareCursor, this.#logDirectory, this.#tuiOptions)
366
+ : new TuiMainScreen(this.#tuiTerminal, hardwareCursor, this.#logDirectory);
367
+ }
368
+ #routePreInput(data) {
369
+ let current = data;
370
+ for (const listener of this.#preInputListeners) {
371
+ if (current.length === 0)
372
+ break;
373
+ const result = listener(current);
374
+ if (result?.data !== undefined)
375
+ current = result.data;
376
+ else if (result?.consume === true)
377
+ current = "";
378
+ }
379
+ return current;
357
380
  }
358
381
  #mountTui(tui) {
359
382
  tui.addChild(this.#rootBridge);
@@ -458,11 +481,37 @@ export class PiTuiRuntimeAdapter {
458
481
  for (const remove of this.#inputListeners.values())
459
482
  remove();
460
483
  this.#inputListeners.clear();
484
+ this.#preInputListeners.clear();
461
485
  for (const dispose of [...this.#overlayDisposers])
462
486
  dispose();
463
487
  this.#disposeRoot();
464
488
  }
465
489
  }
490
+ function preInputTerminal(terminal, route) {
491
+ return {
492
+ get columns() { return terminal.columns; },
493
+ get rows() { return terminal.rows; },
494
+ get kittyProtocolActive() { return terminal.kittyProtocolActive; },
495
+ start(onInput, onResize) {
496
+ terminal.start(data => {
497
+ const routed = route(data);
498
+ if (routed.length > 0)
499
+ onInput(routed);
500
+ }, onResize);
501
+ },
502
+ stop: () => terminal.stop(),
503
+ drainInput: (maxMs, idleMs) => terminal.drainInput(maxMs, idleMs),
504
+ write: data => terminal.write(data),
505
+ moveBy: lines => terminal.moveBy(lines),
506
+ hideCursor: () => terminal.hideCursor(),
507
+ showCursor: () => terminal.showCursor(),
508
+ clearLine: () => terminal.clearLine(),
509
+ clearFromCursor: () => terminal.clearFromCursor(),
510
+ clearScreen: () => terminal.clearScreen(),
511
+ setTitle: title => terminal.setTitle(title),
512
+ setProgress: active => terminal.setProgress(active),
513
+ };
514
+ }
466
515
  function toOverlayOptions(options) {
467
516
  if (options === undefined)
468
517
  return undefined;
@@ -1,3 +1,4 @@
1
+ import type { PresentationComponentPort } from "../../../contracts/presentation/index.js";
1
2
  export type PiTuiRuntimeState = "idle" | "running" | "stopping" | "stopped" | "failed";
2
3
  export interface PiTuiViewport {
3
4
  readonly columns: number;
@@ -9,13 +10,8 @@ export interface PiTuiScrollState {
9
10
  readonly followingEnd: boolean;
10
11
  readonly scrollbarVisible: boolean;
11
12
  }
12
- export interface PiTuiComponentPort {
13
- render(width: number): readonly string[];
14
- invalidate(): void;
15
- handleInput?(data: string): void;
13
+ export interface PiTuiComponentPort extends PresentationComponentPort {
16
14
  readonly wantsKeyRelease?: boolean;
17
- setFocused?(focused: boolean): void;
18
- dispose?(): void;
19
15
  }
20
16
  export interface PiTuiTerminalPort {
21
17
  start(onInput: (data: string) => void, onResize: () => void): void;
@@ -71,6 +67,8 @@ export interface PiTuiInputListenerResult {
71
67
  readonly data?: string;
72
68
  }
73
69
  export type PiTuiInputListener = (data: string) => PiTuiInputListenerResult | undefined;
70
+ /** Runs at the terminal bridge before Pi TUI receives physical input. */
71
+ export type PiTuiPreInputListener = PiTuiInputListener;
74
72
  export interface PiTuiLayoutEntry {
75
73
  readonly node: PiTuiLayoutNode;
76
74
  readonly basis?: number | "auto";
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-26T06:22:58.678Z",
8
+ "builtAt": "2026-08-28T10:16:08.776Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-26T06:22:53.598Z",
8
+ "builtAt": "2026-08-28T10:16:08.593Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "ee8a00eaaf79c707459bbbfb52518e9739314967049fbe5fa625f36ce33db9ee",
@@ -5,11 +5,11 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-26T06:23:46.974Z",
8
+ "builtAt": "2026-08-28T10:16:41.229Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "7b6350b01d875ee3d0db81a1ed5126df5e4a796b93c9ea146e9f24d1f1ab5a2e",
12
- "size": 172544
11
+ "sha256": "8daa77c737ea8f96eb022fce63a25a4fce3e5d15c409201712a7f36853c8e8e4",
12
+ "size": 177664
13
13
  },
14
14
  "provenance": {
15
15
  "language": "Rust",