@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.
- package/README.md +2 -4
- package/dist/cli/capabilities.d.ts +2 -11
- package/dist/cli/capabilities.js +1 -1
- package/dist/cli/dispatch.js +3 -6
- package/dist/composition/owned-ui.js +26 -5
- 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 +28 -0
- package/dist/contracts/owned-ui/validation.js +14 -0
- package/dist/features/launch/development-launch.js +11 -3
- package/dist/features/launch/index.d.ts +0 -1
- package/dist/features/launch/index.js +0 -1
- package/dist/features/launch/intent.d.ts +3 -2
- package/dist/features/launch/intent.js +1 -2
- package/dist/features/launch/prepare-launch.d.ts +0 -2
- package/dist/features/launch/prepare-launch.js +2 -9
- package/dist/features/launch/profile-paths.d.ts +1 -2
- package/dist/features/launch/profile-paths.js +2 -8
- package/dist/features/launch/runtime-selection.d.ts +2 -5
- package/dist/features/owned-ui/settings-app.js +38 -14
- package/dist/foundation/lifecycle/model.d.ts +1 -1
- package/dist/foundation/lifecycle/model.js +1 -1
- package/dist/foundation/release/bootstrap.d.ts +1 -3
- package/dist/foundation/release/bootstrap.js +1 -1
- package/dist/foundation/storage/control-store.js +37 -3
- 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-footer-status.d.ts +1 -1
- package/dist/integrations/pi/components/shell-footer-status.js +14 -5
- package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.js +9 -8
- 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/submitted-prompt-adapter.d.ts +13 -0
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
- 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 +21 -3
- package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
- package/dist/integrations/pi/engine/session-integration.js +17 -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 +34 -5
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +294 -45
- 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 +167 -24
- 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/adapter.d.ts +2 -1
- package/dist/integrations/pi/tui-runtime/adapter.js +51 -2
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -6
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +3 -3
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +2 -2
- package/dist/product-identity.js +3 -4
- package/dist/product-identity.json +3 -5
- package/dist/ui/apps/contracts.d.ts +23 -1
- package/dist/ui/apps/host.js +9 -1
- package/dist/ui/components/index.d.ts +3 -0
- package/dist/ui/components/index.js +3 -0
- package/dist/ui/components/mouse.d.ts +10 -0
- package/dist/ui/components/mouse.js +23 -0
- package/dist/ui/components/prompt-row.d.ts +14 -0
- package/dist/ui/components/prompt-row.js +20 -0
- package/dist/ui/components/scrollbar.d.ts +26 -0
- package/dist/ui/components/scrollbar.js +35 -0
- package/dist/ui/components/spans.d.ts +21 -0
- package/dist/ui/components/spans.js +178 -0
- package/dist/ui/components/submitted-prompt.d.ts +23 -0
- package/dist/ui/components/submitted-prompt.js +50 -0
- package/dist/ui/components/text-selection.d.ts +53 -0
- package/dist/ui/components/text-selection.js +168 -0
- package/dist/ui/components/text.d.ts +14 -0
- package/dist/ui/components/text.js +40 -0
- package/dist/ui/components/transcript-viewport.d.ts +104 -0
- package/dist/ui/components/transcript-viewport.js +381 -0
- package/dist/ui/settings/declarations.d.ts +8 -1
- package/dist/ui/settings/declarations.js +33 -5
- package/dist/ui/settings/migrations.d.ts +1 -1
- package/dist/ui/settings/migrations.js +21 -2
- package/dist/ui/settings/sections.js +15 -8
- package/dist/ui/settings/session.d.ts +2 -0
- package/dist/ui/settings/session.js +7 -5
- package/docs/architecture/boundaries.md +3 -3
- package/docs/architecture/project-structure.md +2 -2
- package/docs/architecture/resource-and-data-policy.md +1 -1
- package/docs/architecture/toolchain.md +5 -5
- package/docs/architecture/ui-reference-provenance.md +11 -2
- package/docs/features/launch-profiles.md +7 -14
- package/docs/manual-launch-instance-acceptance.md +12 -35
- package/docs/manual-owned-ui-checkpoint.md +2 -3
- package/docs/manual-pi-boundary-candidate.md +4 -5
- package/package.json +4 -2
- package/dist/features/launch/profiles.d.ts +0 -12
- package/dist/features/launch/profiles.js +0 -26
- 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,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.#
|
|
356
|
-
: new TuiMainScreen(this.#
|
|
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-
|
|
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-
|
|
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-
|
|
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
|