@timurproko/a1 0.1.8-dev.137 → 0.1.8-dev.151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/capabilities.d.ts +2 -11
  3. package/dist/cli/capabilities.js +1 -1
  4. package/dist/cli/dispatch.js +3 -6
  5. package/dist/composition/owned-ui.js +26 -5
  6. package/dist/composition/settings-route-host.d.ts +1 -1
  7. package/dist/composition/settings-route-host.js +1 -1
  8. package/dist/contracts/owned-ui/model.d.ts +28 -0
  9. package/dist/contracts/owned-ui/validation.js +14 -0
  10. package/dist/features/launch/development-launch.js +11 -3
  11. package/dist/features/launch/index.d.ts +0 -1
  12. package/dist/features/launch/index.js +0 -1
  13. package/dist/features/launch/intent.d.ts +3 -2
  14. package/dist/features/launch/intent.js +1 -2
  15. package/dist/features/launch/prepare-launch.d.ts +0 -2
  16. package/dist/features/launch/prepare-launch.js +2 -9
  17. package/dist/features/launch/profile-paths.d.ts +1 -2
  18. package/dist/features/launch/profile-paths.js +2 -8
  19. package/dist/features/launch/runtime-selection.d.ts +2 -5
  20. package/dist/features/owned-ui/settings-app.js +38 -14
  21. package/dist/foundation/lifecycle/model.d.ts +1 -1
  22. package/dist/foundation/lifecycle/model.js +1 -1
  23. package/dist/foundation/release/bootstrap.d.ts +1 -3
  24. package/dist/foundation/release/bootstrap.js +1 -1
  25. package/dist/foundation/storage/control-store.js +37 -3
  26. package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
  27. package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
  28. package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
  29. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  30. package/dist/integrations/pi/components/shell-footer-status.js +14 -5
  31. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -2
  32. package/dist/integrations/pi/components/shell-presenters-transcript.js +9 -8
  33. package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
  34. package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
  35. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +13 -0
  36. package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
  37. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
  38. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
  39. package/dist/integrations/pi/engine/adapter.js +21 -3
  40. package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
  41. package/dist/integrations/pi/engine/session-integration.js +17 -3
  42. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
  43. package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
  44. package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
  45. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +34 -5
  46. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +294 -45
  47. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
  48. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +167 -24
  49. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
  50. package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
  51. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
  52. package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
  53. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
  54. package/dist/integrations/pi/tui-runtime/adapter.js +51 -2
  55. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -6
  56. package/dist/native/darwin-arm64/manifest.json +1 -1
  57. package/dist/native/linux-x64/manifest.json +1 -1
  58. package/dist/native/win32-x64/manifest.json +3 -3
  59. package/dist/native/win32-x64/process-guardian.exe +0 -0
  60. package/dist/product-identity.d.ts +2 -2
  61. package/dist/product-identity.js +3 -4
  62. package/dist/product-identity.json +3 -5
  63. package/dist/ui/apps/contracts.d.ts +23 -1
  64. package/dist/ui/apps/host.js +9 -1
  65. package/dist/ui/components/index.d.ts +3 -0
  66. package/dist/ui/components/index.js +3 -0
  67. package/dist/ui/components/mouse.d.ts +10 -0
  68. package/dist/ui/components/mouse.js +23 -0
  69. package/dist/ui/components/prompt-row.d.ts +14 -0
  70. package/dist/ui/components/prompt-row.js +20 -0
  71. package/dist/ui/components/scrollbar.d.ts +26 -0
  72. package/dist/ui/components/scrollbar.js +35 -0
  73. package/dist/ui/components/spans.d.ts +21 -0
  74. package/dist/ui/components/spans.js +178 -0
  75. package/dist/ui/components/submitted-prompt.d.ts +23 -0
  76. package/dist/ui/components/submitted-prompt.js +50 -0
  77. package/dist/ui/components/text-selection.d.ts +53 -0
  78. package/dist/ui/components/text-selection.js +168 -0
  79. package/dist/ui/components/text.d.ts +14 -0
  80. package/dist/ui/components/text.js +40 -0
  81. package/dist/ui/components/transcript-viewport.d.ts +104 -0
  82. package/dist/ui/components/transcript-viewport.js +381 -0
  83. package/dist/ui/settings/declarations.d.ts +8 -1
  84. package/dist/ui/settings/declarations.js +33 -5
  85. package/dist/ui/settings/migrations.d.ts +1 -1
  86. package/dist/ui/settings/migrations.js +21 -2
  87. package/dist/ui/settings/sections.js +15 -8
  88. package/dist/ui/settings/session.d.ts +2 -0
  89. package/dist/ui/settings/session.js +7 -5
  90. package/docs/architecture/boundaries.md +3 -3
  91. package/docs/architecture/project-structure.md +2 -2
  92. package/docs/architecture/resource-and-data-policy.md +1 -1
  93. package/docs/architecture/toolchain.md +5 -5
  94. package/docs/architecture/ui-reference-provenance.md +11 -2
  95. package/docs/features/launch-profiles.md +7 -14
  96. package/docs/manual-launch-instance-acceptance.md +12 -35
  97. package/docs/manual-owned-ui-checkpoint.md +2 -3
  98. package/docs/manual-pi-boundary-candidate.md +4 -5
  99. package/package.json +4 -2
  100. package/dist/features/launch/profiles.d.ts +0 -12
  101. package/dist/features/launch/profiles.js +0 -26
  102. package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
  103. /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
  104. /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
  105. /package/dist/integrations/pi/{owned-ui → session-ui}/route-host.js +0 -0
@@ -1,7 +1,9 @@
1
- import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } 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;
@@ -10,6 +12,8 @@ export class OwnedUiSessionShellRoot {
10
12
  #transcript = new Map();
11
13
  #blocksById = new Map();
12
14
  #renderedRows = new Map();
15
+ /** Flattened transcript rows reused by wheel, rail, jump, and selection frames. */
16
+ #documentLayouts = new Map();
13
17
  #themeUnsubscribe;
14
18
  #transcriptOrder = [];
15
19
  #view;
@@ -17,6 +21,10 @@ export class OwnedUiSessionShellRoot {
17
21
  #footer;
18
22
  #queued;
19
23
  #extensionRenderers;
24
+ #promptChips = new PromptChipStore();
25
+ #customViewport;
26
+ #submittedPromptComposer;
27
+ #viewportController;
20
28
  #componentRuntime;
21
29
  #toolsExpanded = false;
22
30
  #thinkingVisible = true;
@@ -34,8 +42,16 @@ export class OwnedUiSessionShellRoot {
34
42
  constructor(view, cwd, handlers, startup = {}, agentDir, extensionRenderers = {
35
43
  getMessageRenderer: () => undefined,
36
44
  getToolDefinition: () => undefined,
37
- }) {
45
+ }, sessionLayout = "pinned") {
38
46
  this.#view = view;
47
+ this.#customViewport = sessionLayout === "custom-viewport";
48
+ this.#submittedPromptComposer = this.#customViewport
49
+ ? {
50
+ layout: submittedPromptLayout,
51
+ compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, style)
52
+ .map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor)),
53
+ }
54
+ : undefined;
39
55
  this.#cwd = cwd;
40
56
  this.#extensionRenderers = extensionRenderers;
41
57
  this.#componentRuntime = handlers;
@@ -43,13 +59,42 @@ export class OwnedUiSessionShellRoot {
43
59
  this.resources = createPiShellLoadedResources(startup.resources ?? [], startup.expanded ?? false);
44
60
  this.#status = createPiShellStatus(view, handlers);
45
61
  this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd);
46
- this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions);
62
+ this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned");
47
63
  this.editor = createPiShellEditor({
48
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),
49
88
  cwd,
50
89
  ...(agentDir === undefined ? {} : { agentDir }),
51
90
  onToolsExpand: () => this.#setToolsExpanded(!this.#toolsExpanded),
52
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
+ });
53
98
  this.editor.setThinkingLevel(view.thinkingLevel);
54
99
  this.#inputSurface = this.editor;
55
100
  for (const block of view.transcript) {
@@ -59,9 +104,19 @@ export class OwnedUiSessionShellRoot {
59
104
  this.#syncTranscript(view.transcript);
60
105
  // Colours come from the active theme, so rendered rows outlive their revision only
61
106
  // until the theme under them changes.
62
- this.#themeUnsubscribe = onPiThemeChange(() => this.#renderedRows.clear());
107
+ this.#themeUnsubscribe = onPiThemeChange(() => {
108
+ this.#renderedRows.clear();
109
+ this.#documentLayouts.clear();
110
+ });
111
+ }
112
+ preparePromptSubmission(text) {
113
+ return this.#promptChips.prepareSubmission(text);
63
114
  }
64
115
  update(view) {
116
+ if (view.diagnostics.length !== this.#view.diagnostics.length
117
+ || view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
118
+ this.#documentLayouts.clear();
119
+ }
65
120
  this.#view = view;
66
121
  this.#status.update(view);
67
122
  this.#footer.update(this.#viewWithExtensionStatuses(view));
@@ -80,7 +135,7 @@ export class OwnedUiSessionShellRoot {
80
135
  this.#blocksById.set(block.id, block);
81
136
  const component = this.#transcript.get(block.id);
82
137
  if (component === undefined) {
83
- const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers);
138
+ const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer);
84
139
  created.setExpanded(this.#toolsExpanded);
85
140
  this.#transcript.set(block.id, created);
86
141
  this.#transcriptOrder.push(block.id);
@@ -91,18 +146,112 @@ export class OwnedUiSessionShellRoot {
91
146
  // One chunk touches one block, and the updated component tracks its own dirtiness.
92
147
  // Invalidating the whole shell here would re-wrap the entire transcript per chunk.
93
148
  this.#renderedRows.delete(block.id);
149
+ this.#documentLayouts.clear();
94
150
  }
95
151
  render(width) {
152
+ if (!this.#customViewport) {
153
+ this.#viewportController.setEditorPointerFrame(undefined);
154
+ return [...this.#renderDocument(width), ...this.#renderDock(width)];
155
+ }
156
+ const height = Math.max(0, this.#componentRuntime.getRows());
157
+ const dock = this.#renderDockLayout(width);
158
+ const availableWithoutTransient = Math.max(0, height - Math.min(height, dock.rowsWithoutTransient.length));
159
+ // Ordinary transcript content uses the full terminal width. The rail is an
160
+ // overlay, not a lost wrapping cell. Submitted prompts alone retain the
161
+ // intentional final gutter after their right-aligned timestamp.
162
+ const documentWidth = width;
163
+ let document = this.#renderDocumentLayout(documentWidth);
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({
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,
190
+ dockRows,
191
+ promptAnchors: document.promptAnchors,
192
+ width,
193
+ height,
194
+ // Anchor above the stable editor/widget/footer group. Queued input,
195
+ // working states, and transient notifications may grow above it without
196
+ // moving the floating control up and down the terminal.
197
+ bottomControlRow: Math.max(0, height - Math.min(height, dock.stableBottomRows) - 1),
198
+ theme: {
199
+ // Match the v2 rail: a continuously dim track and an accent thumb at
200
+ // rest, thickening on hover/drag through the presentation glyph. Reset
201
+ // inherited text decorations so dim transcript rows cannot dull the
202
+ // thumb; the row background deliberately remains intact.
203
+ track: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("dim", text)}`,
204
+ thumb: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("accent", text)}`,
205
+ sticky: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", withoutTerminalBackground(text))),
206
+ quietSticky: text => `\u001b[2m${text.replace(/\u001b\[(?:0|22)m/g, "$&\u001b[2m")}\u001b[22m`,
207
+ bottomControl: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", text)),
208
+ // Selection paints only the familiar dark-blue background. Source
209
+ // foreground roles (including links, warnings, muted text, and bold
210
+ // intensity) remain exactly as rendered instead of being inverted.
211
+ selection: (line, from, to) => backgroundSgrSpan(line, from, to, "\u001b[48;2;38;79;120m", "\u001b[49m"),
212
+ },
213
+ }).rows;
214
+ }
215
+ setViewportConfig(config) {
216
+ if (this.#viewportController.setConfig(config))
217
+ this.#documentLayouts.clear();
218
+ }
219
+ resumeViewportFollowing() {
220
+ this.#viewportController.resumeFollowing();
221
+ }
222
+ noteCompletedAssistantMessage() {
223
+ this.#viewportController.noteCompletedAssistantMessage();
224
+ }
225
+ resetViewport() {
226
+ this.#viewportController.reset();
227
+ }
228
+ clearViewportPointerState() {
229
+ this.#viewportController.clearPointerState();
230
+ }
231
+ handleViewportPreInput(data, allowWheel = true, now = Date.now()) {
232
+ return this.#viewportController.handlePreInput(data, allowWheel, now);
233
+ }
234
+ #renderDock(width) {
235
+ return this.#renderDockLayout(width).rows;
236
+ }
237
+ #renderDockLayout(width) {
96
238
  const queued = this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
97
- return [
98
- ...this.#renderDocument(width),
99
- ...queued,
100
- ...this.#renderStatus(width),
101
- ...this.#renderWidgets("aboveEditor", width),
102
- ...this.#inputSurface.render(width),
103
- ...this.#renderWidgets("belowEditor", width),
104
- ...this.#renderFooter(width),
105
- ];
239
+ const statusRows = this.#renderStatus(width);
240
+ const transientRows = [...queued, ...statusRows];
241
+ const aboveWidgets = this.#renderWidgets("aboveEditor", width);
242
+ const input = this.#inputSurface.render(width);
243
+ const belowWidgets = this.#renderWidgets("belowEditor", width);
244
+ const footer = this.#renderFooter(width);
245
+ const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
246
+ return {
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,
254
+ };
106
255
  }
107
256
  layoutRoot() {
108
257
  const document = layoutPort(width => this.#renderDocument(width), () => this.invalidate());
@@ -154,23 +303,66 @@ export class OwnedUiSessionShellRoot {
154
303
  };
155
304
  }
156
305
  #renderDocument(width) {
157
- const transcript = this.#transcriptOrder.flatMap((id, index) => {
158
- const block = this.#blocksById.get(id);
159
- if (!this.#thinkingVisible && block?.kind === "thinking")
160
- return [];
161
- const rows = this.#blockRows(id, block, width);
162
- if (index > 0 && block?.kind === "user")
163
- return ["", ...rows];
164
- return rows;
165
- });
306
+ return this.#renderDocumentLayout(width).rows;
307
+ }
308
+ #renderDocumentLayout(width) {
309
+ // Extension headers may animate independently of transcript revisions, so
310
+ // only the stable built-in document participates in this frame cache.
311
+ const cached = this.#extensionHeader === null ? this.#documentLayouts.get(width) : undefined;
312
+ if (cached !== undefined)
313
+ return cached;
166
314
  const diagnostics = this.#view.diagnostics;
167
315
  const startupRows = diagnostics
168
316
  .filter(diagnostic => diagnostic.code === "engine-startup")
169
317
  .flatMap(diagnostic => renderPiShellStartupDiagnostic(diagnostic, width));
170
- const packageUpdateRows = diagnostics
318
+ const resourceRows = this.#customViewport ? [] : [...this.resources.render(width)];
319
+ if (resourceRows.at(-1) === "")
320
+ resourceRows.pop();
321
+ const headerRows = this.#extensionHeader !== null
322
+ ? this.#extensionHeader.render(width)
323
+ : this.#customViewport ? [] : this.header.render(width);
324
+ const rows = [
325
+ ...startupRows,
326
+ ...headerRows,
327
+ ...resourceRows,
328
+ ];
329
+ const promptAnchors = [];
330
+ for (let index = 0; index < this.#transcriptOrder.length; index += 1) {
331
+ const id = this.#transcriptOrder[index];
332
+ const block = this.#blocksById.get(id);
333
+ if (!this.#thinkingVisible && block?.kind === "thinking")
334
+ continue;
335
+ const blockWidth = this.#customViewport
336
+ && block?.kind === "user"
337
+ && this.#viewportController.config.scrollbarAppearance !== "hidden"
338
+ && width > 1
339
+ ? width - 1
340
+ : width;
341
+ const blockRows = this.#blockRows(id, block, blockWidth);
342
+ if (block?.kind === "user") {
343
+ // The first natural prompt gets one breathing row at the document top.
344
+ // Once scrolling advances, the prompt itself reaches row zero and then
345
+ // becomes sticky there, so the spacer is never pinned with it.
346
+ if (this.#customViewport && index === 0)
347
+ rows.push("");
348
+ else if (index > 0)
349
+ rows.push("");
350
+ }
351
+ const firstRow = rows.length;
352
+ rows.push(...blockRows);
353
+ if (block?.kind === "user" && blockRows[0] !== undefined) {
354
+ promptAnchors.push({
355
+ id: block.id,
356
+ firstRow,
357
+ lastRow: Math.max(firstRow, rows.length - 1),
358
+ sourceRow: pinnedPromptSourceRow(block, blockRows[0], blockWidth),
359
+ });
360
+ }
361
+ }
362
+ rows.push(...diagnostics
171
363
  .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
- const diagnosticRows = diagnostics
364
+ .flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
365
+ rows.push(...diagnostics
174
366
  .filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "package-updates")
175
367
  .slice(-3)
176
368
  .flatMap(diagnostic => renderPiShellTranscriptBlock({
@@ -181,18 +373,15 @@ export class OwnedUiSessionShellRoot {
181
373
  title: diagnostic.code,
182
374
  text: diagnostic.message,
183
375
  payload: {},
184
- }, width, this.#cwd));
185
- const resourceRows = [...this.resources.render(width)];
186
- if (resourceRows.at(-1) === "")
187
- resourceRows.pop();
188
- return [
189
- ...startupRows,
190
- ...(this.#extensionHeader ?? this.header).render(width),
191
- ...resourceRows,
192
- ...transcript,
193
- ...packageUpdateRows,
194
- ...diagnosticRows,
195
- ];
376
+ }, width, this.#cwd)));
377
+ const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors) };
378
+ if (this.#extensionHeader === null) {
379
+ this.#documentLayouts.set(width, layout);
380
+ // The custom viewport commonly probes full width and reserved-rail width.
381
+ while (this.#documentLayouts.size > 2)
382
+ this.#documentLayouts.delete(this.#documentLayouts.keys().next().value);
383
+ }
384
+ return layout;
196
385
  }
197
386
  /**
198
387
  * Rows for one transcript block. A finalized block renders once for a given revision
@@ -204,13 +393,28 @@ export class OwnedUiSessionShellRoot {
204
393
  const component = this.#transcript.get(id);
205
394
  if (component === undefined)
206
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
+ };
207
402
  if (block === undefined || block.status !== "finalized")
208
- return component.render(width);
209
- const cached = this.#renderedRows.get(id);
210
- if (cached && cached.width === width && cached.revision === block.revision)
403
+ return render();
404
+ let byWidth = this.#renderedRows.get(id);
405
+ const cached = byWidth?.get(width);
406
+ if (cached?.revision === block.revision)
211
407
  return cached.rows;
212
- const rows = component.render(width);
213
- this.#renderedRows.set(id, { width, revision: block.revision, rows });
408
+ const rows = render();
409
+ if (byWidth === undefined) {
410
+ byWidth = new Map();
411
+ this.#renderedRows.set(id, byWidth);
412
+ }
413
+ byWidth.set(width, { revision: block.revision, rows });
414
+ // Bare A1 probes full width before reserving the overflowing rail column;
415
+ // retain both widths without turning resize history into an unbounded cache.
416
+ while (byWidth.size > 2)
417
+ byWidth.delete(byWidth.keys().next().value);
214
418
  return rows;
215
419
  }
216
420
  transcriptComponent(id) {
@@ -222,6 +426,7 @@ export class OwnedUiSessionShellRoot {
222
426
  && this.#transcriptOrder.at(-1) === previousId
223
427
  && this.#workflowStatusAnchors.get(previousId) === this.#view.transcript.length) {
224
428
  this.#workflowStatusMessages.set(previousId, message);
429
+ this.#documentLayouts.clear();
225
430
  this.invalidate();
226
431
  return;
227
432
  }
@@ -313,6 +518,7 @@ export class OwnedUiSessionShellRoot {
313
518
  }
314
519
  toggleThinkingVisibility() {
315
520
  this.#thinkingVisible = !this.#thinkingVisible;
521
+ this.#documentLayouts.clear();
316
522
  this.invalidate();
317
523
  }
318
524
  get toolsExpanded() {
@@ -335,6 +541,7 @@ export class OwnedUiSessionShellRoot {
335
541
  if (this.#extensionHeader !== component)
336
542
  this.#extensionHeader?.dispose?.();
337
543
  this.#extensionHeader = component;
544
+ this.#documentLayouts.clear();
338
545
  this.invalidate();
339
546
  }
340
547
  setExtensionFooter(component) {
@@ -366,6 +573,7 @@ export class OwnedUiSessionShellRoot {
366
573
  this.#workflowStatusMessages.clear();
367
574
  this.#transcriptOrder = this.#transcriptOrder.filter(id => !id.startsWith("workflow-status-"));
368
575
  this.#lastWorkflowStatusId = undefined;
576
+ this.#documentLayouts.clear();
369
577
  this.invalidate();
370
578
  }
371
579
  resetExtensionUi() {
@@ -382,6 +590,7 @@ export class OwnedUiSessionShellRoot {
382
590
  this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
383
591
  // An extension renderer may have drawn transcript blocks that are now unrendered by it.
384
592
  this.#renderedRows.clear();
593
+ this.#documentLayouts.clear();
385
594
  this.invalidate();
386
595
  }
387
596
  addExtensionNotification(message, type) {
@@ -401,6 +610,9 @@ export class OwnedUiSessionShellRoot {
401
610
  onBranchChange: () => () => { },
402
611
  };
403
612
  }
613
+ usesDefaultInputSurface() {
614
+ return this.#inputSurface === this.editor;
615
+ }
404
616
  setInputSurface(component, disposePrevious = true) {
405
617
  const next = component ?? this.editor;
406
618
  if (next === this.#inputSurface)
@@ -432,6 +644,7 @@ export class OwnedUiSessionShellRoot {
432
644
  this.#inputSurface.setFocused?.(focused);
433
645
  }
434
646
  dispose() {
647
+ this.clearViewportPointerState();
435
648
  this.#themeUnsubscribe();
436
649
  this.header.dispose?.();
437
650
  this.resources.dispose?.();
@@ -439,6 +652,7 @@ export class OwnedUiSessionShellRoot {
439
652
  component.dispose?.();
440
653
  this.#transcript.clear();
441
654
  this.#renderedRows.clear();
655
+ this.#documentLayouts.clear();
442
656
  if (this.#inputSurface !== this.editor)
443
657
  this.#inputSurface.dispose?.();
444
658
  this.#extensionHeader?.dispose?.();
@@ -452,6 +666,13 @@ export class OwnedUiSessionShellRoot {
452
666
  this.#queued.dispose?.();
453
667
  }
454
668
  #syncTranscript(blocks) {
669
+ const previousIds = this.#transcriptOrder.filter(id => !id.startsWith("workflow-status-"));
670
+ const transcriptChanged = previousIds.length !== blocks.length || blocks.some((block, index) => {
671
+ const previous = this.#blocksById.get(block.id);
672
+ return previousIds[index] !== block.id || previous?.revision !== block.revision;
673
+ });
674
+ if (transcriptChanged)
675
+ this.#documentLayouts.clear();
455
676
  this.#blocksById.clear();
456
677
  for (const block of blocks)
457
678
  this.#blocksById.set(block.id, block);
@@ -466,7 +687,7 @@ export class OwnedUiSessionShellRoot {
466
687
  for (const block of blocks) {
467
688
  const component = this.#transcript.get(block.id);
468
689
  if (component === undefined) {
469
- const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers);
690
+ const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer);
470
691
  created.setExpanded(this.#toolsExpanded);
471
692
  this.#transcript.set(block.id, created);
472
693
  }
@@ -508,6 +729,7 @@ export class OwnedUiSessionShellRoot {
508
729
  this.#transcript.set(id, component);
509
730
  this.#workflowStatusAnchors.set(id, this.#view.transcript.length);
510
731
  this.#transcriptOrder.push(id);
732
+ this.#documentLayouts.clear();
511
733
  this.invalidate();
512
734
  return id;
513
735
  }
@@ -555,9 +777,36 @@ export class OwnedUiSessionShellRoot {
555
777
  component.setExpanded(expanded);
556
778
  // Expansion changes what a block draws without changing its revision.
557
779
  this.#renderedRows.clear();
780
+ this.#documentLayouts.clear();
558
781
  this.invalidate();
559
782
  }
560
783
  }
784
+ const SCROLLBAR_CELL_RESET = "\u001b[22;23;24;25;27;28;29;39;54;55m";
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
+ }
790
+ /** Repaint a source prompt with viewport chrome while preserving text, links, and foreground roles. */
791
+ function withoutTerminalBackground(text) {
792
+ return text.replace(TERMINAL_BACKGROUND, "");
793
+ }
794
+ /** A pinned timestamp is content now, not secondary transcript metadata. */
795
+ function pinnedPromptSourceRow(block, sourceRow, width) {
796
+ if (typeof block.payload !== "object" || block.payload === null)
797
+ return sourceRow;
798
+ const value = block.payload.timestamp;
799
+ if (typeof value !== "number" || !Number.isFinite(value))
800
+ return sourceRow;
801
+ const timestamp = formatSubmittedPromptTime(value);
802
+ if (timestamp === null || submittedPromptLayout(width, value).timestamp === null)
803
+ return sourceRow;
804
+ const rowWidth = displayWidth(sourceRow);
805
+ const timestampWidth = displayWidth(timestamp);
806
+ if (rowWidth < timestampWidth)
807
+ return sourceRow;
808
+ return overlaySpan(sourceRow, rowWidth - timestampWidth, rowWidth, piTheme().fg("userMessageText", timestamp));
809
+ }
561
810
  function layoutPort(render, invalidate, handleInput) {
562
811
  return {
563
812
  render,
@@ -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>;