@timurproko/a1 0.1.8-dev.299 → 0.1.8-dev.322

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 (73) hide show
  1. package/README.md +16 -4
  2. package/bin/guardian.js +4 -3
  3. package/bin/ui.js +4 -2
  4. package/bin/update-recovery.js +3 -1
  5. package/dist/composition/owned-ui.js +2 -1
  6. package/dist/features/launch/profile-paths.d.ts +2 -0
  7. package/dist/features/launch/profile-paths.js +11 -0
  8. package/dist/foundation/launch-context/index.d.ts +29 -0
  9. package/dist/foundation/launch-context/index.js +83 -0
  10. package/dist/foundation/launch-guardian/main.js +4 -3
  11. package/dist/foundation/release/bootstrap.d.ts +1 -1
  12. package/dist/foundation/release/bootstrap.js +18 -12
  13. package/dist/foundation/release/cohort-state.d.ts +1 -0
  14. package/dist/foundation/release/cohort-state.js +4 -0
  15. package/dist/foundation/release/release-gc.js +2 -2
  16. package/dist/foundation/release/release-store.js +10 -3
  17. package/dist/foundation/release/release.d.ts +3 -1
  18. package/dist/foundation/release/release.js +7 -3
  19. package/dist/foundation/release/restart-certification.d.ts +1 -0
  20. package/dist/foundation/release/restart-certification.js +5 -0
  21. package/dist/foundation/release/update-recovery.d.ts +2 -0
  22. package/dist/foundation/release/update-recovery.js +4 -0
  23. package/dist/foundation/release/update.js +5 -0
  24. package/dist/foundation/release/warmup.d.ts +1 -1
  25. package/dist/foundation/release/warmup.js +5 -4
  26. package/dist/foundation/startup/startup-runtime.js +13 -11
  27. package/dist/foundation/supervision/main.js +8 -7
  28. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -0
  29. package/dist/integrations/pi/components/owned-editor-ux.js +54 -3
  30. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
  31. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  32. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  33. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  34. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  35. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  36. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  37. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  38. package/dist/integrations/pi/engine/adapter.d.ts +13 -1
  39. package/dist/integrations/pi/engine/adapter.js +86 -25
  40. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  41. package/dist/integrations/pi/engine/changelog.js +63 -0
  42. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  43. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  44. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  45. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
  46. package/dist/integrations/pi/session-ui/prompt-chips.js +37 -13
  47. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +25 -0
  48. package/dist/integrations/pi/session-ui/session-shell-root.js +65 -74
  49. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  50. package/dist/integrations/pi/session-ui/session-shell.js +58 -7
  51. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  52. package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
  53. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  54. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
  55. package/dist/native/darwin-arm64/manifest.json +1 -1
  56. package/dist/native/linux-x64/manifest.json +1 -1
  57. package/dist/native/win32-x64/manifest.json +2 -2
  58. package/dist/native/win32-x64/process-guardian.exe +0 -0
  59. package/dist/product-identity.d.ts +1 -1
  60. package/dist/product-identity.js +2 -35
  61. package/dist/product-identity.json +0 -22
  62. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  63. package/dist/ui/components/transcript-viewport.js +23 -2
  64. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  65. package/dist/ui/components/visible-hyperlinks.js +1 -0
  66. package/dist/ui/settings/declarations.js +1 -0
  67. package/dist/ui/settings/sections.js +11 -1
  68. package/docs/architecture/boundaries.md +2 -1
  69. package/docs/architecture/internal-naming.md +89 -0
  70. package/docs/architecture/resource-and-data-policy.md +9 -4
  71. package/docs/architecture/toolchain.md +15 -1
  72. package/docs/features/prompt-history.md +39 -11
  73. package/package.json +8 -3
@@ -17,5 +17,5 @@ export declare class PiWorkflowControllerPort implements AgentWorkflowPort {
17
17
  listWorkflows(): Promise<readonly AgentWorkflowDescriptor[]>;
18
18
  executeWorkflow(workflowId: string, input: AgentJsonValue, signal?: AbortSignal): Promise<AgentJsonValue>;
19
19
  }
20
- export declare const PI_BUILTIN_WORKFLOW_ROUTES: readonly ("name" | "share" | "model" | "login" | "logout" | "settings" | "reload" | "compact" | "new" | "resume" | "quit" | "session" | "scoped-models" | "export" | "import" | "copy" | "changelog" | "hotkeys" | "fork" | "clone" | "tree" | "trust" | "debug" | "arminsayshi" | "dementedelves")[];
20
+ export declare const PI_BUILTIN_WORKFLOW_ROUTES: readonly ("name" | "share" | "model" | "login" | "logout" | "settings" | "reload" | "compact" | "new" | "resume" | "quit" | "session" | "tree" | "scoped-models" | "export" | "import" | "copy" | "changelog" | "hotkeys" | "fork" | "clone" | "trust" | "debug" | "arminsayshi" | "dementedelves")[];
21
21
  export declare const PI_WORKFLOW_CAPABILITIES: readonly WorkflowCapability[];
@@ -178,7 +178,7 @@ export interface PiWorkflowHost {
178
178
  readonly stdout: string;
179
179
  readonly stderr: string;
180
180
  }>;
181
- readChangelog(): Promise<string>;
181
+ readChangelog(sinceVersion?: string): Promise<string>;
182
182
  }
183
183
  export interface PiBashWorkflowResult {
184
184
  readonly command: string;
@@ -11,7 +11,7 @@ export interface PreparedPrompt {
11
11
  /** Owns semantic chips and bounded pending paste references; cancels background work on reset or disposal. */
12
12
  export declare class PromptChipStore {
13
13
  #private;
14
- beginPaste(currentText: string, read: (signal: AbortSignal) => Promise<PiShellClipboardContent | null>, onError: (error: unknown) => void): {
14
+ beginPaste(currentText: string, read: (signal: AbortSignal) => Promise<PiShellClipboardContent | null>, onError: (error: unknown) => void, onImage?: () => void): {
15
15
  marker: string;
16
16
  result: Promise<string>;
17
17
  };
@@ -21,6 +21,8 @@ export declare class PromptChipStore {
21
21
  resetPastes(text: string): string;
22
22
  dispose(): Promise<void>;
23
23
  transformPastedContent(content: PiShellClipboardContent, currentText?: string): string;
24
+ /** Hide provisional clipboard identities until the read identifies an actual image. */
25
+ hiddenRanges(line: string): readonly PiShellEditorTextRange[];
24
26
  atomicRanges(line: string): readonly PiShellEditorTextRange[];
25
27
  hyperlinkRanges(text: string): readonly {
26
28
  start: number;
@@ -15,27 +15,32 @@ export class PromptChipStore {
15
15
  #pending = new Map();
16
16
  #preparation = new ImagePreparationClient();
17
17
  #stopping = new Set();
18
- beginPaste(currentText, read, onError) {
19
- const marker = `[📷 preparing-${randomBytes(5).toString("hex")}]`;
18
+ beginPaste(currentText, read, onError, onImage = () => { }) {
19
+ const id = randomBytes(5).toString("hex");
20
+ // Invariant: reserve paste identity immediately, but only display a screenshot after identifying an image.
21
+ const marker = `[📷 screenshot-${id}]`;
20
22
  const job = this.#preparation.start(async (signal) => {
21
23
  const content = await read(signal);
22
24
  entry.kind = content?.kind === "image" ? "image" : "text";
23
- if (content?.kind === "image" && this.#imageCount(currentText) >= 8)
24
- throw new ImageAttachmentError("image-count");
25
+ if (content?.kind === "image") {
26
+ onImage();
27
+ if (this.#imageCount(currentText) >= 8)
28
+ throw new ImageAttachmentError("image-count");
29
+ }
25
30
  return content;
26
31
  });
27
32
  const entry = { marker, job, references: 0, kind: "unknown", completion: job.result.then(content => {
28
33
  if (content === null)
29
34
  return "";
30
35
  if (content.kind === "image") {
31
- return this.#addPreparedImage(content);
36
+ return this.#addPreparedImage(content, id);
32
37
  }
33
38
  return this.transformPastedContent(content);
34
39
  }).catch(error => {
35
40
  entry.error = error instanceof ImageAttachmentError ? error : new ImageAttachmentError("image-codec");
36
41
  if (entry.error.code !== "image-canceled" && entry.references === 0)
37
42
  onError(entry.error);
38
- return marker.replace("preparing-", "failed-");
43
+ return marker.replace("screenshot-", "failed-");
39
44
  }).then(replacement => {
40
45
  entry.replacement = replacement;
41
46
  return replacement;
@@ -95,12 +100,17 @@ export class PromptChipStore {
95
100
  }
96
101
  async dispose() { await Promise.all([this.#preparation.dispose(), ...this.#stopping]); }
97
102
  #imageCount(text) {
98
- const regular = [...this.#chips.values()].filter(chip => chip.kind === "image" && text.includes(chip.tag)).length;
99
- return regular + [...this.#pending.values()].filter(entry => entry.kind !== "text" && (text.includes(entry.marker) || text.includes(entry.marker.replace("preparing-", "failed-")))).length;
103
+ const tags = new Set([...this.#chips.values()].filter(chip => chip.kind === "image" && text.includes(chip.tag)).map(chip => chip.tag));
104
+ for (const entry of this.#pending.values()) {
105
+ if (entry.kind !== "text" && (text.includes(entry.marker) || text.includes(entry.marker.replace("screenshot-", "failed-")))) {
106
+ // Invariant: a ready screenshot can retain its pending label but still occupies only one slot.
107
+ tags.add(entry.replacement ?? entry.marker);
108
+ }
109
+ }
110
+ return tags.size;
100
111
  }
101
- #addPreparedImage(image) {
102
- const suffix = image.mimeType === "image/jpeg" ? "jpg" : image.mimeType.split("/")[1] ?? "png";
103
- const tag = `[📷 screenshot-${randomBytes(5).toString("hex")}${image.transformed ? "-resized" : ""}.${suffix}]`;
112
+ #addPreparedImage(image, id) {
113
+ const tag = `[📷 screenshot-${id}${image.transformed ? "-resized" : ""}]`;
104
114
  const attachment = Object.freeze({ type: "image", data: image.data, mimeType: image.mimeType });
105
115
  assertPromptImages([attachment]);
106
116
  this.#chips.set(tag, { kind: "image", tag, image: attachment });
@@ -114,7 +124,7 @@ export class PromptChipStore {
114
124
  return "";
115
125
  assertPromptImages([...this.prepareSubmission(currentText).images, { type: "image", ...image }]);
116
126
  const id = randomBytes(5).toString("hex");
117
- const tag = `[📷 screenshot-${id}.png]`;
127
+ const tag = `[📷 screenshot-${id}]`;
118
128
  this.#chips.set(tag, {
119
129
  kind: "image",
120
130
  tag,
@@ -142,6 +152,20 @@ export class PromptChipStore {
142
152
  return this.#recordUnique({ kind: "file", tag: `[${icon} ${label}]`, path: item.fullPath });
143
153
  }).join("");
144
154
  }
155
+ /** Hide provisional clipboard identities until the read identifies an actual image. */
156
+ hiddenRanges(line) {
157
+ const ranges = [];
158
+ for (const entry of this.#pending.values()) {
159
+ if (entry.kind === "image")
160
+ continue;
161
+ let from = 0;
162
+ while ((from = line.indexOf(entry.marker, from)) >= 0) {
163
+ ranges.push({ start: from, end: from + entry.marker.length });
164
+ from += entry.marker.length;
165
+ }
166
+ }
167
+ return ranges.sort((left, right) => left.start - right.start);
168
+ }
145
169
  atomicRanges(line) {
146
170
  return [...line.matchAll(CHIP_PATTERN)].map(match => ({
147
171
  start: match.index,
@@ -181,7 +205,7 @@ export class PromptChipStore {
181
205
  #replaceResolvable(text, includeImages) {
182
206
  let expanded = text;
183
207
  for (const entry of this.#pending.values()) {
184
- if (!expanded.includes(entry.marker) && !expanded.includes(entry.marker.replace("preparing-", "failed-")))
208
+ if (!expanded.includes(entry.marker) && !expanded.includes(entry.marker.replace("screenshot-", "failed-")))
185
209
  continue;
186
210
  if (includeImages && entry.error !== undefined)
187
211
  throw entry.error;
@@ -116,6 +116,31 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
116
116
  applyTranscriptBlock(block: OwnedUiSessionViewModel["transcript"][number]): void;
117
117
  render(width: number): readonly string[];
118
118
  viewportFrameDescriptor(): TranscriptViewportFrameDescriptor | null;
119
+ /** Read-only, payload-free evidence; observing a pending frame must never render or repair it. */
120
+ viewportPresentationEvidence(): {
121
+ candidate: boolean;
122
+ candidateRevision: number | null;
123
+ currentRevision: number;
124
+ composedRevision: number | null;
125
+ currentPointer: {
126
+ readonly column: number;
127
+ readonly row: number;
128
+ } | null;
129
+ composedPointer: {
130
+ readonly column: number;
131
+ readonly row: number;
132
+ } | null;
133
+ frameId: number | null;
134
+ bottom: {
135
+ readonly row: number;
136
+ readonly columnStart: number;
137
+ readonly columnEnd: number;
138
+ } | null;
139
+ followingEnd: boolean;
140
+ scrollTop: number;
141
+ maxScroll: number;
142
+ cause: "detached" | "initial" | "steady" | "dock-input" | "follow-shift" | "geometry-change" | null;
143
+ };
119
144
  /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
120
145
  viewportTransientTailRowCount(): number;
121
146
  hasActiveSelection(): boolean;
@@ -1,6 +1,6 @@
1
1
  import { PROMPT_GLYPH, backgroundSgrSpan, caretCell, composeSubmittedPromptRows, displayWidth, faint, formatSubmittedPromptTime, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, progressStatusText, 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, createPiShellCollapsedChangelog, 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";
3
+ import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, 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, renderPiShellCommandMessage, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
4
4
  import { PiTuiRuntimeAdapter, classifyPiTuiInput, } from "../tui-runtime/index.js";
5
5
  import { PromptChipStore } from "./prompt-chips.js";
6
6
  import { SessionViewportController } from "./session-viewport-controller.js";
@@ -45,7 +45,8 @@ export class OwnedUiSessionShellRoot {
45
45
  #inputSurfaceCoordination = "editor";
46
46
  #dockInputReuseEnabled;
47
47
  #dockInputCandidate = false;
48
- #dockInputSnapshot;
48
+ // Invariant: receipt-time eligibility is evidence, never authorization for composition-time reuse.
49
+ #dockInputRevision;
49
50
  #visibleViewportSnapshot;
50
51
  #fullViewportCompositions = 0;
51
52
  #dockOnlyViewportCompositions = 0;
@@ -94,8 +95,9 @@ export class OwnedUiSessionShellRoot {
94
95
  return "";
95
96
  }
96
97
  },
97
- ...(this.#customViewport ? { beginClipboardPaste: () => this.#promptChips.beginPaste(this.editor.getText(), signal => handlers.readClipboardContent?.(signal) ?? Promise.resolve(null), error => handlers.onPasteRejected?.(error)) } : {}),
98
+ ...(this.#customViewport ? { beginClipboardPaste: () => this.#promptChips.beginPaste(this.editor.getText(), signal => handlers.readClipboardContent?.(signal) ?? Promise.resolve(null), error => handlers.onPasteRejected?.(error), () => handlers.requestRender()) } : {}),
98
99
  editorAtomicRanges: line => this.#promptChips.atomicRanges(line),
100
+ editorHiddenRanges: line => this.#promptChips.hiddenRanges(line),
99
101
  decorateEditorRow: (row, width) => {
100
102
  const plain = stripAnsi(row);
101
103
  const ranges = this.#promptChips.hyperlinkRanges(plain);
@@ -289,13 +291,12 @@ export class OwnedUiSessionShellRoot {
289
291
  const transientSignature = transientRowsSignature(steeringRows, statusRows);
290
292
  const snapshot = this.#visibleViewportSnapshot;
291
293
  const dockInputCandidate = this.#dockInputCandidate;
292
- const dockInputSnapshot = dockInputCandidate ? this.#dockInputSnapshot : undefined;
293
- const dockInputTransientSignature = dockInputCandidate
294
- ? dockInputSnapshot?.transientSignature ?? transientSignature
295
- : transientSignature;
296
- // Invariant: pending Steering and live Working rows share one non-selectable
297
- // viewport tail; only non-working status, widgets, input, and footer stay docked.
298
- const scrollRows = [...document.rows, ...steeringRows, ...statusRows];
294
+ // Concurrency: input may change after a keyboard receipt but before its paint.
295
+ // Capture only the live inputs this composition will actually represent.
296
+ const viewportRevision = this.#viewportController.presentationRevision;
297
+ const selectionRevision = this.#viewportController.selectionRevision;
298
+ const pointerPosition = this.#viewportController.pointerPosition;
299
+ const inputSurface = this.#inputSurface;
299
300
  const dockRows = dock.rows;
300
301
  const selectableDocumentRowCount = document.rows.length;
301
302
  const dockStartRow = height - dockRows.length + 1;
@@ -311,28 +312,27 @@ export class OwnedUiSessionShellRoot {
311
312
  && snapshot !== undefined
312
313
  && snapshot.width === width
313
314
  && snapshot.height === height
314
- && snapshot.documentRows === (dockInputSnapshot?.documentRows ?? scrollRows)
315
- && snapshot.transientSignature === dockInputTransientSignature
316
- && snapshot.promptAnchors === (dockInputSnapshot?.promptAnchors ?? document.promptAnchors)
315
+ && snapshot.documentRows === document.rows
316
+ && snapshot.transientSignature === transientSignature
317
+ && snapshot.promptAnchors === document.promptAnchors
317
318
  && snapshot.dockLength === dockRows.length
318
- && snapshot.inputSurface === (dockInputSnapshot?.inputSurface ?? this.#inputSurface)
319
- && snapshot.viewportRevision === (dockInputSnapshot?.viewportRevision ?? this.#viewportController.presentationRevision)
320
- && snapshot.selectionRevision === (dockInputSnapshot?.selectionRevision ?? this.#viewportController.selectionRevision)
319
+ && snapshot.inputSurface === inputSurface
320
+ && snapshot.viewportRevision === viewportRevision
321
+ && snapshot.selectionRevision === selectionRevision
321
322
  ? this.#viewportController.composeDockOnly(dockRows, width, height)
322
323
  : null;
323
- // Performance: a spinner tick or queue update can race the input-triggered
324
- // render. Recompute the complete transient signature before reusing rows.
325
- if (frame !== null && dockInputTransientSignature !== transientSignature)
326
- frame = null;
327
324
  if (frame === null) {
328
325
  frame = this.#viewportController.compose({
329
- documentRows: scrollRows,
326
+ // Performance: do not copy the complete document on unchanged dock-only input.
327
+ // Steering and Working retain their one non-selectable viewport-tail ownership.
328
+ documentRows: [...document.rows, ...steeringRows, ...statusRows],
330
329
  ...(this.#viewportController.transcriptPointerSelecting
331
330
  ? { paintDocumentRow: heldNativeHyperlinkStyle }
332
331
  : {}),
333
332
  // Invariant: selection and copying stop at the real document tail; Steering,
334
333
  // fitting alignment, and live Working remain transient presentation chrome.
335
334
  selectableDocumentRowCount,
335
+ ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
336
336
  bottomAlignedTailRowCount: statusRows.length,
337
337
  dockRows,
338
338
  promptAnchors: document.promptAnchors,
@@ -350,13 +350,16 @@ export class OwnedUiSessionShellRoot {
350
350
  this.#visibleViewportSnapshot = {
351
351
  width,
352
352
  height,
353
- documentRows: scrollRows,
353
+ documentRows: document.rows,
354
354
  transientSignature,
355
355
  promptAnchors: document.promptAnchors,
356
356
  dockLength: dockRows.length,
357
- inputSurface: this.#inputSurface,
358
- viewportRevision: this.#viewportController.presentationRevision,
359
- selectionRevision: this.#viewportController.selectionRevision,
357
+ inputSurface,
358
+ // Invariant: a callback during composition cannot bless older rows with its
359
+ // newer revision. Its ordinary pending render will publish the newer state.
360
+ viewportRevision,
361
+ selectionRevision: frame.descriptor.selectionRevision,
362
+ pointerPosition,
360
363
  };
361
364
  this.#onViewportFrame?.(frame);
362
365
  return frame.rows;
@@ -364,6 +367,25 @@ export class OwnedUiSessionShellRoot {
364
367
  viewportFrameDescriptor() {
365
368
  return this.#viewportController.frame?.descriptor ?? null;
366
369
  }
370
+ /** Read-only, payload-free evidence; observing a pending frame must never render or repair it. */
371
+ viewportPresentationEvidence() {
372
+ const frame = this.#viewportController.frame;
373
+ const snapshot = this.#visibleViewportSnapshot;
374
+ return {
375
+ candidate: this.#dockInputCandidate,
376
+ candidateRevision: this.#dockInputCandidate ? this.#dockInputRevision ?? null : null,
377
+ currentRevision: this.#viewportController.presentationRevision,
378
+ composedRevision: snapshot?.viewportRevision ?? null,
379
+ currentPointer: this.#viewportController.pointerPosition ?? null,
380
+ composedPointer: snapshot?.pointerPosition ?? null,
381
+ frameId: frame?.descriptor.frameId ?? null,
382
+ bottom: frame?.hits.bottom ?? null,
383
+ followingEnd: frame?.followingEnd ?? true,
384
+ scrollTop: frame?.scrollTop ?? 0,
385
+ maxScroll: frame?.maxScroll ?? 0,
386
+ cause: frame?.descriptor.cause ?? null,
387
+ };
388
+ }
367
389
  /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
368
390
  viewportTransientTailRowCount() {
369
391
  return this.#viewportController.frame?.hits.transientTail.length ?? 0;
@@ -485,6 +507,10 @@ export class OwnedUiSessionShellRoot {
485
507
  ...resourceRows,
486
508
  ];
487
509
  const promptAnchors = [];
510
+ // Rationale: a block that is still streaming re-renders every row it owns, so the frame
511
+ // reports where its rows begin. Damage-aware painting uses that to tell a live block's
512
+ // own reflow apart from a repaint that reaches settled rows.
513
+ let liveTailStartRow;
488
514
  for (let index = 0; index < this.#transcriptOrder.length; index += 1) {
489
515
  const id = this.#transcriptOrder[index];
490
516
  const block = this.#blocksById.get(id);
@@ -507,6 +533,9 @@ export class OwnedUiSessionShellRoot {
507
533
  rows.push("");
508
534
  }
509
535
  const firstRow = rows.length;
536
+ if (liveTailStartRow === undefined && block !== undefined && block.status !== "finalized") {
537
+ liveTailStartRow = firstRow;
538
+ }
510
539
  rows.push(...blockRows);
511
540
  if (block?.kind === "user" && blockRows[0] !== undefined) {
512
541
  promptAnchors.push({
@@ -538,7 +567,7 @@ export class OwnedUiSessionShellRoot {
538
567
  text: diagnostic.message,
539
568
  payload: {},
540
569
  }, width, this.#cwd)));
541
- const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors) };
570
+ const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors), liveTailStartRow };
542
571
  if (this.#extensionHeader === null) {
543
572
  this.#documentLayouts.set(width, layout);
544
573
  // Performance: the custom viewport commonly probes full width and reserved-rail width.
@@ -606,7 +635,7 @@ export class OwnedUiSessionShellRoot {
606
635
  }
607
636
  const id = this.#appendAnchoredWorkflowComponent(width => [
608
637
  "",
609
- ...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width, this.#outputPad),
638
+ ...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width),
610
639
  ]);
611
640
  this.#workflowStatusMessages.set(id, message);
612
641
  this.#lastWorkflowStatusId = id;
@@ -617,10 +646,8 @@ export class OwnedUiSessionShellRoot {
617
646
  return;
618
647
  }
619
648
  this.#lastWorkflowStatusId = undefined;
620
- const prefix = message.kind === "warning" ? "Warning" : message.kind === "error" ? "Error" : undefined;
621
- const color = message.kind === "accent" ? "accent" : message.kind;
622
- const text = prefix === undefined ? message.message : `${prefix}: ${message.message}`;
623
- this.#appendAnchoredWorkflowComponent(() => ["", ` ${piTheme().fg(color, text)}`]);
649
+ const presentation = { kind: message.kind, message: message.message };
650
+ this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
624
651
  }
625
652
  appendWorkflowResult(result) {
626
653
  if (result.messages !== undefined) {
@@ -642,7 +669,7 @@ export class OwnedUiSessionShellRoot {
642
669
  }
643
670
  if (result.command === "hotkeys" && result.outcome === "completed") {
644
671
  this.#lastWorkflowStatusId = undefined;
645
- const hotkeys = createPiShellHotkeys();
672
+ const hotkeys = createPiShellHotkeys(this.editor.keybindingConfig(), bindings => this.#extensionRenderers.getShortcuts?.(bindings) ?? [], this.#customViewport ? "a1" : "pi");
646
673
  this.#appendAnchoredWorkflowComponent(width => hotkeys.render(width), () => hotkeys.dispose?.());
647
674
  return;
648
675
  }
@@ -661,26 +688,10 @@ export class OwnedUiSessionShellRoot {
661
688
  }
662
689
  if (result.outcome === "completed" && (result.command === "quit" || result.command === "compact"))
663
690
  return;
664
- if (result.command === "new" && result.outcome === "completed") {
665
- this.appendWorkflowMessage({ kind: "accent", message: result.message });
666
- return;
667
- }
668
- if (result.command === "name" && result.outcome === "completed") {
669
- this.#lastWorkflowStatusId = undefined;
670
- this.#appendAnchoredWorkflowComponent(() => [
671
- ...(result.detail ? ["", ` ${piTheme().fg("warning", `Warning: ${result.detail}`)}`] : []),
672
- "",
673
- ` ${piTheme().fg("dim", result.message)}`,
674
- ]);
675
- return;
676
- }
677
- if (result.command === "debug" && result.outcome === "completed") {
691
+ if (result.outcome === "completed" && (result.command === "new" || result.command === "name" || result.command === "debug")) {
678
692
  this.#lastWorkflowStatusId = undefined;
679
- this.#appendAnchoredWorkflowComponent(() => [
680
- "",
681
- ` ${piTheme().fg("accent", result.message)}`,
682
- ...(result.detail ? [` ${piTheme().fg("muted", result.detail)}`] : []),
683
- ]);
693
+ const presentation = { kind: result.command, message: result.message, ...(result.detail === undefined ? {} : { detail: result.detail }) };
694
+ this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
684
695
  return;
685
696
  }
686
697
  if (result.command === "arminsayshi" && result.outcome === "completed") {
@@ -785,9 +796,7 @@ export class OwnedUiSessionShellRoot {
785
796
  this.appendWorkflowStatus(message);
786
797
  return;
787
798
  }
788
- this.#lastWorkflowStatusId = undefined;
789
- const prefix = type === "warning" ? "Warning" : "Error";
790
- this.#appendAnchoredWorkflowComponent(() => ["", ` ${piTheme().fg(type, `${prefix}: ${message}`)}`]);
799
+ this.appendWorkflowMessage({ kind: type, message });
791
800
  }
792
801
  extensionFooterData() {
793
802
  return {
@@ -829,8 +838,7 @@ export class OwnedUiSessionShellRoot {
829
838
  handleInput(data) {
830
839
  this.#dockInputCandidate = this.#customViewport && this.#dockInputReuseEnabled
831
840
  && classifyPiTuiInput(data, this.#inputSurfaceCoordination) === "safe";
832
- if (this.#dockInputCandidate)
833
- this.#captureDockInputSnapshot();
841
+ this.#dockInputRevision = this.#dockInputCandidate ? this.#viewportController.presentationRevision : undefined;
834
842
  try {
835
843
  this.#inputSurface.handleInput?.(data);
836
844
  }
@@ -839,23 +847,6 @@ export class OwnedUiSessionShellRoot {
839
847
  throw error;
840
848
  }
841
849
  }
842
- #captureDockInputSnapshot() {
843
- const width = Math.max(1, this.#componentRuntime.getColumns());
844
- const steeringRows = this.#renderQueued(width);
845
- const statusRows = this.#renderStatus(width);
846
- const transientSignature = transientRowsSignature(steeringRows, statusRows);
847
- const snapshot = this.#visibleViewportSnapshot;
848
- this.#dockInputSnapshot = snapshot === undefined
849
- ? undefined
850
- : {
851
- documentRows: snapshot.documentRows,
852
- transientSignature,
853
- promptAnchors: snapshot.promptAnchors,
854
- inputSurface: this.#inputSurface,
855
- viewportRevision: this.#viewportController.presentationRevision,
856
- selectionRevision: this.#viewportController.selectionRevision,
857
- };
858
- }
859
850
  invalidate() {
860
851
  this.header.invalidate();
861
852
  this.resources.invalidate();
@@ -884,7 +875,7 @@ export class OwnedUiSessionShellRoot {
884
875
  this.#documentLayouts.clear();
885
876
  this.#visibleViewportSnapshot = undefined;
886
877
  this.#dockInputCandidate = false;
887
- this.#dockInputSnapshot = undefined;
878
+ this.#dockInputRevision = undefined;
888
879
  if (this.#inputSurface !== this.editor)
889
880
  this.#inputSurface.dispose?.();
890
881
  this.#extensionHeader?.dispose?.();
@@ -37,7 +37,7 @@ export declare class OwnedUiSessionShell {
37
37
  showLoginMethodSelector(providerReference: string): void;
38
38
  showTreeSelector(initialSelectedId?: string): void;
39
39
  showLoginAuthTypeSelector(): void;
40
- showLoginProviderSelector(authType: "oauth" | "api_key"): void;
40
+ showLoginProviderSelector(authType?: "oauth" | "api_key", initialSearchInput?: string): void;
41
41
  showSessionSelector(): void;
42
42
  showSettingsSelector(): void;
43
43
  shutdown(): Promise<AdapterCommandResult>;
@@ -139,6 +139,7 @@ export class OwnedUiSessionShell {
139
139
  }, this.backend.agentDir, {
140
140
  getMessageRenderer: customType => this.backend.pinnedMessageRenderer(customType),
141
141
  getToolDefinition: toolName => this.backend.pinnedToolDefinition(toolName),
142
+ getShortcuts: bindings => this.backend.pinnedShortcutDescriptions(bindings),
142
143
  }, options.sessionLayout, {
143
144
  resolve: assetId => this.backend.resolveTranscriptImage(assetId),
144
145
  });
@@ -237,6 +238,17 @@ export class OwnedUiSessionShell {
237
238
  ? this.runtime.addPreInputListener(data => {
238
239
  if (options.inputPresentation?.coordination === false)
239
240
  this.#streamPresentation.noteImmediatePresentation();
241
+ // Compatibility: Pi's fullscreen renderer also intercepts plain Home/End.
242
+ // Deliver them to the focused owned input before that outer scroll handler;
243
+ // overlays retain Pi's normal dispatch, and comparison profiles never enter here.
244
+ if (!this.runtime.hasOverlay() && (this.root.editor.matchesTerminalKey(data, "home")
245
+ || this.root.editor.matchesTerminalKey(data, "end"))) {
246
+ if (this.root.usesDefaultInputSurface())
247
+ this.root.handleViewportPreInput(data, true);
248
+ this.root.handleInput(data);
249
+ this.runtime.requestRender();
250
+ return { consume: true };
251
+ }
240
252
  // Invariant: an overlay or editor-replacement screen owns its entire pointer
241
253
  // surface. Letting the transcript pre-router inspect those reports
242
254
  // steals settings value menus and numeric +/- controls before the
@@ -793,8 +805,7 @@ export class OwnedUiSessionShell {
793
805
  showLoginMethodSelector(providerReference) {
794
806
  const method = this.backend.pinnedLoginMethodOptions(providerReference);
795
807
  if (method.options.length === 0) {
796
- this.root.appendWorkflowResult({ command: "login", outcome: "failed", message: `No authentication methods available for ${providerReference}` });
797
- this.runtime.requestRender();
808
+ this.showLoginProviderSelector(undefined, providerReference);
798
809
  return;
799
810
  }
800
811
  if (method.options.length === 1) {
@@ -836,6 +847,20 @@ export class OwnedUiSessionShell {
836
847
  initialFilterMode: context.filterMode,
837
848
  ...(initialSelectedId === undefined ? {} : { initialSelectedId }),
838
849
  onLabelChange: context.appendLabelChange,
850
+ onCopy: text => {
851
+ if (!text) {
852
+ this.root.appendWorkflowResult({ command: "tree", outcome: "failed", message: "Selected entry has no text to copy" });
853
+ this.runtime.requestRender();
854
+ return;
855
+ }
856
+ void this.backend.copyWorkflowText(text).then(() => {
857
+ this.root.appendWorkflowStatus("Copied selected message to clipboard");
858
+ this.runtime.requestRender();
859
+ }).catch(error => {
860
+ this.root.appendWorkflowResult({ command: "tree", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
861
+ this.runtime.requestRender();
862
+ });
863
+ },
839
864
  onCancel: close,
840
865
  onSelect: entryId => {
841
866
  close();
@@ -863,10 +888,10 @@ export class OwnedUiSessionShell {
863
888
  this.root.setInputSurface(component);
864
889
  this.runtime.requestRender();
865
890
  }
866
- showLoginProviderSelector(authType) {
891
+ showLoginProviderSelector(authType, initialSearchInput) {
867
892
  const options = this.backend.pinnedLoginOptions(authType);
868
893
  if (options.length === 0) {
869
- this.root.appendWorkflowStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
894
+ this.root.appendWorkflowStatus(authType === "oauth" ? "No subscription providers available." : authType === "api_key" ? "No API key providers available." : "No login providers available.");
870
895
  this.runtime.requestRender();
871
896
  return;
872
897
  }
@@ -879,8 +904,9 @@ export class OwnedUiSessionShell {
879
904
  void this.runWorkflow({ command: "login", argument: "", selection: id });
880
905
  }, () => {
881
906
  close();
882
- this.showLoginAuthTypeSelector();
883
- });
907
+ if (authType !== undefined)
908
+ this.showLoginAuthTypeSelector();
909
+ }, initialSearchInput);
884
910
  this.root.setInputSurface(component);
885
911
  this.runtime.requestRender();
886
912
  }
@@ -961,6 +987,24 @@ export class OwnedUiSessionShell {
961
987
  return this.runWorkflow({ command: "quit", argument: "" });
962
988
  }
963
989
  async runWorkflow(request) {
990
+ if (request.command === "login" && request.selection !== undefined) {
991
+ const setup = this.backend.pinnedAmbientAuthentication(request.selection);
992
+ if (setup) {
993
+ const close = () => {
994
+ this.root.setInputSurface(null);
995
+ this.runtime.requestRender();
996
+ };
997
+ const dialog = createPiShellLoginDialog({
998
+ getColumns: () => this.runtime.viewport().columns,
999
+ getRows: () => this.runtime.viewport().rows,
1000
+ requestRender: () => this.runtime.requestRender(),
1001
+ }, setup.providerId, close, setup.providerName, setup.title);
1002
+ dialog.showInfo(setup.message, [], true);
1003
+ this.root.setInputSurface(dialog);
1004
+ this.runtime.requestRender();
1005
+ return { outcome: "completed", diagnostic: null };
1006
+ }
1007
+ }
964
1008
  if (request.command === "scoped-models" && request.selection === undefined && request.confirmed === undefined) {
965
1009
  this.showScopedModelsSelector();
966
1010
  return { outcome: "completed", diagnostic: null };
@@ -1001,8 +1045,13 @@ export class OwnedUiSessionShell {
1001
1045
  return { outcome: "completed", diagnostic: null };
1002
1046
  }
1003
1047
  if (request.command === "reload") {
1048
+ const blocked = this.backend.reloadBlockedResult();
1049
+ if (blocked) {
1050
+ this.root.appendWorkflowResult(blocked);
1051
+ this.runtime.requestRender();
1052
+ return workflowAdapterResult(blocked);
1053
+ }
1004
1054
  this.root.resetExtensionUi();
1005
- this.root.resetWorkflowPresentation();
1006
1055
  }
1007
1056
  const shareSurface = request.command === "share"
1008
1057
  ? createPiShellOperationLoader({
@@ -1053,6 +1102,8 @@ export class OwnedUiSessionShell {
1053
1102
  return { outcome: "failed", diagnostic: `Owned controller missing for ${request.command}` };
1054
1103
  }
1055
1104
  if (request.command === "reload" && result.outcome === "completed") {
1105
+ this.root.resetWorkflowPresentation();
1106
+ this.root.editor.reloadKeybindings();
1056
1107
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
1057
1108
  }
1058
1109
  this.root.appendWorkflowResult(result);
@@ -28,6 +28,11 @@ export declare class SessionViewportController {
28
28
  get hasSelection(): boolean;
29
29
  get selectionRevision(): number;
30
30
  get presentationRevision(): number;
31
+ /** Latest reported coordinates; exposed without rendering for frame provenance and evidence. */
32
+ get pointerPosition(): {
33
+ readonly column: number;
34
+ readonly row: number;
35
+ } | undefined;
31
36
  get frame(): TranscriptViewportFrame | null;
32
37
  setEditorPointerFrame(frame: {
33
38
  readonly rowStart: number;