@timurproko/a1 0.1.8-dev.139 → 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 (51) hide show
  1. package/dist/composition/owned-ui.js +10 -7
  2. package/dist/composition/settings-route-host.d.ts +1 -1
  3. package/dist/composition/settings-route-host.js +1 -1
  4. package/dist/contracts/owned-ui/model.d.ts +8 -1
  5. package/dist/contracts/owned-ui/validation.js +11 -0
  6. package/dist/features/owned-ui/settings-app.js +2 -1
  7. package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
  8. package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
  9. package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
  10. package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
  11. package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
  12. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
  13. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
  14. package/dist/integrations/pi/engine/adapter.js +5 -1
  15. package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
  16. package/dist/integrations/pi/engine/session-integration.js +14 -3
  17. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
  18. package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
  19. package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
  20. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +17 -3
  21. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +100 -260
  22. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
  23. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +52 -12
  24. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
  25. package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
  26. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
  27. package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
  28. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -6
  29. package/dist/native/darwin-arm64/manifest.json +1 -1
  30. package/dist/native/linux-x64/manifest.json +3 -3
  31. package/dist/native/linux-x64/process-guardian +0 -0
  32. package/dist/native/win32-x64/manifest.json +3 -3
  33. package/dist/native/win32-x64/process-guardian.exe +0 -0
  34. package/dist/ui/apps/contracts.d.ts +23 -1
  35. package/dist/ui/apps/host.js +9 -1
  36. package/dist/ui/components/scrollbar.d.ts +2 -2
  37. package/dist/ui/components/scrollbar.js +3 -3
  38. package/dist/ui/components/spans.d.ts +16 -1
  39. package/dist/ui/components/spans.js +144 -2
  40. package/dist/ui/components/transcript-viewport.d.ts +6 -0
  41. package/dist/ui/components/transcript-viewport.js +36 -8
  42. package/dist/ui/settings/declarations.d.ts +1 -1
  43. package/dist/ui/settings/declarations.js +3 -3
  44. package/dist/ui/settings/migrations.js +9 -0
  45. package/docs/architecture/boundaries.md +1 -1
  46. package/docs/architecture/project-structure.md +1 -1
  47. package/package.json +4 -1
  48. package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
  49. /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
  50. /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
  51. /package/dist/integrations/pi/{owned-ui → session-ui}/route-host.js +0 -0
@@ -0,0 +1,39 @@
1
+ import type { OwnedUiViewportSettings } from "../../../contracts/owned-ui/index.js";
2
+ import { type TranscriptViewportFrame, type TranscriptViewportFrameInput } from "../../../ui/components/index.js";
3
+ import type { PiShellEditorPort } from "../components/index.js";
4
+ export interface SessionViewportControllerOptions {
5
+ readonly enabled: boolean;
6
+ readonly editor: PiShellEditorPort;
7
+ readonly requestRender: (force?: boolean) => void;
8
+ /** Whether mutable editor URL chips need an immediate forced repaint on deletion. */
9
+ readonly hasEditorLinks?: () => boolean;
10
+ }
11
+ export interface SessionViewportInputResult {
12
+ readonly data: string;
13
+ readonly consumed: boolean;
14
+ readonly copyText?: string;
15
+ }
16
+ /**
17
+ * Owns the stateful interaction policy of the bare-A1 session viewport. The
18
+ * shell root supplies semantic rows and dock geometry; this controller owns
19
+ * follow state, selection, pointer latches, timers, and input routing.
20
+ */
21
+ export declare class SessionViewportController {
22
+ #private;
23
+ constructor(options: SessionViewportControllerOptions);
24
+ get config(): OwnedUiViewportSettings;
25
+ get editorPointerSelecting(): boolean;
26
+ get transcriptPointerSelecting(): boolean;
27
+ setEditorPointerFrame(frame: {
28
+ readonly rowStart: number;
29
+ readonly rowEnd: number;
30
+ } | undefined): void;
31
+ compose(input: TranscriptViewportFrameInput): TranscriptViewportFrame;
32
+ /** Returns true when document wrapping may have changed. */
33
+ setConfig(config: OwnedUiViewportSettings): boolean;
34
+ resumeFollowing(): void;
35
+ noteCompletedAssistantMessage(): void;
36
+ reset(): void;
37
+ clearPointerState(): void;
38
+ handlePreInput(data: string, allowWheel?: boolean, now?: number): SessionViewportInputResult;
39
+ }
@@ -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,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;
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-27T09:31:38.156Z",
8
+ "builtAt": "2026-08-28T10:16:08.776Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
@@ -5,11 +5,11 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-27T09:31:34.074Z",
8
+ "builtAt": "2026-08-28T10:16:08.593Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
- "sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
12
- "size": 415696
11
+ "sha256": "ee8a00eaaf79c707459bbbfb52518e9739314967049fbe5fa625f36ce33db9ee",
12
+ "size": 414568
13
13
  },
14
14
  "provenance": {
15
15
  "language": "Rust",
@@ -5,11 +5,11 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-27T09:32:08.840Z",
8
+ "builtAt": "2026-08-28T10:16:41.229Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "41b1b470549052c30109bdbd3187693ce4de9236295d729acc3a1204a2687025",
12
- "size": 172544
11
+ "sha256": "8daa77c737ea8f96eb022fce63a25a4fce3e5d15c409201712a7f36853c8e8e4",
12
+ "size": 177664
13
13
  },
14
14
  "provenance": {
15
15
  "language": "Rust",
@@ -1,4 +1,4 @@
1
- import type { PaneInputResult, PaneMouseEvent, PaneRect, UiTheme } from "../components/index.js";
1
+ import type { PaneInputResult, PaneMouseEvent, PaneRect, RenderCacheContract, UiTheme } from "../components/index.js";
2
2
  export interface AppSize {
3
3
  readonly width: number;
4
4
  readonly height: number;
@@ -27,6 +27,8 @@ export interface AppHostServices {
27
27
  }
28
28
  export interface UiApp {
29
29
  readonly id: string;
30
+ /** Optional declared revisions used by the host's shared frame cache. */
31
+ readonly renderCache?: RenderCacheContract;
30
32
  render(rect: PaneRect, host: AppHostServices): readonly string[];
31
33
  onInput?(data: string, host: AppHostServices): PaneInputResult;
32
34
  onMouse?(event: PaneMouseEvent, host: AppHostServices): PaneInputResult;
@@ -39,4 +41,24 @@ export interface AppRegistration {
39
41
  readonly route: string;
40
42
  create(): UiApp;
41
43
  }
44
+ /** A neutral owned-app surface mounted by a surrounding presentation runtime. */
45
+ export interface UiRouteSurface {
46
+ readonly id: string;
47
+ /** Renders exactly `height` rows of at most `width` columns. */
48
+ render(width: number, height: number): readonly string[];
49
+ /** True when the key was consumed; false leaves it to the surrounding shell. */
50
+ handleInput(data: string): boolean;
51
+ /** Mouse report in surface-local coordinates. True when consumed. */
52
+ handleMouse(event: PaneMouseEvent): boolean;
53
+ isClosed(): boolean;
54
+ close(): void;
55
+ onRenderRequested(listener: () => void): void;
56
+ /** The surface asks to leave A1 entirely, not merely to close itself. */
57
+ onExitRequested(listener: () => void): void;
58
+ }
59
+ /** Route lookup seam shared by owned apps and any presentation runtime hosting them. */
60
+ export interface UiRouteHost {
61
+ claims(route: string): boolean;
62
+ open(route: string): UiRouteSurface | null;
63
+ }
42
64
  export declare function assertAppRegistration(registration: AppRegistration): void;
@@ -16,6 +16,7 @@ export class UiAppHost {
16
16
  #theme;
17
17
  /** When the first interrupt of a chord was seen, or null when disarmed. */
18
18
  #interruptArmedAt = null;
19
+ #renderedInterruptArmed;
19
20
  #cache = new FrameCache();
20
21
  #app = null;
21
22
  constructor(options) {
@@ -58,7 +59,11 @@ export class UiAppHost {
58
59
  }
59
60
  const size = this.#surface.size();
60
61
  const rect = { width: Math.max(0, size.width), height: Math.max(0, size.height) };
61
- const lines = this.#guard(() => finalizeFrame(app.render(rect, this.#services()), rect, app.id));
62
+ const interruptArmed = this.interruptArmed;
63
+ if (this.#renderedInterruptArmed !== interruptArmed)
64
+ this.#cache.invalidate();
65
+ this.#renderedInterruptArmed = interruptArmed;
66
+ const lines = this.#guard(() => this.#cache.render(app, rect, () => finalizeFrame(app.render(rect, this.#services()), rect, app.id)));
62
67
  this.#surface.present(lines ?? null);
63
68
  }
64
69
  handleInput(data) {
@@ -101,6 +106,9 @@ export class UiAppHost {
101
106
  return { consumed: true, render: true };
102
107
  }
103
108
  this.#interruptArmedAt = now;
109
+ // Host-owned state participates in the app's next frame independently of
110
+ // the app's own revision contract.
111
+ this.#cache.invalidate();
104
112
  return { consumed: true, render: true };
105
113
  }
106
114
  get interruptArmed() {
@@ -19,12 +19,12 @@ export interface ScrollbarInput {
19
19
  readonly scroll: number;
20
20
  readonly trackHeight: number;
21
21
  }
22
- export type ScrollbarAppearance = "always" | "hover" | "hidden";
22
+ export type ScrollbarAppearance = "auto" | "always" | "hidden";
23
23
  export type ScrollbarStyle = "thin" | "thick";
24
24
  export type ScrollbarSpeed = "normal" | "fast" | "high";
25
25
  /** Wheel distance: normal (3), fast (6), high as normal + fast (9). */
26
26
  export declare function scrollbarWheelRows(speed: ScrollbarSpeed): number;
27
- /** Selection edge distance at each fixed 30ms tick. */
27
+ /** Selection edge distance uses the same configured rate as a wheel event. */
28
28
  export declare function scrollbarSelectionRows(speed: ScrollbarSpeed): number;
29
29
  export interface ScrollbarPresentationInput {
30
30
  readonly geometry: ScrollbarGeometry | null;