@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
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { backgroundSgrSpan, composeSubmittedPromptRows, displayWidth, formatSubmittedPromptTime, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, submittedPromptLayout, stripAnsi, } from "../../../ui/components/index.js";
|
|
2
2
|
import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
|
|
3
|
-
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
|
|
3
|
+
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piShellVisibleWidth, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
|
|
4
4
|
import { PiTuiRuntimeAdapter, } from "../tui-runtime/index.js";
|
|
5
|
+
import { PromptChipStore } from "./prompt-chips.js";
|
|
6
|
+
import { SessionViewportController } from "./session-viewport-controller.js";
|
|
5
7
|
export class OwnedUiSessionShellRoot {
|
|
6
8
|
editor;
|
|
7
9
|
header;
|
|
@@ -19,20 +21,10 @@ export class OwnedUiSessionShellRoot {
|
|
|
19
21
|
#footer;
|
|
20
22
|
#queued;
|
|
21
23
|
#extensionRenderers;
|
|
24
|
+
#promptChips = new PromptChipStore();
|
|
22
25
|
#customViewport;
|
|
23
26
|
#submittedPromptComposer;
|
|
24
|
-
#
|
|
25
|
-
#viewportConfig = {
|
|
26
|
-
scrollbarAppearance: "hover",
|
|
27
|
-
scrollbarStyle: "thin",
|
|
28
|
-
scrollbarSpeed: "normal",
|
|
29
|
-
};
|
|
30
|
-
#dragGrabOffset = null;
|
|
31
|
-
/** A left-button sequence begun in status/editor/footer chrome is swallowed. */
|
|
32
|
-
#dockPointerSuppressed = false;
|
|
33
|
-
#selectionAutoScrollTimer;
|
|
34
|
-
#selectionAutoScrollPointer;
|
|
35
|
-
#viewportActivityTimer;
|
|
27
|
+
#viewportController;
|
|
36
28
|
#componentRuntime;
|
|
37
29
|
#toolsExpanded = false;
|
|
38
30
|
#thinkingVisible = true;
|
|
@@ -54,7 +46,11 @@ export class OwnedUiSessionShellRoot {
|
|
|
54
46
|
this.#view = view;
|
|
55
47
|
this.#customViewport = sessionLayout === "custom-viewport";
|
|
56
48
|
this.#submittedPromptComposer = this.#customViewport
|
|
57
|
-
? {
|
|
49
|
+
? {
|
|
50
|
+
layout: submittedPromptLayout,
|
|
51
|
+
compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, style)
|
|
52
|
+
.map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor)),
|
|
53
|
+
}
|
|
58
54
|
: undefined;
|
|
59
55
|
this.#cwd = cwd;
|
|
60
56
|
this.#extensionRenderers = extensionRenderers;
|
|
@@ -66,10 +62,39 @@ export class OwnedUiSessionShellRoot {
|
|
|
66
62
|
this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned");
|
|
67
63
|
this.editor = createPiShellEditor({
|
|
68
64
|
...handlers,
|
|
65
|
+
keybindingProfile: this.#customViewport ? "a1" : "pi",
|
|
66
|
+
paintEditorSelection: (line, from, to, atomic) => backgroundSgrSpan(line, from, to, atomic ? "\u001b[7m" : "\u001b[27m\u001b[48;2;38;79;120m", atomic ? "\u001b[27m" : "\u001b[49m", piShellVisibleWidth),
|
|
67
|
+
transformPastedContent: content => this.#promptChips.transformPastedContent(content),
|
|
68
|
+
editorAtomicRanges: line => this.#promptChips.atomicRanges(line),
|
|
69
|
+
decorateEditorRow: (row, width) => {
|
|
70
|
+
const plain = stripAnsi(row);
|
|
71
|
+
const ranges = this.#promptChips.hyperlinkRanges(plain);
|
|
72
|
+
if (ranges.length === 0)
|
|
73
|
+
return row;
|
|
74
|
+
const linkResetAndTail = `\u001b]8;;\u001b\\\u001b[24m${" ".repeat(Math.max(0, width - piShellVisibleWidth(row)))}`;
|
|
75
|
+
// VS15 is zero-column and default-ignorable. It breaks Windows Terminal's
|
|
76
|
+
// plain-text URL detector only in the held-button paint; semantic text stays exact.
|
|
77
|
+
const paintRow = this.#viewportController.editorPointerSelecting
|
|
78
|
+
? row.replaceAll("https://", "https:\uFE0E//").replaceAll("http://", "http:\uFE0E//")
|
|
79
|
+
: row;
|
|
80
|
+
const decorated = this.#viewportController.editorPointerSelecting
|
|
81
|
+
? ranges.reduce((result, range) => backgroundSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), "\u001b[4:4m", "\u001b[24m", piShellVisibleWidth), paintRow)
|
|
82
|
+
: ranges.reduce((result, range) => hyperlinkSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), range.target, piShellVisibleWidth), row);
|
|
83
|
+
// Windows Terminal can retain stale native dotted-link cells when a mutable
|
|
84
|
+
// prompt replaces a longer URL. Explicit non-link spaces overwrite that tail.
|
|
85
|
+
return `${decorated}${linkResetAndTail}`;
|
|
86
|
+
},
|
|
87
|
+
expandCopiedEditorText: text => this.#promptChips.expandCopiedText(text),
|
|
69
88
|
cwd,
|
|
70
89
|
...(agentDir === undefined ? {} : { agentDir }),
|
|
71
90
|
onToolsExpand: () => this.#setToolsExpanded(!this.#toolsExpanded),
|
|
72
91
|
});
|
|
92
|
+
this.#viewportController = new SessionViewportController({
|
|
93
|
+
enabled: this.#customViewport,
|
|
94
|
+
editor: this.editor,
|
|
95
|
+
requestRender: force => this.#componentRuntime.requestRender(force),
|
|
96
|
+
hasEditorLinks: () => this.#promptChips.hyperlinkRanges(this.editor.getText()).length > 0,
|
|
97
|
+
});
|
|
73
98
|
this.editor.setThinkingLevel(view.thinkingLevel);
|
|
74
99
|
this.#inputSurface = this.editor;
|
|
75
100
|
for (const block of view.transcript) {
|
|
@@ -84,6 +109,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
84
109
|
this.#documentLayouts.clear();
|
|
85
110
|
});
|
|
86
111
|
}
|
|
112
|
+
preparePromptSubmission(text) {
|
|
113
|
+
return this.#promptChips.prepareSubmission(text);
|
|
114
|
+
}
|
|
87
115
|
update(view) {
|
|
88
116
|
if (view.diagnostics.length !== this.#view.diagnostics.length
|
|
89
117
|
|| view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
|
|
@@ -121,31 +149,44 @@ export class OwnedUiSessionShellRoot {
|
|
|
121
149
|
this.#documentLayouts.clear();
|
|
122
150
|
}
|
|
123
151
|
render(width) {
|
|
124
|
-
if (!this.#customViewport)
|
|
152
|
+
if (!this.#customViewport) {
|
|
153
|
+
this.#viewportController.setEditorPointerFrame(undefined);
|
|
125
154
|
return [...this.#renderDocument(width), ...this.#renderDock(width)];
|
|
155
|
+
}
|
|
126
156
|
const height = Math.max(0, this.#componentRuntime.getRows());
|
|
127
157
|
const dock = this.#renderDockLayout(width);
|
|
128
|
-
const
|
|
158
|
+
const availableWithoutTransient = Math.max(0, height - Math.min(height, dock.rowsWithoutTransient.length));
|
|
129
159
|
// Ordinary transcript content uses the full terminal width. The rail is an
|
|
130
160
|
// overlay, not a lost wrapping cell. Submitted prompts alone retain the
|
|
131
161
|
// intentional final gutter after their right-aligned timestamp.
|
|
132
162
|
const documentWidth = width;
|
|
133
163
|
let document = this.#renderDocumentLayout(documentWidth);
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.#
|
|
147
|
-
|
|
164
|
+
// Keep queued steering/follow-up messages and Working together in Pi's
|
|
165
|
+
// semantic order while content fits. Once the transcript overflows, move
|
|
166
|
+
// the complete transient group into its tail so every row naturally
|
|
167
|
+
// scrolls away and the detached viewport can use the full space above the
|
|
168
|
+
// stable editor/footer dock.
|
|
169
|
+
const pinTransient = document.rows.length + dock.transientRows.length <= availableWithoutTransient;
|
|
170
|
+
const dockRows = pinTransient ? dock.rows : dock.rowsWithoutTransient;
|
|
171
|
+
const selectableDocumentRowCount = document.rows.length;
|
|
172
|
+
if (!pinTransient)
|
|
173
|
+
document = { ...document, rows: [...document.rows, ...dock.transientRows] };
|
|
174
|
+
const dockStartRow = height - dockRows.length + 1;
|
|
175
|
+
const editorOffset = pinTransient ? dock.editorOffsetWithTransient : dock.editorOffsetWithoutTransient;
|
|
176
|
+
this.#viewportController.setEditorPointerFrame(this.usesDefaultInputSurface()
|
|
177
|
+
? {
|
|
178
|
+
rowStart: dockStartRow + editorOffset,
|
|
179
|
+
rowEnd: dockStartRow + editorOffset + dock.inputRows - 1,
|
|
180
|
+
}
|
|
181
|
+
: undefined);
|
|
182
|
+
return this.#viewportController.compose({
|
|
148
183
|
documentRows: document.rows,
|
|
184
|
+
...(this.#viewportController.transcriptPointerSelecting
|
|
185
|
+
? { paintDocumentRow: heldNativeHyperlinkStyle }
|
|
186
|
+
: {}),
|
|
187
|
+
// Overflowing queued and Working rows scroll with the transcript but
|
|
188
|
+
// remain status chrome: selection and copying stop at the real document tail.
|
|
189
|
+
selectableDocumentRowCount,
|
|
149
190
|
dockRows,
|
|
150
191
|
promptAnchors: document.promptAnchors,
|
|
151
192
|
width,
|
|
@@ -172,236 +213,23 @@ export class OwnedUiSessionShellRoot {
|
|
|
172
213
|
}).rows;
|
|
173
214
|
}
|
|
174
215
|
setViewportConfig(config) {
|
|
175
|
-
if (
|
|
216
|
+
if (this.#viewportController.setConfig(config))
|
|
176
217
|
this.#documentLayouts.clear();
|
|
177
|
-
this.#viewportConfig = config;
|
|
178
|
-
this.#viewport.setConfig(config);
|
|
179
|
-
this.#componentRuntime.requestRender();
|
|
180
218
|
}
|
|
181
219
|
resumeViewportFollowing() {
|
|
182
|
-
|
|
183
|
-
return;
|
|
184
|
-
this.#stopSelectionAutoScroll();
|
|
185
|
-
const selectionChanged = this.#viewport.clearSelection();
|
|
186
|
-
const scrolled = this.#viewport.scrollToEnd();
|
|
187
|
-
if (scrolled)
|
|
188
|
-
this.#scheduleViewportActivityExpiry();
|
|
189
|
-
if (scrolled || selectionChanged)
|
|
190
|
-
this.#componentRuntime.requestRender();
|
|
220
|
+
this.#viewportController.resumeFollowing();
|
|
191
221
|
}
|
|
192
222
|
noteCompletedAssistantMessage() {
|
|
193
|
-
|
|
194
|
-
return;
|
|
195
|
-
this.#componentRuntime.requestRender();
|
|
223
|
+
this.#viewportController.noteCompletedAssistantMessage();
|
|
196
224
|
}
|
|
197
225
|
resetViewport() {
|
|
198
|
-
|
|
199
|
-
clearTimeout(this.#viewportActivityTimer);
|
|
200
|
-
this.#viewportActivityTimer = undefined;
|
|
201
|
-
this.#stopSelectionAutoScroll();
|
|
202
|
-
this.#viewport.reset();
|
|
226
|
+
this.#viewportController.reset();
|
|
203
227
|
}
|
|
204
228
|
clearViewportPointerState() {
|
|
205
|
-
|
|
206
|
-
clearTimeout(this.#viewportActivityTimer);
|
|
207
|
-
this.#viewportActivityTimer = undefined;
|
|
208
|
-
this.#dragGrabOffset = null;
|
|
209
|
-
this.#dockPointerSuppressed = false;
|
|
210
|
-
this.#stopSelectionAutoScroll();
|
|
211
|
-
this.#viewport.clearSelection();
|
|
212
|
-
this.#viewport.clearTransient();
|
|
229
|
+
this.#viewportController.clearPointerState();
|
|
213
230
|
}
|
|
214
231
|
handleViewportPreInput(data, allowWheel = true, now = Date.now()) {
|
|
215
|
-
|
|
216
|
-
return { data, consumed: false };
|
|
217
|
-
if (data === "\u0003") {
|
|
218
|
-
const copyText = this.#viewport.selectedText();
|
|
219
|
-
if (copyText !== null && copyText.length > 0) {
|
|
220
|
-
this.#viewport.clearSelection();
|
|
221
|
-
this.#componentRuntime.requestRender();
|
|
222
|
-
return { data: "", consumed: true, copyText };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (allowWheel && ALT_END_INPUTS.has(data) && !this.#viewport.followingEnd) {
|
|
226
|
-
this.#viewport.scrollToEnd(now);
|
|
227
|
-
this.#scheduleViewportActivityExpiry();
|
|
228
|
-
this.#componentRuntime.requestRender();
|
|
229
|
-
return { data: "", consumed: true };
|
|
230
|
-
}
|
|
231
|
-
if (allowWheel && ALT_HOME_INPUTS.has(data)) {
|
|
232
|
-
if (this.#viewport.scrollToPreviousPrompt(now)) {
|
|
233
|
-
this.#scheduleViewportActivityExpiry();
|
|
234
|
-
this.#componentRuntime.requestRender();
|
|
235
|
-
}
|
|
236
|
-
return { data: "", consumed: true };
|
|
237
|
-
}
|
|
238
|
-
const frame = this.#viewport.frame;
|
|
239
|
-
let repaint = false;
|
|
240
|
-
let activity = false;
|
|
241
|
-
const routed = routeMouseInput(data, event => {
|
|
242
|
-
const hits = frame.hits;
|
|
243
|
-
const overRail = hits.rail !== null
|
|
244
|
-
&& event.column === hits.rail.column
|
|
245
|
-
&& event.row >= hits.rail.rowStart
|
|
246
|
-
&& event.row < hits.rail.rowStart + hits.rail.trackHeight;
|
|
247
|
-
const overSticky = hits.sticky !== null && event.row === hits.sticky.row && event.column <= hits.sticky.width;
|
|
248
|
-
const overBottom = hits.bottom !== null && event.row === hits.bottom.row
|
|
249
|
-
&& event.column >= hits.bottom.columnStart && event.column <= hits.bottom.columnEnd;
|
|
250
|
-
if (event.kind === "motion") {
|
|
251
|
-
this.#viewport.setRailHovered(overRail);
|
|
252
|
-
this.#viewport.setStickyHovered(overSticky);
|
|
253
|
-
this.#viewport.setBottomHovered(overBottom);
|
|
254
|
-
repaint = true;
|
|
255
|
-
if (this.#dockPointerSuppressed)
|
|
256
|
-
return true;
|
|
257
|
-
// Once a left-button selection starts, its release—not the button bits
|
|
258
|
-
// on an intermediate motion report—ends ownership. Some terminals emit
|
|
259
|
-
// no-button motion while busy frames are being coalesced; accepting it
|
|
260
|
-
// keeps the drag continuous instead of apparently freezing.
|
|
261
|
-
if (this.#viewport.selectionActive) {
|
|
262
|
-
this.#viewport.extendSelection(event.column, event.row, now, false);
|
|
263
|
-
this.#updateSelectionAutoScroll(event.column, event.row, hits.viewportHeight);
|
|
264
|
-
activity = true;
|
|
265
|
-
return true;
|
|
266
|
-
}
|
|
267
|
-
if (this.#dragGrabOffset !== null && hits.rail !== null) {
|
|
268
|
-
const target = scrollForThumbRow(hits.rail.geometry, event.row - hits.rail.rowStart - this.#dragGrabOffset);
|
|
269
|
-
this.#viewport.scrollTo(target, now);
|
|
270
|
-
activity = true;
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
return overRail || overSticky || overBottom;
|
|
274
|
-
}
|
|
275
|
-
if (event.kind === "wheel-up" || event.kind === "wheel-down") {
|
|
276
|
-
if (!allowWheel || event.row < 1 || event.row > hits.viewportHeight)
|
|
277
|
-
return false;
|
|
278
|
-
const distance = scrollbarWheelRows(this.#viewportConfig.scrollbarSpeed);
|
|
279
|
-
this.#viewport.scrollBy(event.kind === "wheel-up" ? -distance : distance, now);
|
|
280
|
-
activity = true;
|
|
281
|
-
repaint = true;
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
if (event.kind === "press") {
|
|
285
|
-
if (event.button !== 0)
|
|
286
|
-
return false;
|
|
287
|
-
this.#stopSelectionAutoScroll();
|
|
288
|
-
if (this.#viewport.clearSelection())
|
|
289
|
-
repaint = true;
|
|
290
|
-
if (overBottom) {
|
|
291
|
-
this.#viewport.scrollToEnd(now);
|
|
292
|
-
activity = true;
|
|
293
|
-
repaint = true;
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
if (overSticky && hits.sticky !== null) {
|
|
297
|
-
this.#viewport.scrollTo(hits.sticky.target, now);
|
|
298
|
-
activity = true;
|
|
299
|
-
repaint = true;
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
if (overRail && hits.rail !== null) {
|
|
303
|
-
const trackRow = event.row - hits.rail.rowStart;
|
|
304
|
-
const geometry = hits.rail.geometry;
|
|
305
|
-
if (trackRow >= geometry.thumbTop && trackRow < geometry.thumbTop + geometry.thumbHeight) {
|
|
306
|
-
this.#dragGrabOffset = trackRow - geometry.thumbTop;
|
|
307
|
-
this.#viewport.setRailDragging(true);
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
this.#viewport.scrollTo(scrollForTrackPage(geometry, trackRow, frame.scrollTop, hits.viewportHeight), now);
|
|
311
|
-
}
|
|
312
|
-
activity = true;
|
|
313
|
-
repaint = true;
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
if (event.row > hits.viewportHeight) {
|
|
317
|
-
// Status, editor, widgets, and footer are controls rather than
|
|
318
|
-
// transcript. Consume the complete drag so Pi's fullscreen fallback
|
|
319
|
-
// selection cannot paint those rows.
|
|
320
|
-
this.#dockPointerSuppressed = true;
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
if (event.row >= 1 && event.row <= hits.viewportHeight && event.column <= frame.contentWidth) {
|
|
324
|
-
this.#viewport.pressSelection(event.column, event.row, now);
|
|
325
|
-
repaint = true;
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
330
|
-
if (event.kind === "release") {
|
|
331
|
-
if (this.#viewport.releaseSelection()) {
|
|
332
|
-
this.#stopSelectionAutoScroll();
|
|
333
|
-
repaint = true;
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
336
|
-
if (this.#dragGrabOffset !== null) {
|
|
337
|
-
this.#dragGrabOffset = null;
|
|
338
|
-
this.#viewport.setRailDragging(false);
|
|
339
|
-
repaint = true;
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
|
-
if (this.#dockPointerSuppressed) {
|
|
343
|
-
this.#dockPointerSuppressed = false;
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
return false;
|
|
348
|
-
});
|
|
349
|
-
if (activity)
|
|
350
|
-
this.#scheduleViewportActivityExpiry();
|
|
351
|
-
if (repaint)
|
|
352
|
-
this.#componentRuntime.requestRender();
|
|
353
|
-
return routed;
|
|
354
|
-
}
|
|
355
|
-
#updateSelectionAutoScroll(column, row, viewportHeight) {
|
|
356
|
-
const beyondEdge = row <= 1 || row > viewportHeight;
|
|
357
|
-
if (!beyondEdge) {
|
|
358
|
-
this.#stopSelectionAutoScroll();
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
this.#selectionAutoScrollPointer = { column, row };
|
|
362
|
-
if (this.#selectionAutoScrollTimer !== undefined)
|
|
363
|
-
return;
|
|
364
|
-
this.#selectionAutoScrollTimer = setTimeout(() => this.#selectionAutoScrollTick(), SELECTION_AUTO_SCROLL_INTERVAL_MS);
|
|
365
|
-
this.#selectionAutoScrollTimer.unref?.();
|
|
366
|
-
}
|
|
367
|
-
#selectionAutoScrollTick() {
|
|
368
|
-
this.#selectionAutoScrollTimer = undefined;
|
|
369
|
-
const pointer = this.#selectionAutoScrollPointer;
|
|
370
|
-
if (pointer === undefined || !this.#viewport.selectionActive) {
|
|
371
|
-
this.#stopSelectionAutoScroll();
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
const before = this.#viewport.scrollTop;
|
|
375
|
-
const rowsPerTick = scrollbarSelectionRows(this.#viewportConfig.scrollbarSpeed);
|
|
376
|
-
for (let row = 0; row < rowsPerTick; row += 1) {
|
|
377
|
-
const previous = this.#viewport.scrollTop;
|
|
378
|
-
this.#viewport.extendSelection(pointer.column, pointer.row);
|
|
379
|
-
if (this.#viewport.scrollTop === previous)
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
if (this.#viewport.scrollTop === before) {
|
|
383
|
-
this.#stopSelectionAutoScroll();
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
this.#componentRuntime.requestRender();
|
|
387
|
-
this.#scheduleViewportActivityExpiry();
|
|
388
|
-
this.#selectionAutoScrollTimer = setTimeout(() => this.#selectionAutoScrollTick(), SELECTION_AUTO_SCROLL_INTERVAL_MS);
|
|
389
|
-
this.#selectionAutoScrollTimer.unref?.();
|
|
390
|
-
}
|
|
391
|
-
#stopSelectionAutoScroll() {
|
|
392
|
-
if (this.#selectionAutoScrollTimer !== undefined)
|
|
393
|
-
clearTimeout(this.#selectionAutoScrollTimer);
|
|
394
|
-
this.#selectionAutoScrollTimer = undefined;
|
|
395
|
-
this.#selectionAutoScrollPointer = undefined;
|
|
396
|
-
}
|
|
397
|
-
#scheduleViewportActivityExpiry() {
|
|
398
|
-
if (this.#viewportActivityTimer !== undefined)
|
|
399
|
-
clearTimeout(this.#viewportActivityTimer);
|
|
400
|
-
this.#viewportActivityTimer = setTimeout(() => {
|
|
401
|
-
this.#viewportActivityTimer = undefined;
|
|
402
|
-
this.#componentRuntime.requestRender();
|
|
403
|
-
}, 925);
|
|
404
|
-
this.#viewportActivityTimer.unref?.();
|
|
232
|
+
return this.#viewportController.handlePreInput(data, allowWheel, now);
|
|
405
233
|
}
|
|
406
234
|
#renderDock(width) {
|
|
407
235
|
return this.#renderDockLayout(width).rows;
|
|
@@ -409,15 +237,20 @@ export class OwnedUiSessionShellRoot {
|
|
|
409
237
|
#renderDockLayout(width) {
|
|
410
238
|
const queued = this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
|
|
411
239
|
const statusRows = this.#renderStatus(width);
|
|
240
|
+
const transientRows = [...queued, ...statusRows];
|
|
412
241
|
const aboveWidgets = this.#renderWidgets("aboveEditor", width);
|
|
413
242
|
const input = this.#inputSurface.render(width);
|
|
414
243
|
const belowWidgets = this.#renderWidgets("belowEditor", width);
|
|
415
244
|
const footer = this.#renderFooter(width);
|
|
245
|
+
const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
|
|
416
246
|
return {
|
|
417
|
-
rows: [...
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
stableBottomRows:
|
|
247
|
+
rows: [...transientRows, ...rowsWithoutTransient],
|
|
248
|
+
rowsWithoutTransient,
|
|
249
|
+
transientRows,
|
|
250
|
+
stableBottomRows: rowsWithoutTransient.length,
|
|
251
|
+
editorOffsetWithTransient: transientRows.length + aboveWidgets.length,
|
|
252
|
+
editorOffsetWithoutTransient: aboveWidgets.length,
|
|
253
|
+
inputRows: input.length,
|
|
421
254
|
};
|
|
422
255
|
}
|
|
423
256
|
layoutRoot() {
|
|
@@ -501,7 +334,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
501
334
|
continue;
|
|
502
335
|
const blockWidth = this.#customViewport
|
|
503
336
|
&& block?.kind === "user"
|
|
504
|
-
&& this.#
|
|
337
|
+
&& this.#viewportController.config.scrollbarAppearance !== "hidden"
|
|
505
338
|
&& width > 1
|
|
506
339
|
? width - 1
|
|
507
340
|
: width;
|
|
@@ -560,13 +393,19 @@ export class OwnedUiSessionShellRoot {
|
|
|
560
393
|
const component = this.#transcript.get(id);
|
|
561
394
|
if (component === undefined)
|
|
562
395
|
return [];
|
|
396
|
+
const render = () => {
|
|
397
|
+
const rows = component.render(width);
|
|
398
|
+
if (!this.#customViewport || block?.kind === "user")
|
|
399
|
+
return rows;
|
|
400
|
+
return rows.map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor));
|
|
401
|
+
};
|
|
563
402
|
if (block === undefined || block.status !== "finalized")
|
|
564
|
-
return
|
|
403
|
+
return render();
|
|
565
404
|
let byWidth = this.#renderedRows.get(id);
|
|
566
405
|
const cached = byWidth?.get(width);
|
|
567
406
|
if (cached?.revision === block.revision)
|
|
568
407
|
return cached.rows;
|
|
569
|
-
const rows =
|
|
408
|
+
const rows = render();
|
|
570
409
|
if (byWidth === undefined) {
|
|
571
410
|
byWidth = new Map();
|
|
572
411
|
this.#renderedRows.set(id, byWidth);
|
|
@@ -942,11 +781,12 @@ export class OwnedUiSessionShellRoot {
|
|
|
942
781
|
this.invalidate();
|
|
943
782
|
}
|
|
944
783
|
}
|
|
945
|
-
const SELECTION_AUTO_SCROLL_INTERVAL_MS = 30;
|
|
946
|
-
const ALT_END_INPUTS = new Set(["\u001b[1;3F", "\u001b[4;3~", "\u001b[8;3~"]);
|
|
947
|
-
const ALT_HOME_INPUTS = new Set(["\u001b[1;3H", "\u001b[1;3~", "\u001b[7;3~"]);
|
|
948
784
|
const SCROLLBAR_CELL_RESET = "\u001b[22;23;24;25;27;28;29;39;54;55m";
|
|
949
785
|
const TERMINAL_BACKGROUND = /\u001b\[(?:4[0-9]|10[0-7]|48(?:[;:][0-9;:]*)?)m/g;
|
|
786
|
+
/** Web URLs use link blue; file targets retain A1's cyan interactive accent. */
|
|
787
|
+
function nativeTranscriptLinkColor(text, target) {
|
|
788
|
+
return piTheme().fg(/^file:/iu.test(target) ? "accent" : "mdLink", text);
|
|
789
|
+
}
|
|
950
790
|
/** Repaint a source prompt with viewport chrome while preserving text, links, and foreground roles. */
|
|
951
791
|
function withoutTerminalBackground(text) {
|
|
952
792
|
return text.replace(TERMINAL_BACKGROUND, "");
|
|
@@ -16,7 +16,7 @@ export declare class OwnedUiSessionShell {
|
|
|
16
16
|
waitUntilStopped(): Promise<void>;
|
|
17
17
|
submit(text: string): Promise<AdapterCommandResult>;
|
|
18
18
|
clearOrExit(now?: number): Promise<AdapterCommandResult>;
|
|
19
|
-
interrupt(): Promise<AdapterCommandResult>;
|
|
19
|
+
interrupt(now?: number): Promise<AdapterCommandResult>;
|
|
20
20
|
abort(): Promise<AdapterCommandResult>;
|
|
21
21
|
retry(): Promise<AdapterCommandResult>;
|
|
22
22
|
compact(): Promise<AdapterCommandResult>;
|
|
@@ -2,6 +2,7 @@ import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../..
|
|
|
2
2
|
import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
|
|
3
3
|
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
|
|
4
4
|
import { PiTuiRuntimeAdapter, } from "../tui-runtime/index.js";
|
|
5
|
+
import { preloadSystemClipboard, readSystemClipboardContent, writeSystemClipboardText, } from "./system-clipboard.js";
|
|
5
6
|
import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
|
|
6
7
|
export { OwnedUiSessionShellRoot } from "./session-shell-root.js";
|
|
7
8
|
export class OwnedUiSessionShell {
|
|
@@ -27,6 +28,7 @@ export class OwnedUiSessionShell {
|
|
|
27
28
|
#unsubscribeSettings;
|
|
28
29
|
#compactionQueue = [];
|
|
29
30
|
#lastClearTime = 0;
|
|
31
|
+
#lastEscapeTime = 0;
|
|
30
32
|
#activeLoginDialog;
|
|
31
33
|
#sessionGeneration;
|
|
32
34
|
constructor(options) {
|
|
@@ -35,14 +37,17 @@ export class OwnedUiSessionShell {
|
|
|
35
37
|
this.#cwd = options.cwd;
|
|
36
38
|
this.#routeHost = options.routeHost ?? null;
|
|
37
39
|
this.#customViewport = options.sessionLayout === "custom-viewport";
|
|
40
|
+
if (this.#customViewport && options.clipboard === undefined)
|
|
41
|
+
preloadSystemClipboard();
|
|
38
42
|
this.#stopped = new Promise(resolve => {
|
|
39
43
|
this.#resolveStopped = resolve;
|
|
40
44
|
});
|
|
41
45
|
let runtime;
|
|
46
|
+
let pendingClipboardWrite = Promise.resolve();
|
|
42
47
|
this.root = new OwnedUiSessionShellRoot(this.backend.view(), options.cwd, {
|
|
43
48
|
getColumns: () => runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
|
|
44
49
|
getRows: () => runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
|
|
45
|
-
requestRender:
|
|
50
|
+
requestRender: force => runtime?.requestRender(force),
|
|
46
51
|
onSubmit: text => { void this.submit(text); },
|
|
47
52
|
onInterrupt: () => { void this.interrupt(); },
|
|
48
53
|
onClear: () => { void this.clearOrExit(); },
|
|
@@ -57,6 +62,23 @@ export class OwnedUiSessionShell {
|
|
|
57
62
|
onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
|
|
58
63
|
onFollowUp: () => { void this.queueFollowUp(); },
|
|
59
64
|
onDequeue: () => this.restoreQueuedInput(),
|
|
65
|
+
onCopyText: text => {
|
|
66
|
+
runtime?.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
|
|
67
|
+
const write = options.clipboard === undefined
|
|
68
|
+
? writeSystemClipboardText(text)
|
|
69
|
+
: options.clipboard.writeText?.(text) ?? Promise.resolve();
|
|
70
|
+
pendingClipboardWrite = write.catch(() => { });
|
|
71
|
+
},
|
|
72
|
+
readClipboardContent: async () => {
|
|
73
|
+
await pendingClipboardWrite;
|
|
74
|
+
if (options.clipboard === undefined)
|
|
75
|
+
return readSystemClipboardContent();
|
|
76
|
+
const image = await options.clipboard.readImage?.();
|
|
77
|
+
if (image !== null && image !== undefined)
|
|
78
|
+
return { kind: "image", ...image };
|
|
79
|
+
const text = await options.clipboard.readText();
|
|
80
|
+
return text === null ? null : { kind: "text", text };
|
|
81
|
+
},
|
|
60
82
|
}, {
|
|
61
83
|
...options.startup,
|
|
62
84
|
resources: options.startup?.resources ?? shellResourceEntries(this.backend),
|
|
@@ -98,7 +120,7 @@ export class OwnedUiSessionShell {
|
|
|
98
120
|
const applyViewportSettings = () => {
|
|
99
121
|
const snapshot = options.viewportSettings?.snapshot();
|
|
100
122
|
this.root.setViewportConfig(snapshot ?? {
|
|
101
|
-
scrollbarAppearance: "
|
|
123
|
+
scrollbarAppearance: "auto",
|
|
102
124
|
scrollbarStyle: "thin",
|
|
103
125
|
scrollbarSpeed: "normal",
|
|
104
126
|
});
|
|
@@ -182,16 +204,18 @@ export class OwnedUiSessionShell {
|
|
|
182
204
|
return this.#stopped;
|
|
183
205
|
}
|
|
184
206
|
async submit(text) {
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
207
|
+
const displayInput = text.trim();
|
|
208
|
+
if (!displayInput)
|
|
187
209
|
return { outcome: "completed", diagnostic: null };
|
|
188
|
-
if (
|
|
189
|
-
return this.#slashCommand(
|
|
210
|
+
if (displayInput.startsWith("/"))
|
|
211
|
+
return this.#slashCommand(displayInput);
|
|
212
|
+
const prepared = this.root.preparePromptSubmission(displayInput);
|
|
213
|
+
const input = prepared.text.trim();
|
|
190
214
|
if (input.startsWith("!")) {
|
|
191
215
|
const excludeFromContext = input.startsWith("!!");
|
|
192
216
|
const command = input.slice(excludeFromContext ? 2 : 1).trim();
|
|
193
217
|
if (command) {
|
|
194
|
-
this.root.editor.addToHistory(
|
|
218
|
+
this.root.editor.addToHistory(displayInput);
|
|
195
219
|
try {
|
|
196
220
|
const result = await this.backend.executeBashWorkflow(command, excludeFromContext);
|
|
197
221
|
const workflow = {
|
|
@@ -213,20 +237,21 @@ export class OwnedUiSessionShell {
|
|
|
213
237
|
}
|
|
214
238
|
}
|
|
215
239
|
if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
|
|
216
|
-
this.root.editor.addToHistory(
|
|
240
|
+
this.root.editor.addToHistory(displayInput);
|
|
217
241
|
this.#compactionQueue.push({ text: input, type: "steer" });
|
|
218
242
|
this.root.appendWorkflowResult({ command: "compact", outcome: "completed", message: `Queued during compaction: ${input}` });
|
|
219
243
|
this.runtime.requestRender();
|
|
220
244
|
return { outcome: "completed", diagnostic: null };
|
|
221
245
|
}
|
|
222
246
|
const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
|
|
223
|
-
this.root.editor.addToHistory(
|
|
247
|
+
this.root.editor.addToHistory(displayInput);
|
|
224
248
|
this.root.resumeViewportFollowing();
|
|
225
249
|
return this.#execute({
|
|
226
250
|
type,
|
|
227
251
|
correlationId: this.#correlation(type),
|
|
228
252
|
sessionId: this.backend.sessionId,
|
|
229
253
|
text: input,
|
|
254
|
+
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
230
255
|
});
|
|
231
256
|
}
|
|
232
257
|
async clearOrExit(now = Date.now()) {
|
|
@@ -237,14 +262,29 @@ export class OwnedUiSessionShell {
|
|
|
237
262
|
this.runtime.requestRender();
|
|
238
263
|
return { outcome: "completed", diagnostic: null };
|
|
239
264
|
}
|
|
240
|
-
async interrupt() {
|
|
265
|
+
async interrupt(now = Date.now()) {
|
|
241
266
|
if (this.view().lifecycle === "busy")
|
|
242
267
|
return this.abort();
|
|
243
|
-
if (this.root.editor.getText().length > 0) {
|
|
268
|
+
if (this.root.editor.getText().trim().length > 0) {
|
|
244
269
|
this.root.editor.setText("");
|
|
245
270
|
return { outcome: "completed", diagnostic: null };
|
|
246
271
|
}
|
|
247
|
-
|
|
272
|
+
// Match Pi: with an empty editor, two escapes inside 500 ms open the
|
|
273
|
+
// configured session navigator. The first escape intentionally does not
|
|
274
|
+
// interrupt or mutate the prompt.
|
|
275
|
+
const action = this.backend.pinnedSettingsSnapshot().doubleEscapeAction;
|
|
276
|
+
if (action === "none")
|
|
277
|
+
return { outcome: "rejected", diagnostic: "nothing to interrupt" };
|
|
278
|
+
if (now - this.#lastEscapeTime < 500) {
|
|
279
|
+
this.#lastEscapeTime = 0;
|
|
280
|
+
if (action === "tree")
|
|
281
|
+
this.showTreeSelector();
|
|
282
|
+
else
|
|
283
|
+
this.showForkSelector();
|
|
284
|
+
return { outcome: "completed", diagnostic: null };
|
|
285
|
+
}
|
|
286
|
+
this.#lastEscapeTime = now;
|
|
287
|
+
return { outcome: "completed", diagnostic: null };
|
|
248
288
|
}
|
|
249
289
|
async abort() {
|
|
250
290
|
return this.#execute(this.#simple("abort"));
|