@timurproko/a1 0.1.8-dev.137 → 0.1.8-dev.139
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 +19 -1
- package/dist/contracts/owned-ui/model.d.ts +21 -0
- package/dist/contracts/owned-ui/validation.js +3 -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 +36 -13
- 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/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/submitted-prompt-adapter.d.ts +13 -0
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
- package/dist/integrations/pi/engine/adapter.js +16 -2
- package/dist/integrations/pi/engine/session-integration.js +5 -2
- package/dist/integrations/pi/owned-ui/session-shell-root.d.ts +17 -2
- package/dist/integrations/pi/owned-ui/session-shell-root.js +451 -42
- package/dist/integrations/pi/owned-ui/session-shell.js +116 -13
- 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 +2 -0
- 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 +2 -2
- 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/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 +6 -0
- package/dist/ui/components/spans.js +36 -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 +98 -0
- package/dist/ui/components/transcript-viewport.js +353 -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 +12 -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 +2 -2
- package/docs/architecture/project-structure.md +1 -1
- 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 +1 -2
- package/dist/features/launch/profiles.d.ts +0 -12
- package/dist/features/launch/profiles.js +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TranscriptViewport, backgroundSgrSpan, composeSubmittedPromptRows, displayWidth, formatSubmittedPromptTime, overlaySpan, submittedPromptLayout, routeMouseInput, scrollForTrackPage, scrollForThumbRow, scrollbarSelectionRows, scrollbarWheelRows, } from "../../../ui/components/index.js";
|
|
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";
|
|
@@ -10,6 +10,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
10
10
|
#transcript = new Map();
|
|
11
11
|
#blocksById = new Map();
|
|
12
12
|
#renderedRows = new Map();
|
|
13
|
+
/** Flattened transcript rows reused by wheel, rail, jump, and selection frames. */
|
|
14
|
+
#documentLayouts = new Map();
|
|
13
15
|
#themeUnsubscribe;
|
|
14
16
|
#transcriptOrder = [];
|
|
15
17
|
#view;
|
|
@@ -17,6 +19,20 @@ export class OwnedUiSessionShellRoot {
|
|
|
17
19
|
#footer;
|
|
18
20
|
#queued;
|
|
19
21
|
#extensionRenderers;
|
|
22
|
+
#customViewport;
|
|
23
|
+
#submittedPromptComposer;
|
|
24
|
+
#viewport = new TranscriptViewport();
|
|
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;
|
|
20
36
|
#componentRuntime;
|
|
21
37
|
#toolsExpanded = false;
|
|
22
38
|
#thinkingVisible = true;
|
|
@@ -34,8 +50,12 @@ export class OwnedUiSessionShellRoot {
|
|
|
34
50
|
constructor(view, cwd, handlers, startup = {}, agentDir, extensionRenderers = {
|
|
35
51
|
getMessageRenderer: () => undefined,
|
|
36
52
|
getToolDefinition: () => undefined,
|
|
37
|
-
}) {
|
|
53
|
+
}, sessionLayout = "pinned") {
|
|
38
54
|
this.#view = view;
|
|
55
|
+
this.#customViewport = sessionLayout === "custom-viewport";
|
|
56
|
+
this.#submittedPromptComposer = this.#customViewport
|
|
57
|
+
? { layout: submittedPromptLayout, compose: composeSubmittedPromptRows }
|
|
58
|
+
: undefined;
|
|
39
59
|
this.#cwd = cwd;
|
|
40
60
|
this.#extensionRenderers = extensionRenderers;
|
|
41
61
|
this.#componentRuntime = handlers;
|
|
@@ -43,7 +63,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
43
63
|
this.resources = createPiShellLoadedResources(startup.resources ?? [], startup.expanded ?? false);
|
|
44
64
|
this.#status = createPiShellStatus(view, handlers);
|
|
45
65
|
this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd);
|
|
46
|
-
this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions);
|
|
66
|
+
this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned");
|
|
47
67
|
this.editor = createPiShellEditor({
|
|
48
68
|
...handlers,
|
|
49
69
|
cwd,
|
|
@@ -59,9 +79,16 @@ export class OwnedUiSessionShellRoot {
|
|
|
59
79
|
this.#syncTranscript(view.transcript);
|
|
60
80
|
// Colours come from the active theme, so rendered rows outlive their revision only
|
|
61
81
|
// until the theme under them changes.
|
|
62
|
-
this.#themeUnsubscribe = onPiThemeChange(() =>
|
|
82
|
+
this.#themeUnsubscribe = onPiThemeChange(() => {
|
|
83
|
+
this.#renderedRows.clear();
|
|
84
|
+
this.#documentLayouts.clear();
|
|
85
|
+
});
|
|
63
86
|
}
|
|
64
87
|
update(view) {
|
|
88
|
+
if (view.diagnostics.length !== this.#view.diagnostics.length
|
|
89
|
+
|| view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
|
|
90
|
+
this.#documentLayouts.clear();
|
|
91
|
+
}
|
|
65
92
|
this.#view = view;
|
|
66
93
|
this.#status.update(view);
|
|
67
94
|
this.#footer.update(this.#viewWithExtensionStatuses(view));
|
|
@@ -80,7 +107,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
80
107
|
this.#blocksById.set(block.id, block);
|
|
81
108
|
const component = this.#transcript.get(block.id);
|
|
82
109
|
if (component === undefined) {
|
|
83
|
-
const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers);
|
|
110
|
+
const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer);
|
|
84
111
|
created.setExpanded(this.#toolsExpanded);
|
|
85
112
|
this.#transcript.set(block.id, created);
|
|
86
113
|
this.#transcriptOrder.push(block.id);
|
|
@@ -91,18 +118,307 @@ export class OwnedUiSessionShellRoot {
|
|
|
91
118
|
// One chunk touches one block, and the updated component tracks its own dirtiness.
|
|
92
119
|
// Invalidating the whole shell here would re-wrap the entire transcript per chunk.
|
|
93
120
|
this.#renderedRows.delete(block.id);
|
|
121
|
+
this.#documentLayouts.clear();
|
|
94
122
|
}
|
|
95
123
|
render(width) {
|
|
124
|
+
if (!this.#customViewport)
|
|
125
|
+
return [...this.#renderDocument(width), ...this.#renderDock(width)];
|
|
126
|
+
const height = Math.max(0, this.#componentRuntime.getRows());
|
|
127
|
+
const dock = this.#renderDockLayout(width);
|
|
128
|
+
const availableWithoutStatus = Math.max(0, height - Math.min(height, dock.rowsWithoutStatus.length));
|
|
129
|
+
// Ordinary transcript content uses the full terminal width. The rail is an
|
|
130
|
+
// overlay, not a lost wrapping cell. Submitted prompts alone retain the
|
|
131
|
+
// intentional final gutter after their right-aligned timestamp.
|
|
132
|
+
const documentWidth = width;
|
|
133
|
+
let document = this.#renderDocumentLayout(documentWidth);
|
|
134
|
+
const statusRows = dock.statusRows;
|
|
135
|
+
// v2 pins Working only while the complete transcript still fits. Once the
|
|
136
|
+
// transcript overflows, status joins the scrollable document tail so it
|
|
137
|
+
// naturally leaves the screen when the user scrolls up.
|
|
138
|
+
// Pi presents pending messages before Working. Keep status in the dock while
|
|
139
|
+
// a queue exists so moving overflowing status into the document cannot
|
|
140
|
+
// reverse that semantic order; it returns to v2 scrolling once the queue clears.
|
|
141
|
+
const pinStatus = this.#view.editor.queuedSubmissions.length > 0
|
|
142
|
+
|| document.rows.length + statusRows.length <= availableWithoutStatus;
|
|
143
|
+
const dockRows = pinStatus ? dock.rows : dock.rowsWithoutStatus;
|
|
144
|
+
if (!pinStatus)
|
|
145
|
+
document = { ...document, rows: [...document.rows, ...statusRows] };
|
|
146
|
+
this.#viewport.setConfig(this.#viewportConfig);
|
|
147
|
+
return this.#viewport.compose({
|
|
148
|
+
documentRows: document.rows,
|
|
149
|
+
dockRows,
|
|
150
|
+
promptAnchors: document.promptAnchors,
|
|
151
|
+
width,
|
|
152
|
+
height,
|
|
153
|
+
// Anchor above the stable editor/widget/footer group. Queued input,
|
|
154
|
+
// working states, and transient notifications may grow above it without
|
|
155
|
+
// moving the floating control up and down the terminal.
|
|
156
|
+
bottomControlRow: Math.max(0, height - Math.min(height, dock.stableBottomRows) - 1),
|
|
157
|
+
theme: {
|
|
158
|
+
// Match the v2 rail: a continuously dim track and an accent thumb at
|
|
159
|
+
// rest, thickening on hover/drag through the presentation glyph. Reset
|
|
160
|
+
// inherited text decorations so dim transcript rows cannot dull the
|
|
161
|
+
// thumb; the row background deliberately remains intact.
|
|
162
|
+
track: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("dim", text)}`,
|
|
163
|
+
thumb: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("accent", text)}`,
|
|
164
|
+
sticky: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", withoutTerminalBackground(text))),
|
|
165
|
+
quietSticky: text => `\u001b[2m${text.replace(/\u001b\[(?:0|22)m/g, "$&\u001b[2m")}\u001b[22m`,
|
|
166
|
+
bottomControl: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", text)),
|
|
167
|
+
// Selection paints only the familiar dark-blue background. Source
|
|
168
|
+
// foreground roles (including links, warnings, muted text, and bold
|
|
169
|
+
// intensity) remain exactly as rendered instead of being inverted.
|
|
170
|
+
selection: (line, from, to) => backgroundSgrSpan(line, from, to, "\u001b[48;2;38;79;120m", "\u001b[49m"),
|
|
171
|
+
},
|
|
172
|
+
}).rows;
|
|
173
|
+
}
|
|
174
|
+
setViewportConfig(config) {
|
|
175
|
+
if (config.scrollbarAppearance !== this.#viewportConfig.scrollbarAppearance)
|
|
176
|
+
this.#documentLayouts.clear();
|
|
177
|
+
this.#viewportConfig = config;
|
|
178
|
+
this.#viewport.setConfig(config);
|
|
179
|
+
this.#componentRuntime.requestRender();
|
|
180
|
+
}
|
|
181
|
+
resumeViewportFollowing() {
|
|
182
|
+
if (!this.#customViewport)
|
|
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();
|
|
191
|
+
}
|
|
192
|
+
noteCompletedAssistantMessage() {
|
|
193
|
+
if (!this.#customViewport || !this.#viewport.noteNewMessage())
|
|
194
|
+
return;
|
|
195
|
+
this.#componentRuntime.requestRender();
|
|
196
|
+
}
|
|
197
|
+
resetViewport() {
|
|
198
|
+
if (this.#viewportActivityTimer !== undefined)
|
|
199
|
+
clearTimeout(this.#viewportActivityTimer);
|
|
200
|
+
this.#viewportActivityTimer = undefined;
|
|
201
|
+
this.#stopSelectionAutoScroll();
|
|
202
|
+
this.#viewport.reset();
|
|
203
|
+
}
|
|
204
|
+
clearViewportPointerState() {
|
|
205
|
+
if (this.#viewportActivityTimer !== undefined)
|
|
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();
|
|
213
|
+
}
|
|
214
|
+
handleViewportPreInput(data, allowWheel = true, now = Date.now()) {
|
|
215
|
+
if (!this.#customViewport || this.#viewport.frame === null)
|
|
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?.();
|
|
405
|
+
}
|
|
406
|
+
#renderDock(width) {
|
|
407
|
+
return this.#renderDockLayout(width).rows;
|
|
408
|
+
}
|
|
409
|
+
#renderDockLayout(width) {
|
|
96
410
|
const queued = this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
...
|
|
104
|
-
...
|
|
105
|
-
|
|
411
|
+
const statusRows = this.#renderStatus(width);
|
|
412
|
+
const aboveWidgets = this.#renderWidgets("aboveEditor", width);
|
|
413
|
+
const input = this.#inputSurface.render(width);
|
|
414
|
+
const belowWidgets = this.#renderWidgets("belowEditor", width);
|
|
415
|
+
const footer = this.#renderFooter(width);
|
|
416
|
+
return {
|
|
417
|
+
rows: [...queued, ...statusRows, ...aboveWidgets, ...input, ...belowWidgets, ...footer],
|
|
418
|
+
rowsWithoutStatus: [...queued, ...aboveWidgets, ...input, ...belowWidgets, ...footer],
|
|
419
|
+
statusRows,
|
|
420
|
+
stableBottomRows: aboveWidgets.length + input.length + belowWidgets.length + footer.length,
|
|
421
|
+
};
|
|
106
422
|
}
|
|
107
423
|
layoutRoot() {
|
|
108
424
|
const document = layoutPort(width => this.#renderDocument(width), () => this.invalidate());
|
|
@@ -154,23 +470,66 @@ export class OwnedUiSessionShellRoot {
|
|
|
154
470
|
};
|
|
155
471
|
}
|
|
156
472
|
#renderDocument(width) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return
|
|
165
|
-
});
|
|
473
|
+
return this.#renderDocumentLayout(width).rows;
|
|
474
|
+
}
|
|
475
|
+
#renderDocumentLayout(width) {
|
|
476
|
+
// Extension headers may animate independently of transcript revisions, so
|
|
477
|
+
// only the stable built-in document participates in this frame cache.
|
|
478
|
+
const cached = this.#extensionHeader === null ? this.#documentLayouts.get(width) : undefined;
|
|
479
|
+
if (cached !== undefined)
|
|
480
|
+
return cached;
|
|
166
481
|
const diagnostics = this.#view.diagnostics;
|
|
167
482
|
const startupRows = diagnostics
|
|
168
483
|
.filter(diagnostic => diagnostic.code === "engine-startup")
|
|
169
484
|
.flatMap(diagnostic => renderPiShellStartupDiagnostic(diagnostic, width));
|
|
170
|
-
const
|
|
485
|
+
const resourceRows = this.#customViewport ? [] : [...this.resources.render(width)];
|
|
486
|
+
if (resourceRows.at(-1) === "")
|
|
487
|
+
resourceRows.pop();
|
|
488
|
+
const headerRows = this.#extensionHeader !== null
|
|
489
|
+
? this.#extensionHeader.render(width)
|
|
490
|
+
: this.#customViewport ? [] : this.header.render(width);
|
|
491
|
+
const rows = [
|
|
492
|
+
...startupRows,
|
|
493
|
+
...headerRows,
|
|
494
|
+
...resourceRows,
|
|
495
|
+
];
|
|
496
|
+
const promptAnchors = [];
|
|
497
|
+
for (let index = 0; index < this.#transcriptOrder.length; index += 1) {
|
|
498
|
+
const id = this.#transcriptOrder[index];
|
|
499
|
+
const block = this.#blocksById.get(id);
|
|
500
|
+
if (!this.#thinkingVisible && block?.kind === "thinking")
|
|
501
|
+
continue;
|
|
502
|
+
const blockWidth = this.#customViewport
|
|
503
|
+
&& block?.kind === "user"
|
|
504
|
+
&& this.#viewportConfig.scrollbarAppearance !== "hidden"
|
|
505
|
+
&& width > 1
|
|
506
|
+
? width - 1
|
|
507
|
+
: width;
|
|
508
|
+
const blockRows = this.#blockRows(id, block, blockWidth);
|
|
509
|
+
if (block?.kind === "user") {
|
|
510
|
+
// The first natural prompt gets one breathing row at the document top.
|
|
511
|
+
// Once scrolling advances, the prompt itself reaches row zero and then
|
|
512
|
+
// becomes sticky there, so the spacer is never pinned with it.
|
|
513
|
+
if (this.#customViewport && index === 0)
|
|
514
|
+
rows.push("");
|
|
515
|
+
else if (index > 0)
|
|
516
|
+
rows.push("");
|
|
517
|
+
}
|
|
518
|
+
const firstRow = rows.length;
|
|
519
|
+
rows.push(...blockRows);
|
|
520
|
+
if (block?.kind === "user" && blockRows[0] !== undefined) {
|
|
521
|
+
promptAnchors.push({
|
|
522
|
+
id: block.id,
|
|
523
|
+
firstRow,
|
|
524
|
+
lastRow: Math.max(firstRow, rows.length - 1),
|
|
525
|
+
sourceRow: pinnedPromptSourceRow(block, blockRows[0], blockWidth),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
rows.push(...diagnostics
|
|
171
530
|
.filter(diagnostic => diagnostic.code === "package-updates")
|
|
172
|
-
.flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width));
|
|
173
|
-
|
|
531
|
+
.flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
|
|
532
|
+
rows.push(...diagnostics
|
|
174
533
|
.filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "package-updates")
|
|
175
534
|
.slice(-3)
|
|
176
535
|
.flatMap(diagnostic => renderPiShellTranscriptBlock({
|
|
@@ -181,18 +540,15 @@ export class OwnedUiSessionShellRoot {
|
|
|
181
540
|
title: diagnostic.code,
|
|
182
541
|
text: diagnostic.message,
|
|
183
542
|
payload: {},
|
|
184
|
-
}, width, this.#cwd));
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
...packageUpdateRows,
|
|
194
|
-
...diagnosticRows,
|
|
195
|
-
];
|
|
543
|
+
}, width, this.#cwd)));
|
|
544
|
+
const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors) };
|
|
545
|
+
if (this.#extensionHeader === null) {
|
|
546
|
+
this.#documentLayouts.set(width, layout);
|
|
547
|
+
// The custom viewport commonly probes full width and reserved-rail width.
|
|
548
|
+
while (this.#documentLayouts.size > 2)
|
|
549
|
+
this.#documentLayouts.delete(this.#documentLayouts.keys().next().value);
|
|
550
|
+
}
|
|
551
|
+
return layout;
|
|
196
552
|
}
|
|
197
553
|
/**
|
|
198
554
|
* Rows for one transcript block. A finalized block renders once for a given revision
|
|
@@ -206,11 +562,20 @@ export class OwnedUiSessionShellRoot {
|
|
|
206
562
|
return [];
|
|
207
563
|
if (block === undefined || block.status !== "finalized")
|
|
208
564
|
return component.render(width);
|
|
209
|
-
|
|
210
|
-
|
|
565
|
+
let byWidth = this.#renderedRows.get(id);
|
|
566
|
+
const cached = byWidth?.get(width);
|
|
567
|
+
if (cached?.revision === block.revision)
|
|
211
568
|
return cached.rows;
|
|
212
569
|
const rows = component.render(width);
|
|
213
|
-
|
|
570
|
+
if (byWidth === undefined) {
|
|
571
|
+
byWidth = new Map();
|
|
572
|
+
this.#renderedRows.set(id, byWidth);
|
|
573
|
+
}
|
|
574
|
+
byWidth.set(width, { revision: block.revision, rows });
|
|
575
|
+
// Bare A1 probes full width before reserving the overflowing rail column;
|
|
576
|
+
// retain both widths without turning resize history into an unbounded cache.
|
|
577
|
+
while (byWidth.size > 2)
|
|
578
|
+
byWidth.delete(byWidth.keys().next().value);
|
|
214
579
|
return rows;
|
|
215
580
|
}
|
|
216
581
|
transcriptComponent(id) {
|
|
@@ -222,6 +587,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
222
587
|
&& this.#transcriptOrder.at(-1) === previousId
|
|
223
588
|
&& this.#workflowStatusAnchors.get(previousId) === this.#view.transcript.length) {
|
|
224
589
|
this.#workflowStatusMessages.set(previousId, message);
|
|
590
|
+
this.#documentLayouts.clear();
|
|
225
591
|
this.invalidate();
|
|
226
592
|
return;
|
|
227
593
|
}
|
|
@@ -313,6 +679,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
313
679
|
}
|
|
314
680
|
toggleThinkingVisibility() {
|
|
315
681
|
this.#thinkingVisible = !this.#thinkingVisible;
|
|
682
|
+
this.#documentLayouts.clear();
|
|
316
683
|
this.invalidate();
|
|
317
684
|
}
|
|
318
685
|
get toolsExpanded() {
|
|
@@ -335,6 +702,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
335
702
|
if (this.#extensionHeader !== component)
|
|
336
703
|
this.#extensionHeader?.dispose?.();
|
|
337
704
|
this.#extensionHeader = component;
|
|
705
|
+
this.#documentLayouts.clear();
|
|
338
706
|
this.invalidate();
|
|
339
707
|
}
|
|
340
708
|
setExtensionFooter(component) {
|
|
@@ -366,6 +734,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
366
734
|
this.#workflowStatusMessages.clear();
|
|
367
735
|
this.#transcriptOrder = this.#transcriptOrder.filter(id => !id.startsWith("workflow-status-"));
|
|
368
736
|
this.#lastWorkflowStatusId = undefined;
|
|
737
|
+
this.#documentLayouts.clear();
|
|
369
738
|
this.invalidate();
|
|
370
739
|
}
|
|
371
740
|
resetExtensionUi() {
|
|
@@ -382,6 +751,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
382
751
|
this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
|
|
383
752
|
// An extension renderer may have drawn transcript blocks that are now unrendered by it.
|
|
384
753
|
this.#renderedRows.clear();
|
|
754
|
+
this.#documentLayouts.clear();
|
|
385
755
|
this.invalidate();
|
|
386
756
|
}
|
|
387
757
|
addExtensionNotification(message, type) {
|
|
@@ -401,6 +771,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
401
771
|
onBranchChange: () => () => { },
|
|
402
772
|
};
|
|
403
773
|
}
|
|
774
|
+
usesDefaultInputSurface() {
|
|
775
|
+
return this.#inputSurface === this.editor;
|
|
776
|
+
}
|
|
404
777
|
setInputSurface(component, disposePrevious = true) {
|
|
405
778
|
const next = component ?? this.editor;
|
|
406
779
|
if (next === this.#inputSurface)
|
|
@@ -432,6 +805,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
432
805
|
this.#inputSurface.setFocused?.(focused);
|
|
433
806
|
}
|
|
434
807
|
dispose() {
|
|
808
|
+
this.clearViewportPointerState();
|
|
435
809
|
this.#themeUnsubscribe();
|
|
436
810
|
this.header.dispose?.();
|
|
437
811
|
this.resources.dispose?.();
|
|
@@ -439,6 +813,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
439
813
|
component.dispose?.();
|
|
440
814
|
this.#transcript.clear();
|
|
441
815
|
this.#renderedRows.clear();
|
|
816
|
+
this.#documentLayouts.clear();
|
|
442
817
|
if (this.#inputSurface !== this.editor)
|
|
443
818
|
this.#inputSurface.dispose?.();
|
|
444
819
|
this.#extensionHeader?.dispose?.();
|
|
@@ -452,6 +827,13 @@ export class OwnedUiSessionShellRoot {
|
|
|
452
827
|
this.#queued.dispose?.();
|
|
453
828
|
}
|
|
454
829
|
#syncTranscript(blocks) {
|
|
830
|
+
const previousIds = this.#transcriptOrder.filter(id => !id.startsWith("workflow-status-"));
|
|
831
|
+
const transcriptChanged = previousIds.length !== blocks.length || blocks.some((block, index) => {
|
|
832
|
+
const previous = this.#blocksById.get(block.id);
|
|
833
|
+
return previousIds[index] !== block.id || previous?.revision !== block.revision;
|
|
834
|
+
});
|
|
835
|
+
if (transcriptChanged)
|
|
836
|
+
this.#documentLayouts.clear();
|
|
455
837
|
this.#blocksById.clear();
|
|
456
838
|
for (const block of blocks)
|
|
457
839
|
this.#blocksById.set(block.id, block);
|
|
@@ -466,7 +848,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
466
848
|
for (const block of blocks) {
|
|
467
849
|
const component = this.#transcript.get(block.id);
|
|
468
850
|
if (component === undefined) {
|
|
469
|
-
const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers);
|
|
851
|
+
const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer);
|
|
470
852
|
created.setExpanded(this.#toolsExpanded);
|
|
471
853
|
this.#transcript.set(block.id, created);
|
|
472
854
|
}
|
|
@@ -508,6 +890,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
508
890
|
this.#transcript.set(id, component);
|
|
509
891
|
this.#workflowStatusAnchors.set(id, this.#view.transcript.length);
|
|
510
892
|
this.#transcriptOrder.push(id);
|
|
893
|
+
this.#documentLayouts.clear();
|
|
511
894
|
this.invalidate();
|
|
512
895
|
return id;
|
|
513
896
|
}
|
|
@@ -555,9 +938,35 @@ export class OwnedUiSessionShellRoot {
|
|
|
555
938
|
component.setExpanded(expanded);
|
|
556
939
|
// Expansion changes what a block draws without changing its revision.
|
|
557
940
|
this.#renderedRows.clear();
|
|
941
|
+
this.#documentLayouts.clear();
|
|
558
942
|
this.invalidate();
|
|
559
943
|
}
|
|
560
944
|
}
|
|
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
|
+
const SCROLLBAR_CELL_RESET = "\u001b[22;23;24;25;27;28;29;39;54;55m";
|
|
949
|
+
const TERMINAL_BACKGROUND = /\u001b\[(?:4[0-9]|10[0-7]|48(?:[;:][0-9;:]*)?)m/g;
|
|
950
|
+
/** Repaint a source prompt with viewport chrome while preserving text, links, and foreground roles. */
|
|
951
|
+
function withoutTerminalBackground(text) {
|
|
952
|
+
return text.replace(TERMINAL_BACKGROUND, "");
|
|
953
|
+
}
|
|
954
|
+
/** A pinned timestamp is content now, not secondary transcript metadata. */
|
|
955
|
+
function pinnedPromptSourceRow(block, sourceRow, width) {
|
|
956
|
+
if (typeof block.payload !== "object" || block.payload === null)
|
|
957
|
+
return sourceRow;
|
|
958
|
+
const value = block.payload.timestamp;
|
|
959
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
960
|
+
return sourceRow;
|
|
961
|
+
const timestamp = formatSubmittedPromptTime(value);
|
|
962
|
+
if (timestamp === null || submittedPromptLayout(width, value).timestamp === null)
|
|
963
|
+
return sourceRow;
|
|
964
|
+
const rowWidth = displayWidth(sourceRow);
|
|
965
|
+
const timestampWidth = displayWidth(timestamp);
|
|
966
|
+
if (rowWidth < timestampWidth)
|
|
967
|
+
return sourceRow;
|
|
968
|
+
return overlaySpan(sourceRow, rowWidth - timestampWidth, rowWidth, piTheme().fg("userMessageText", timestamp));
|
|
969
|
+
}
|
|
561
970
|
function layoutPort(render, invalidate, handleInput) {
|
|
562
971
|
return {
|
|
563
972
|
render,
|