@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.
- package/dist/composition/owned-ui.js +10 -7
- package/dist/composition/settings-route-host.d.ts +1 -1
- package/dist/composition/settings-route-host.js +1 -1
- package/dist/contracts/owned-ui/model.d.ts +8 -1
- package/dist/contracts/owned-ui/validation.js +11 -0
- package/dist/features/owned-ui/settings-app.js +2 -1
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
- package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
- package/dist/integrations/pi/engine/adapter.js +5 -1
- package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
- package/dist/integrations/pi/engine/session-integration.js +14 -3
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
- package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +17 -3
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +100 -260
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +52 -12
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
- package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
- package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -6
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +3 -3
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/manifest.json +3 -3
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/apps/contracts.d.ts +23 -1
- package/dist/ui/apps/host.js +9 -1
- package/dist/ui/components/scrollbar.d.ts +2 -2
- package/dist/ui/components/scrollbar.js +3 -3
- package/dist/ui/components/spans.d.ts +16 -1
- package/dist/ui/components/spans.js +144 -2
- package/dist/ui/components/transcript-viewport.d.ts +6 -0
- package/dist/ui/components/transcript-viewport.js +36 -8
- package/dist/ui/settings/declarations.d.ts +1 -1
- package/dist/ui/settings/declarations.js +3 -3
- package/dist/ui/settings/migrations.js +9 -0
- package/docs/architecture/boundaries.md +1 -1
- package/docs/architecture/project-structure.md +1 -1
- package/package.json +4 -1
- package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
- /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
- /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
- /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-
|
|
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-
|
|
8
|
+
"builtAt": "2026-08-28T10:16:08.593Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
|
-
"sha256": "
|
|
12
|
-
"size":
|
|
11
|
+
"sha256": "ee8a00eaaf79c707459bbbfb52518e9739314967049fbe5fa625f36ce33db9ee",
|
|
12
|
+
"size": 414568
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
15
15
|
"language": "Rust",
|
|
Binary file
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-28T10:16:41.229Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
12
|
-
"size":
|
|
11
|
+
"sha256": "8daa77c737ea8f96eb022fce63a25a4fce3e5d15c409201712a7f36853c8e8e4",
|
|
12
|
+
"size": 177664
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
15
15
|
"language": "Rust",
|
|
Binary file
|
|
@@ -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;
|
package/dist/ui/apps/host.js
CHANGED
|
@@ -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
|
|
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 = "
|
|
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
|
|
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;
|