@timurproko/a1 0.1.8-dev.139 → 0.1.8-dev.157

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +21 -9
  2. package/bin/cli.js +1 -0
  3. package/dist/cli/dispatch.d.ts +6 -0
  4. package/dist/cli/dispatch.js +107 -81
  5. package/dist/cli/version-stats.js +7 -1
  6. package/dist/composition/owned-ui.js +10 -7
  7. package/dist/composition/settings-route-host.d.ts +1 -1
  8. package/dist/composition/settings-route-host.js +1 -1
  9. package/dist/contracts/owned-ui/model.d.ts +8 -1
  10. package/dist/contracts/owned-ui/validation.js +11 -0
  11. package/dist/features/owned-ui/settings-app.js +2 -1
  12. package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
  13. package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
  14. package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
  15. package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
  16. package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
  17. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
  18. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
  19. package/dist/integrations/pi/engine/adapter.js +5 -1
  20. package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
  21. package/dist/integrations/pi/engine/session-integration.js +14 -3
  22. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
  23. package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
  24. package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
  25. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +17 -3
  26. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +100 -260
  27. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
  28. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +52 -12
  29. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
  30. package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
  31. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
  32. package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
  33. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -6
  34. package/dist/native/darwin-arm64/manifest.json +1 -1
  35. package/dist/native/linux-x64/manifest.json +3 -3
  36. package/dist/native/linux-x64/process-guardian +0 -0
  37. package/dist/native/win32-x64/manifest.json +3 -3
  38. package/dist/native/win32-x64/process-guardian.exe +0 -0
  39. package/dist/ui/apps/contracts.d.ts +23 -1
  40. package/dist/ui/apps/host.js +9 -1
  41. package/dist/ui/components/scrollbar.d.ts +2 -2
  42. package/dist/ui/components/scrollbar.js +3 -3
  43. package/dist/ui/components/spans.d.ts +16 -1
  44. package/dist/ui/components/spans.js +144 -2
  45. package/dist/ui/components/text.js +24 -2
  46. package/dist/ui/components/transcript-viewport.d.ts +6 -0
  47. package/dist/ui/components/transcript-viewport.js +36 -8
  48. package/dist/ui/settings/declarations.d.ts +1 -1
  49. package/dist/ui/settings/declarations.js +3 -3
  50. package/dist/ui/settings/migrations.js +9 -0
  51. package/docs/architecture/boundaries.md +1 -1
  52. package/docs/architecture/project-structure.md +1 -1
  53. package/docs/ci-release-runbook.md +6 -5
  54. package/docs/features/launch-profiles.md +3 -3
  55. package/package.json +4 -1
  56. package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
  57. /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
  58. /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
  59. /package/dist/integrations/pi/{owned-ui → session-ui}/route-host.js +0 -0
@@ -1,7 +1,9 @@
1
- import { TranscriptViewport, backgroundSgrSpan, composeSubmittedPromptRows, displayWidth, formatSubmittedPromptTime, overlaySpan, submittedPromptLayout, routeMouseInput, scrollForTrackPage, scrollForThumbRow, scrollbarSelectionRows, scrollbarWheelRows, } from "../../../ui/components/index.js";
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
- #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;
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
- ? { layout: submittedPromptLayout, compose: composeSubmittedPromptRows }
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 availableWithoutStatus = Math.max(0, height - Math.min(height, dock.rowsWithoutStatus.length));
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
- 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({
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 (config.scrollbarAppearance !== this.#viewportConfig.scrollbarAppearance)
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
- 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();
220
+ this.#viewportController.resumeFollowing();
191
221
  }
192
222
  noteCompletedAssistantMessage() {
193
- if (!this.#customViewport || !this.#viewport.noteNewMessage())
194
- return;
195
- this.#componentRuntime.requestRender();
223
+ this.#viewportController.noteCompletedAssistantMessage();
196
224
  }
197
225
  resetViewport() {
198
- if (this.#viewportActivityTimer !== undefined)
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
- 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();
229
+ this.#viewportController.clearPointerState();
213
230
  }
214
231
  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?.();
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: [...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,
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.#viewportConfig.scrollbarAppearance !== "hidden"
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 component.render(width);
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 = component.render(width);
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: () => runtime?.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: "hover",
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 input = text.trim();
186
- if (!input)
207
+ const displayInput = text.trim();
208
+ if (!displayInput)
187
209
  return { outcome: "completed", diagnostic: null };
188
- if (input.startsWith("/"))
189
- return this.#slashCommand(input);
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(input);
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(input);
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(input);
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
- return { outcome: "rejected", diagnostic: "nothing to interrupt" };
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"));