@timurproko/a1 0.2.7-dev.808 → 0.2.7-dev.809

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 (77) hide show
  1. package/dist/app/session-shell/dialog-coordinator.d.ts +42 -0
  2. package/dist/app/session-shell/dialog-coordinator.js +133 -0
  3. package/dist/app/session-shell/dock-chrome.d.ts +34 -0
  4. package/dist/app/session-shell/dock-chrome.js +109 -0
  5. package/dist/app/session-shell/exit-notice.d.ts +1 -1
  6. package/dist/app/session-shell/exit-notice.js +1 -1
  7. package/dist/app/session-shell/extension-surface.d.ts +30 -0
  8. package/dist/app/session-shell/extension-surface.js +105 -0
  9. package/dist/app/session-shell/input-surface-switch.d.ts +36 -0
  10. package/dist/app/session-shell/input-surface-switch.js +85 -0
  11. package/dist/app/session-shell/login-interaction.d.ts +27 -0
  12. package/dist/app/session-shell/login-interaction.js +91 -0
  13. package/dist/app/session-shell/prompt-submission.d.ts +48 -0
  14. package/dist/app/session-shell/prompt-submission.js +276 -0
  15. package/dist/app/session-shell/resident-tab-coordinator.d.ts +41 -0
  16. package/dist/app/session-shell/resident-tab-coordinator.js +120 -0
  17. package/dist/app/session-shell/session-concern.d.ts +22 -0
  18. package/dist/app/session-shell/session-concern.js +29 -0
  19. package/dist/app/session-shell/session-lifecycle.d.ts +29 -0
  20. package/dist/app/session-shell/session-lifecycle.js +49 -0
  21. package/dist/app/session-shell/session-shell-root.d.ts +1 -1
  22. package/dist/app/session-shell/session-shell-root.js +182 -931
  23. package/dist/app/session-shell/session-shell.d.ts +2 -8
  24. package/dist/app/session-shell/session-shell.js +202 -901
  25. package/dist/app/session-shell/settings-bindings.d.ts +17 -0
  26. package/dist/app/session-shell/settings-bindings.js +91 -0
  27. package/dist/app/session-shell/shell-editor-factory.d.ts +27 -0
  28. package/dist/app/session-shell/shell-editor-factory.js +102 -0
  29. package/dist/app/session-shell/shell-host.d.ts +6 -0
  30. package/dist/app/session-shell/shell-host.js +8 -0
  31. package/dist/app/session-shell/shell-layout.d.ts +64 -0
  32. package/dist/app/session-shell/shell-layout.js +18 -0
  33. package/dist/app/session-shell/suggestion-surface.d.ts +25 -0
  34. package/dist/app/session-shell/suggestion-surface.js +63 -0
  35. package/dist/app/session-shell/transcript-document.d.ts +78 -0
  36. package/dist/app/session-shell/transcript-document.js +365 -0
  37. package/dist/app/session-shell/viewport-layout.d.ts +31 -0
  38. package/dist/app/session-shell/viewport-layout.js +160 -0
  39. package/dist/app/session-shell/workflow-output.d.ts +33 -0
  40. package/dist/app/session-shell/workflow-output.js +174 -0
  41. package/dist/app/session-shell/workflow-router.d.ts +25 -0
  42. package/dist/app/session-shell/workflow-router.js +39 -0
  43. package/dist/composition/owned-ui.d.ts +4 -25
  44. package/dist/composition/owned-ui.js +26 -270
  45. package/dist/composition/resident-tab-bridge.d.ts +16 -0
  46. package/dist/composition/resident-tab-bridge.js +159 -0
  47. package/dist/composition/session-backend.d.ts +19 -0
  48. package/dist/composition/session-backend.js +1 -0
  49. package/dist/composition/session-runtime-registry.d.ts +20 -0
  50. package/dist/composition/session-runtime-registry.js +85 -0
  51. package/dist/composition/startup-route.d.ts +32 -0
  52. package/dist/composition/startup-route.js +72 -0
  53. package/dist/integrations/pi/engine/adapter.d.ts +4 -77
  54. package/dist/integrations/pi/engine/adapter.js +85 -393
  55. package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
  56. package/dist/integrations/pi/engine/command-dispatch.js +57 -0
  57. package/dist/integrations/pi/engine/host.js +2 -2
  58. package/dist/integrations/pi/engine/ports.d.ts +56 -0
  59. package/dist/integrations/pi/engine/ports.js +89 -0
  60. package/dist/integrations/pi/engine/view-state.d.ts +42 -0
  61. package/dist/integrations/pi/engine/view-state.js +126 -0
  62. package/dist/native/darwin-arm64/manifest.json +1 -1
  63. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  64. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  65. package/dist/native/darwin-x64/manifest.json +1 -1
  66. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  67. package/dist/native/darwin-x64/terminal-host.node +0 -0
  68. package/dist/native/linux-x64/manifest.json +1 -1
  69. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  70. package/dist/native/linux-x64/terminal-host.node +0 -0
  71. package/dist/native/win32-x64/manifest.json +2 -2
  72. package/dist/native/win32-x64/process-guardian.node +0 -0
  73. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  74. package/dist/native/win32-x64/terminal-host.node +0 -0
  75. package/docs/architecture/boundaries.md +14 -0
  76. package/docs/architecture/resident-tabs.md +6 -6
  77. package/package.json +1 -1
@@ -0,0 +1,365 @@
1
+ import { acceptsTranscriptUpdate } from "../../contracts/owned-ui/index.js";
2
+ import { formatSubmittedPromptTime, submittedPromptLayout } from "../../ui/components/submitted-prompt.js";
3
+ import { nativeHyperlinkStyle, overlaySpan } from "../../ui/components/spans.js";
4
+ import { displayWidth } from "../../ui/components/text.js";
5
+ import { createPiShellChangelog, createPiShellCollapsedChangelog, renderPiShellPackageUpdateNotice, renderPiShellReleaseUpdateNotice, } from "../../integrations/pi/components/shell-presenters-info.js";
6
+ import { isPiPromptStyleCompaction, paintPiSubmittedPromptTimestamp, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
7
+ import { piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
8
+ const LAYOUT_ONLY = new Set(["editor-padding", "diagnostics", "workflow-status", "extension-header"]);
9
+ const WORKFLOW_STATUS_PREFIX = "workflow-status-";
10
+ /**
11
+ * The only owner of the transcript's blocks, their order (including anchored workflow output), the
12
+ * per-block rendered rows, and the per-width document layouts. Every cache drop names its reason.
13
+ */
14
+ export class TranscriptDocument {
15
+ #options;
16
+ #components = new Map();
17
+ #blocks = new Map();
18
+ #anchors = new Map();
19
+ #order = [];
20
+ #renderedRows = new Map();
21
+ // Performance: document layouts are shared by wheel, rail, jump, and selection frames.
22
+ #layouts = new Map();
23
+ #renders = 0;
24
+ constructor(options) {
25
+ this.#options = options;
26
+ }
27
+ /** Block component renders since construction, for rendering evidence. */
28
+ get renderCount() {
29
+ return this.#renders;
30
+ }
31
+ get lastId() {
32
+ return this.#order.at(-1);
33
+ }
34
+ component(id) {
35
+ return this.#components.get(id);
36
+ }
37
+ components() {
38
+ return this.#components.values();
39
+ }
40
+ anchorOf(id) {
41
+ return this.#anchors.get(id);
42
+ }
43
+ /**
44
+ * Applies one block: its component is created or updated in place and the order grows
45
+ * only when the block is new. A streamed chunk costs one component update rather than a
46
+ * walk of the whole transcript.
47
+ */
48
+ apply(block) {
49
+ const current = this.#blocks.get(block.id);
50
+ // Invariant: a full-view/final presentation may preempt queued partials. Never revive an older revision.
51
+ if (current !== undefined && !acceptsTranscriptUpdate(current, block))
52
+ return;
53
+ this.#blocks.set(block.id, block);
54
+ const component = this.#components.get(block.id);
55
+ if (component === undefined) {
56
+ this.#components.set(block.id, this.#options.mount(block));
57
+ this.#order.push(block.id);
58
+ }
59
+ else if (component.revision !== block.revision) {
60
+ component.update(block);
61
+ }
62
+ // Performance: one chunk touches one block, and the updated component tracks its own dirtiness.
63
+ // Invalidating the whole shell here would re-wrap the entire transcript per chunk.
64
+ this.blockChanged(block.id);
65
+ }
66
+ /** Replaces the whole transcript with the view's blocks, keeping anchored workflow output in place. */
67
+ sync(blocks) {
68
+ const previousIds = this.#order.filter(id => !id.startsWith(WORKFLOW_STATUS_PREFIX));
69
+ const transcriptChanged = previousIds.length !== blocks.length || blocks.some((block, index) => {
70
+ const previous = this.#blocks.get(block.id);
71
+ return previousIds[index] !== block.id || previous?.revision !== block.revision;
72
+ });
73
+ if (transcriptChanged)
74
+ this.#layouts.clear();
75
+ this.#blocks.clear();
76
+ for (const block of blocks)
77
+ this.#blocks.set(block.id, block);
78
+ const nextIds = new Set(blocks.map(block => block.id));
79
+ for (const [id, component] of this.#components) {
80
+ if (id.startsWith(WORKFLOW_STATUS_PREFIX) || nextIds.has(id))
81
+ continue;
82
+ component.dispose?.();
83
+ this.#components.delete(id);
84
+ this.#renderedRows.delete(id);
85
+ }
86
+ for (const block of blocks) {
87
+ const component = this.#components.get(block.id);
88
+ if (component === undefined)
89
+ this.#components.set(block.id, this.#options.mount(block));
90
+ else if (component.revision !== block.revision)
91
+ component.update(block);
92
+ }
93
+ const statusIds = [...this.#anchors.keys()];
94
+ const order = [];
95
+ for (let index = 0; index <= blocks.length; index += 1) {
96
+ for (const statusId of statusIds) {
97
+ if (this.#anchors.get(statusId) === index)
98
+ order.push(statusId);
99
+ }
100
+ const block = blocks[index];
101
+ if (block !== undefined)
102
+ order.push(block.id);
103
+ }
104
+ const placed = new Set(order);
105
+ for (const statusId of statusIds) {
106
+ if (!placed.has(statusId))
107
+ order.push(statusId);
108
+ }
109
+ this.#order = order;
110
+ }
111
+ /** Appends workflow output anchored after the transcript's first `anchor` blocks. */
112
+ appendAnchored(id, component, anchor) {
113
+ if (!id.startsWith(WORKFLOW_STATUS_PREFIX))
114
+ throw new TypeError(`anchored output id must start with ${WORKFLOW_STATUS_PREFIX}`);
115
+ this.#components.set(id, component);
116
+ this.#anchors.set(id, anchor);
117
+ this.#order.push(id);
118
+ this.#layouts.clear();
119
+ }
120
+ /** Disposes every anchored workflow output and drops it from the order. */
121
+ removeAnchored() {
122
+ for (const id of this.#anchors.keys()) {
123
+ this.#components.get(id)?.dispose?.();
124
+ this.#components.delete(id);
125
+ }
126
+ this.#anchors.clear();
127
+ this.#order = this.#order.filter(id => !id.startsWith(WORKFLOW_STATUS_PREFIX));
128
+ this.#layouts.clear();
129
+ }
130
+ /** One block's component drew something new: drop its rows and every layout that contains them. */
131
+ blockChanged(id) {
132
+ this.#renderedRows.delete(id);
133
+ this.#layouts.clear();
134
+ }
135
+ invalidate(reason) {
136
+ if (!LAYOUT_ONLY.has(reason))
137
+ this.#renderedRows.clear();
138
+ this.#layouts.clear();
139
+ if (reason === "repaint")
140
+ for (const component of this.#components.values())
141
+ component.invalidate();
142
+ }
143
+ /** A new session binding owns new mounts even when it reuses semantic invocation ids. */
144
+ reset() {
145
+ for (const component of this.#components.values())
146
+ component.dispose?.();
147
+ this.#components.clear();
148
+ this.#blocks.clear();
149
+ this.#order = [];
150
+ this.#renderedRows.clear();
151
+ this.#layouts.clear();
152
+ }
153
+ /** Disposes every component; anchors and blocks are left for a shell that is going away. */
154
+ dispose() {
155
+ for (const component of this.#components.values())
156
+ component.dispose?.();
157
+ this.#components.clear();
158
+ this.#renderedRows.clear();
159
+ this.#layouts.clear();
160
+ }
161
+ layout(width) {
162
+ const { chrome, customViewport } = this.#options;
163
+ // Performance: extension headers may animate independently of transcript revisions, so
164
+ // only the stable built-in document participates in this frame cache.
165
+ const cacheable = !chrome.hasExtensionHeader();
166
+ const cached = cacheable ? this.#layouts.get(width) : undefined;
167
+ if (cached !== undefined)
168
+ return cached;
169
+ const diagnostics = chrome.diagnostics();
170
+ const thinkingVisible = chrome.thinkingVisible();
171
+ const startupRows = diagnostics
172
+ // Compatibility: the pinned route keeps trust failures with Pi-style startup diagnostics;
173
+ // bare A1 presents them in its prompt-adjacent notice dock instead.
174
+ .filter(diagnostic => diagnostic.code === "engine-startup"
175
+ || !customViewport && diagnostic.code === "project-trust")
176
+ .flatMap(diagnostic => renderPiShellStartupDiagnostic(diagnostic, width));
177
+ const resourceRows = [...chrome.resourceRows(width)];
178
+ if (resourceRows.at(-1) === "")
179
+ resourceRows.pop();
180
+ const rows = [
181
+ ...startupRows,
182
+ ...chrome.headerRows(width),
183
+ ...resourceRows,
184
+ ];
185
+ const promptAnchors = [];
186
+ // Rationale: a block that is still streaming re-renders every row it owns, so the frame
187
+ // reports where its rows begin. Damage-aware painting uses that to tell a live block's
188
+ // own reflow apart from a repaint that reaches settled rows.
189
+ let liveTailStartRow;
190
+ for (let index = 0; index < this.#order.length; index += 1) {
191
+ const id = this.#order[index];
192
+ const block = this.#blocks.get(id);
193
+ if (!thinkingVisible && block?.kind === "thinking")
194
+ continue;
195
+ const promptLike = block !== undefined
196
+ && (block.kind === "user" || customViewport && isPiPromptStyleCompaction(block));
197
+ // Invariant: the custom viewport has already removed its rail gutter from this width.
198
+ // Prompt-like and ordinary blocks therefore share one right content edge.
199
+ const blockWidth = width;
200
+ const blockRows = this.#blockRows(id, block, blockWidth);
201
+ if (promptLike) {
202
+ // Compatibility: the first natural prompt gets one breathing row at the document top.
203
+ // Once scrolling advances, the prompt itself reaches row zero and then
204
+ // becomes sticky there, so the spacer is never pinned with it.
205
+ if (customViewport && index === 0)
206
+ rows.push("");
207
+ else if (index > 0)
208
+ rows.push("");
209
+ }
210
+ const firstRow = rows.length;
211
+ if (liveTailStartRow === undefined && block !== undefined && block.status !== "finalized") {
212
+ liveTailStartRow = firstRow;
213
+ }
214
+ rows.push(...blockRows);
215
+ if (promptLike && blockRows[0] !== undefined) {
216
+ const source = pinnedPromptSource(block, blockRows[0], blockWidth, chrome.timeFormat());
217
+ promptAnchors.push({
218
+ id: block.id,
219
+ firstRow,
220
+ lastRow: Math.max(firstRow, rows.length - 1),
221
+ sourceRow: source.row,
222
+ copySuffixWidth: source.copySuffixWidth,
223
+ prominentSourceRow: blockRows[0],
224
+ });
225
+ }
226
+ }
227
+ rows.push(...this.#noticeRows(diagnostics, width));
228
+ const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors), liveTailStartRow };
229
+ if (cacheable) {
230
+ this.#layouts.set(width, layout);
231
+ // Performance: the custom viewport commonly probes full width and reserved-rail width.
232
+ while (this.#layouts.size > 2)
233
+ this.#layouts.delete(this.#layouts.keys().next().value);
234
+ }
235
+ return layout;
236
+ }
237
+ // Invariant: release, package, and changelog notices, then the last three other diagnostics, close the document.
238
+ #noticeRows(diagnostics, width) {
239
+ const { customViewport, cwd } = this.#options;
240
+ const rows = [];
241
+ // Invariant: the A1 release notice precedes the extension-package notice, matching pinned Pi's order.
242
+ // Bare A1 docks it in the viewport tail instead of the document.
243
+ rows.push(...diagnostics
244
+ .filter(diagnostic => !customViewport && diagnostic.code === "release-update")
245
+ .flatMap(diagnostic => {
246
+ const release = parseReleaseUpdateDiagnostic(diagnostic.message);
247
+ return release === null ? [] : renderPiShellReleaseUpdateNotice(release, width);
248
+ }));
249
+ rows.push(...diagnostics
250
+ .filter(diagnostic => diagnostic.code === "package-updates")
251
+ .flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
252
+ rows.push(...diagnostics
253
+ .filter(diagnostic => diagnostic.code === "changelog-collapsed" || diagnostic.code === "changelog-expanded")
254
+ // Rationale: bare A1 translates either startup diagnostic into transient dock chrome;
255
+ // the pinned layout retains Pi's collapsed hint or expanded document.
256
+ .flatMap(diagnostic => customViewport
257
+ ? []
258
+ : diagnostic.code === "changelog-collapsed"
259
+ ? createPiShellCollapsedChangelog().render(width)
260
+ : createPiShellChangelog(diagnostic.message).render(width)));
261
+ rows.push(...diagnostics
262
+ .filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "project-trust"
263
+ && diagnostic.code !== "release-update"
264
+ && diagnostic.code !== "package-updates" && diagnostic.code !== "changelog-collapsed"
265
+ && diagnostic.code !== "changelog-expanded")
266
+ .slice(-3)
267
+ .flatMap(diagnostic => renderPiShellTranscriptBlock({
268
+ id: `diagnostic-${diagnostic.sequence}`,
269
+ kind: diagnostic.severity === "error" ? "error" : "system",
270
+ status: "finalized",
271
+ revision: diagnostic.sequence,
272
+ title: diagnostic.code,
273
+ text: diagnostic.message,
274
+ payload: {},
275
+ }, width, cwd)));
276
+ return rows;
277
+ }
278
+ /** The transcript as plain rows for the parent terminal after a fullscreen leave. */
279
+ exitTranscript(width) {
280
+ const thinkingVisible = this.#options.chrome.thinkingVisible();
281
+ const rows = [];
282
+ for (const id of this.#order) {
283
+ const block = this.#blocks.get(id);
284
+ if (block === undefined || (!thinkingVisible && block.kind === "thinking"))
285
+ continue;
286
+ if (rows.length > 0 && (block.kind === "user" || this.#options.customViewport && isPiPromptStyleCompaction(block)))
287
+ rows.push("");
288
+ rows.push(...this.#blockRows(id, block, width).map(sanitizeExitTranscriptRow));
289
+ }
290
+ while (rows.at(-1) === "")
291
+ rows.pop();
292
+ return rows.join("\n");
293
+ }
294
+ // Performance: finalized blocks cache by revision and width; live blocks always render.
295
+ #blockRows(id, block, width) {
296
+ const component = this.#components.get(id);
297
+ if (component === undefined)
298
+ return [];
299
+ const render = () => {
300
+ this.#renders += 1;
301
+ const rows = component.render(width);
302
+ if (!this.#options.customViewport || block?.kind === "user")
303
+ return rows;
304
+ return rows.map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor));
305
+ };
306
+ if (block === undefined || block.status !== "finalized")
307
+ return render();
308
+ let byWidth = this.#renderedRows.get(id);
309
+ const cached = byWidth?.get(width);
310
+ const presentationRevision = component.presentationRevision ?? 0;
311
+ if (cached?.revision === block.revision && cached.presentationRevision === presentationRevision)
312
+ return cached.rows;
313
+ const rows = render();
314
+ if (byWidth === undefined) {
315
+ byWidth = new Map();
316
+ this.#renderedRows.set(id, byWidth);
317
+ }
318
+ byWidth.set(width, { revision: block.revision, presentationRevision, rows });
319
+ // Invariant: bare A1 probes full width before reserving the overflowing rail column;
320
+ // retain both widths without turning resize history into an unbounded cache.
321
+ while (byWidth.size > 2)
322
+ byWidth.delete(byWidth.keys().next().value);
323
+ return rows;
324
+ }
325
+ }
326
+ /** The release, command, and optional changelog carried by the adapter's `release-update` diagnostic. */
327
+ export function parseReleaseUpdateDiagnostic(message) {
328
+ const [instruction, changelog] = message.split("\n");
329
+ const match = /^New version (\S+) is available\. Run (.+)$/.exec(instruction ?? "");
330
+ if (match === null)
331
+ return null;
332
+ const url = changelog === undefined ? null : /^Changelog: (\S+)$/.exec(changelog)?.[1] ?? null;
333
+ return { version: match[1], command: match[2], changelogUrl: url };
334
+ }
335
+ const KITTY_IMAGE_CONTROL = /\u001b_G[\s\S]*?\u001b\\/g;
336
+ const ITERM_IMAGE_CONTROL = /\u001b]1337;File=[^\u0007]*(?:\u0007|\u001b\\)/g;
337
+ /** Keep semantic SGR/OSC styling while excluding non-replayable image payloads. */
338
+ function sanitizeExitTranscriptRow(row) {
339
+ return row.replace(KITTY_IMAGE_CONTROL, "").replace(ITERM_IMAGE_CONTROL, "");
340
+ }
341
+ /** Web URLs use link blue; file targets retain A1's cyan interactive accent. */
342
+ export function nativeTranscriptLinkColor(text, target) {
343
+ return piTheme().fg(/^file:/iu.test(target) ? "accent" : "mdLink", text);
344
+ }
345
+ /** Preserve the stable metadata-grey foreground for every pinned timestamp state. */
346
+ function pinnedPromptSource(block, sourceRow, width, timeFormat) {
347
+ const unchanged = { row: sourceRow, copySuffixWidth: 0 };
348
+ if (typeof block.payload !== "object" || block.payload === null)
349
+ return unchanged;
350
+ const value = block.payload.timestamp;
351
+ if (typeof value !== "number" || !Number.isFinite(value))
352
+ return unchanged;
353
+ const format = timeFormat ?? "8:00 PM";
354
+ const timestamp = formatSubmittedPromptTime(value, format);
355
+ if (timestamp === null || submittedPromptLayout(width, value, format).timestamp === null)
356
+ return unchanged;
357
+ const rowWidth = displayWidth(sourceRow);
358
+ const timestampWidth = displayWidth(timestamp);
359
+ if (rowWidth < timestampWidth)
360
+ return unchanged;
361
+ return {
362
+ row: overlaySpan(sourceRow, rowWidth - timestampWidth, rowWidth, paintPiSubmittedPromptTimestamp(timestamp)),
363
+ copySuffixWidth: timestampWidth,
364
+ };
365
+ }
@@ -0,0 +1,31 @@
1
+ import type { TranscriptPromptAnchor, TranscriptViewportFrameInput } from "../../ui/components/transcript-viewport.js";
2
+ import type { PiShellComponentPort } from "../../integrations/pi/components/shell-shared-facade.js";
3
+ import type { ShellLayout, ShellLayoutPorts } from "./shell-layout.js";
4
+ interface VisibleViewportSnapshot {
5
+ readonly width: number;
6
+ readonly height: number;
7
+ readonly documentRows: readonly string[];
8
+ readonly transientSignature: string;
9
+ readonly promptAnchors: readonly TranscriptPromptAnchor[];
10
+ readonly dockLength: number;
11
+ readonly inputSurface: PiShellComponentPort;
12
+ readonly viewportRevision: number;
13
+ readonly selectionRevision: number;
14
+ readonly pointerPosition: TranscriptViewportFrameInput["pointerPosition"];
15
+ }
16
+ /**
17
+ * Bare A1's custom viewport: a bounded transcript viewport above the dock, composed by the session
18
+ * viewport controller. It owns the snapshot that lets a safe keystroke repaint only the dock.
19
+ */
20
+ export declare class ViewportLayout implements ShellLayout {
21
+ #private;
22
+ constructor(ports: ShellLayoutPorts);
23
+ invalidate(): void;
24
+ composed(): VisibleViewportSnapshot | undefined;
25
+ compositions(): {
26
+ readonly full: number;
27
+ readonly dockOnly: number;
28
+ };
29
+ render(width: number): readonly string[];
30
+ }
31
+ export {};
@@ -0,0 +1,160 @@
1
+ import { heldNativeHyperlinkStyle } from "../../ui/components/spans.js";
2
+ /**
3
+ * Bare A1's custom viewport: a bounded transcript viewport above the dock, composed by the session
4
+ * viewport controller. It owns the snapshot that lets a safe keystroke repaint only the dock.
5
+ */
6
+ export class ViewportLayout {
7
+ #ports;
8
+ #snapshot;
9
+ #fullCompositions = 0;
10
+ #dockOnlyCompositions = 0;
11
+ constructor(ports) {
12
+ this.#ports = ports;
13
+ }
14
+ invalidate() {
15
+ this.#snapshot = undefined;
16
+ }
17
+ composed() {
18
+ return this.#snapshot;
19
+ }
20
+ compositions() {
21
+ return { full: this.#fullCompositions, dockOnly: this.#dockOnlyCompositions };
22
+ }
23
+ render(width) {
24
+ const { viewport, input, dockChrome } = this.#ports;
25
+ const height = Math.max(0, this.#ports.rows());
26
+ const dock = this.#ports.dock(width);
27
+ const copyAcknowledgement = dockChrome.renderCopyAcknowledgement(width);
28
+ // Invariant: auto/always own one stable final-column gutter. Every scrollable
29
+ // surface ends before it; the dock remains full-width and hidden mode returns it.
30
+ const documentWidth = viewport.config.scrollbarAppearance === "hidden"
31
+ ? width
32
+ : Math.max(1, width - 1);
33
+ const document = this.#ports.document(documentWidth);
34
+ const steeringRows = this.#ports.queued(documentWidth);
35
+ const releaseBanner = dockChrome.renderReleaseBanner(this.#ports.diagnostics(), documentWidth);
36
+ // Rationale: the release notice rides the bottom-aligned tail directly above Working, so it
37
+ // never displaces transcript content and scrolls away with the live status.
38
+ // Invariant: when idle the banner's last row sits on the line Working would occupy.
39
+ const statusRows = [...releaseBanner?.rows ?? [], ...this.#ports.liveStatus(documentWidth)];
40
+ const transientSignature = transientRowsSignature(steeringRows, statusRows);
41
+ const snapshot = this.#snapshot;
42
+ // Invariant: paint-only feedback may cover either viewport or dock cells, so
43
+ // ordinary dock-only reuse cannot safely omit its overlay composition.
44
+ const dockInputCandidate = input.dockInputCandidate && copyAcknowledgement === undefined;
45
+ // Concurrency: input may change after a keyboard receipt but before its paint.
46
+ // Capture only the live inputs this composition will actually represent.
47
+ const inputSurface = input.surface;
48
+ const dockRows = dock.rows;
49
+ const dockStartRow = height - dockRows.length + 1;
50
+ const copyableDockRows = this.#copyableDockRows(dock);
51
+ this.#placeInputFrames(dock, width, height, dockStartRow);
52
+ const viewportRevision = viewport.presentationRevision;
53
+ const selectionRevision = viewport.selectionRevision;
54
+ const pointerPosition = viewport.pointerPosition;
55
+ input.takeDockInputCandidate();
56
+ let frame = dockInputCandidate
57
+ && snapshot !== undefined
58
+ && snapshot.width === width
59
+ && snapshot.height === height
60
+ && snapshot.documentRows === document.rows
61
+ && snapshot.transientSignature === transientSignature
62
+ && snapshot.promptAnchors === document.promptAnchors
63
+ && snapshot.dockLength === dockRows.length
64
+ && snapshot.inputSurface === inputSurface
65
+ && snapshot.viewportRevision === viewportRevision
66
+ && snapshot.selectionRevision === selectionRevision
67
+ ? viewport.composeDockOnly(dockRows, width, height, copyableDockRows)
68
+ : null;
69
+ if (frame === null) {
70
+ frame = viewport.compose({
71
+ // Performance: do not copy the complete document on unchanged dock-only input.
72
+ // Steering and Working retain their one non-persistent viewport-tail ownership.
73
+ documentRows: [...document.rows, ...steeringRows, ...statusRows],
74
+ ...(viewport.transcriptPointerSelecting
75
+ ? { paintDocumentRow: heldNativeHyperlinkStyle }
76
+ : {}),
77
+ // Invariant: this boundary tracks persistent transcript rows for streaming and
78
+ // geometry; transient Steering and Working rows remain complete-frame selectable.
79
+ selectableDocumentRowCount: document.rows.length,
80
+ ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
81
+ bottomAlignedTailRowCount: steeringRows.length + statusRows.length,
82
+ dockRows,
83
+ copyableDockRows,
84
+ promptAnchors: document.promptAnchors,
85
+ width,
86
+ height,
87
+ // Invariant: the control belongs immediately above the complete dock and
88
+ // floats over transient viewport content rather than consuming a dock row.
89
+ bottomControlRow: Math.max(0, height - Math.min(height, dockRows.length) - 1),
90
+ ...(copyAcknowledgement === undefined || height <= 0 ? {} : {
91
+ frameOverlay: {
92
+ row: Math.max(1, Math.min(height, dockStartRow + dock.inputOffset - 1)),
93
+ text: copyAcknowledgement,
94
+ },
95
+ }),
96
+ theme: this.#ports.theme,
97
+ });
98
+ this.#fullCompositions += 1;
99
+ }
100
+ else
101
+ this.#dockOnlyCompositions += 1;
102
+ dockChrome.locateReleaseClose(frame, releaseBanner, document.rows.length + steeringRows.length + statusRows.length, statusRows.length);
103
+ this.#snapshot = {
104
+ width,
105
+ height,
106
+ documentRows: document.rows,
107
+ transientSignature,
108
+ promptAnchors: document.promptAnchors,
109
+ dockLength: dockRows.length,
110
+ inputSurface,
111
+ // Invariant: a callback during composition cannot bless older rows with its
112
+ // newer revision. Its ordinary pending render will publish the newer state.
113
+ viewportRevision,
114
+ selectionRevision: frame.descriptor.selectionRevision,
115
+ pointerPosition,
116
+ };
117
+ this.#ports.onFrame?.(frame);
118
+ return frame.rows;
119
+ }
120
+ #copyableDockRows(dock) {
121
+ // Rationale: rendered chip tags are lossy presentation (truncated labels), so a frame drag over
122
+ // a draft holding chips stays visual-only; editor-owned word/line selection still copies the
123
+ // expanded chip text through its semantic route.
124
+ const draft = this.#ports.input.usesEditor ? this.#ports.editor.getText() : "";
125
+ const promptCopyRows = draft.length > 0
126
+ && !draft.split("\n").some(line => this.#ports.promptChips.atomicRanges(line).length > 0)
127
+ ? Math.max(0, dock.inputRows - 2)
128
+ : 0;
129
+ return [
130
+ ...dock.copyableMessageRows,
131
+ ...Array.from({ length: promptCopyRows }, (_value, index) => ({
132
+ row: dock.editorOffset + index + 1,
133
+ prompt: index === 0 ? "first" : "continuation",
134
+ })),
135
+ ...Array.from({ length: dock.footerRows }, (_value, index) => ({
136
+ row: dock.rows.length - dock.footerRows + index,
137
+ })),
138
+ ];
139
+ }
140
+ // Invariant: pointer routing needs the replacement surface's frame, or the editor's rows when it is active.
141
+ #placeInputFrames(dock, width, height, dockStartRow) {
142
+ const { viewport, input } = this.#ports;
143
+ viewport.setInputSurfaceFrame(input.usesEditor ? undefined : {
144
+ component: input.surface,
145
+ columnStart: 1,
146
+ columnEnd: width,
147
+ rowStart: Math.max(1, dockStartRow + dock.editorOffset),
148
+ rowEnd: Math.min(height, dockStartRow + dock.editorOffset + dock.inputRows - 1),
149
+ });
150
+ viewport.setEditorPointerFrame(input.usesEditor
151
+ ? {
152
+ rowStart: dockStartRow + dock.editorOffset,
153
+ rowEnd: dockStartRow + dock.editorOffset + dock.inputRows - 1,
154
+ }
155
+ : undefined);
156
+ }
157
+ }
158
+ function transientRowsSignature(steeringRows, statusRows) {
159
+ return `${steeringRows.length}\u0000${steeringRows.join("\u0000")}\u0001${statusRows.length}\u0000${statusRows.join("\u0000")}`;
160
+ }
@@ -0,0 +1,33 @@
1
+ import type { ImageAttachmentError, OwnedUiWorkflowMessage, OwnedUiWorkflowResult } from "../../contracts/owned-ui/index.js";
2
+ import { type PiShellHotkeysPresentation } from "../../integrations/pi/components/shell-presenters-info.js";
3
+ import type { DockChrome } from "./dock-chrome.js";
4
+ import type { TranscriptDocument } from "./transcript-document.js";
5
+ export interface WorkflowOutputPorts {
6
+ readonly customViewport: boolean;
7
+ readonly document: Pick<TranscriptDocument, "lastId" | "anchorOf" | "invalidate" | "appendAnchored" | "removeAnchored">;
8
+ readonly dock: Pick<DockChrome, "setNotice" | "dismissNotice">;
9
+ /** How many semantic transcript blocks precede new output; workflow output anchors after them. */
10
+ transcriptLength(): number;
11
+ outputPad(): 0 | 1;
12
+ hotkeysPresentation(): PiShellHotkeysPresentation;
13
+ readonly componentRuntime: {
14
+ getColumns(): number;
15
+ getRows(): number;
16
+ requestRender(): void;
17
+ };
18
+ /** Output changed what the shell draws; the root invalidates. */
19
+ invalidate(): void;
20
+ }
21
+ /**
22
+ * Presents workflow results: bare A1 answers in the dock notice, the pinned profile appends
23
+ * transcript-anchored output, and consecutive status lines at the same anchor replace each other.
24
+ */
25
+ export declare class WorkflowOutput {
26
+ #private;
27
+ constructor(ports: WorkflowOutputPorts);
28
+ appendStatus(message: string): void;
29
+ appendMessage(message: OwnedUiWorkflowMessage, errorCode?: ImageAttachmentError["code"]): void;
30
+ appendResult(result: OwnedUiWorkflowResult, errorCode?: ImageAttachmentError["code"]): void;
31
+ /** Drops every anchored output and the status-replacement chain. */
32
+ reset(): void;
33
+ }