@timurproko/a1 0.1.8-dev.290 → 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 (112) hide show
  1. package/README.md +21 -0
  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 +13 -1
  6. package/dist/contracts/owned-ui/index.d.ts +1 -0
  7. package/dist/contracts/owned-ui/index.js +1 -0
  8. package/dist/contracts/owned-ui/prompt-history.d.ts +37 -0
  9. package/dist/contracts/owned-ui/prompt-history.js +23 -0
  10. package/dist/features/launch/profile-paths.d.ts +2 -0
  11. package/dist/features/launch/profile-paths.js +11 -0
  12. package/dist/features/prompt-history/index.d.ts +2 -0
  13. package/dist/features/prompt-history/index.js +2 -0
  14. package/dist/features/prompt-history/paths.d.ts +4 -0
  15. package/dist/features/prompt-history/paths.js +16 -0
  16. package/dist/features/prompt-history/service.d.ts +24 -0
  17. package/dist/features/prompt-history/service.js +260 -0
  18. package/dist/features/prompt-history/store.d.ts +15 -0
  19. package/dist/features/prompt-history/store.js +166 -0
  20. package/dist/features/prompt-history/worker.d.ts +9 -0
  21. package/dist/features/prompt-history/worker.js +42 -0
  22. package/dist/foundation/launch-context/index.d.ts +29 -0
  23. package/dist/foundation/launch-context/index.js +83 -0
  24. package/dist/foundation/launch-guardian/main.js +4 -3
  25. package/dist/foundation/release/bootstrap.d.ts +1 -1
  26. package/dist/foundation/release/bootstrap.js +18 -12
  27. package/dist/foundation/release/cohort-state.d.ts +1 -0
  28. package/dist/foundation/release/cohort-state.js +4 -0
  29. package/dist/foundation/release/release-gc.js +2 -2
  30. package/dist/foundation/release/release-store.js +10 -3
  31. package/dist/foundation/release/release.d.ts +3 -1
  32. package/dist/foundation/release/release.js +7 -3
  33. package/dist/foundation/release/restart-certification.d.ts +1 -0
  34. package/dist/foundation/release/restart-certification.js +5 -0
  35. package/dist/foundation/release/update-recovery.d.ts +2 -0
  36. package/dist/foundation/release/update-recovery.js +4 -0
  37. package/dist/foundation/release/update.js +5 -0
  38. package/dist/foundation/release/warmup.d.ts +1 -1
  39. package/dist/foundation/release/warmup.js +5 -4
  40. package/dist/foundation/startup/startup-runtime.js +13 -11
  41. package/dist/foundation/supervision/main.js +8 -7
  42. package/dist/integrations/pi/components/editor-interaction.d.ts +38 -0
  43. package/dist/integrations/pi/components/editor-interaction.js +1 -0
  44. package/dist/integrations/pi/components/history-editor-loader.d.ts +5 -0
  45. package/dist/integrations/pi/components/history-editor-loader.js +5 -0
  46. package/dist/integrations/pi/components/index.d.ts +1 -0
  47. package/dist/integrations/pi/components/index.js +1 -0
  48. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  49. package/dist/integrations/pi/components/owned-editor-ux.js +95 -22
  50. package/dist/integrations/pi/components/shell-editor-autocomplete.js +12 -1
  51. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  52. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  53. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  54. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  55. package/dist/integrations/pi/components/shell-shared-facade.d.ts +15 -2
  56. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  57. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +13 -9
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +137 -133
  60. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +280 -0
  61. package/dist/integrations/pi/components/upstream/history/editor-core.js +2062 -0
  62. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +32 -0
  63. package/dist/integrations/pi/components/upstream/history/kill-ring.js +48 -0
  64. package/dist/integrations/pi/components/upstream/history/printable-key.d.ts +1 -0
  65. package/dist/integrations/pi/components/upstream/history/printable-key.js +40 -0
  66. package/dist/integrations/pi/components/upstream/history/text-helpers.d.ts +14 -0
  67. package/dist/integrations/pi/components/upstream/history/text-helpers.js +28 -0
  68. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +22 -0
  69. package/dist/integrations/pi/components/upstream/history/undo-stack.js +36 -0
  70. package/dist/integrations/pi/components/upstream/history/word-navigation.d.ts +24 -0
  71. package/dist/integrations/pi/components/upstream/history/word-navigation.js +100 -0
  72. package/dist/integrations/pi/engine/adapter.d.ts +13 -1
  73. package/dist/integrations/pi/engine/adapter.js +86 -25
  74. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  75. package/dist/integrations/pi/engine/changelog.js +63 -0
  76. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  77. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  78. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  79. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +4 -1
  80. package/dist/integrations/pi/session-ui/prompt-chips.js +40 -13
  81. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +30 -0
  82. package/dist/integrations/pi/session-ui/prompt-history-controller.js +147 -0
  83. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +33 -0
  84. package/dist/integrations/pi/session-ui/session-shell-root.js +68 -74
  85. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  86. package/dist/integrations/pi/session-ui/session-shell.js +98 -12
  87. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  88. package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
  89. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  90. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
  91. package/dist/native/darwin-arm64/manifest.json +1 -1
  92. package/dist/native/linux-x64/manifest.json +1 -1
  93. package/dist/native/win32-x64/manifest.json +2 -2
  94. package/dist/native/win32-x64/process-guardian.exe +0 -0
  95. package/dist/product-identity.d.ts +2 -2
  96. package/dist/product-identity.js +3 -36
  97. package/dist/product-identity.json +1 -22
  98. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  99. package/dist/ui/components/transcript-viewport.js +23 -2
  100. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  101. package/dist/ui/components/visible-hyperlinks.js +1 -0
  102. package/dist/ui/settings/declarations.js +19 -0
  103. package/dist/ui/settings/sections.js +11 -1
  104. package/dist/ui/settings/session.js +13 -4
  105. package/docs/architecture/boundaries.md +2 -0
  106. package/docs/architecture/history-editor-provenance.md +96 -0
  107. package/docs/architecture/internal-naming.md +89 -0
  108. package/docs/architecture/project-structure.md +1 -0
  109. package/docs/architecture/resource-and-data-policy.md +27 -1
  110. package/docs/architecture/toolchain.md +15 -1
  111. package/docs/features/prompt-history.md +149 -0
  112. package/package.json +8 -3
@@ -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);
@@ -204,6 +206,9 @@ export class OwnedUiSessionShellRoot {
204
206
  component.setImagePresentation(showImages, imageWidthCells);
205
207
  this.#invalidateTranscriptPresentation();
206
208
  }
209
+ prepareHistoryText(text) {
210
+ return this.#promptChips.prepareHistoryText(text);
211
+ }
207
212
  preparePromptSubmission(text) {
208
213
  return this.#promptChips.prepareSubmission(text);
209
214
  }
@@ -286,13 +291,12 @@ export class OwnedUiSessionShellRoot {
286
291
  const transientSignature = transientRowsSignature(steeringRows, statusRows);
287
292
  const snapshot = this.#visibleViewportSnapshot;
288
293
  const dockInputCandidate = this.#dockInputCandidate;
289
- const dockInputSnapshot = dockInputCandidate ? this.#dockInputSnapshot : undefined;
290
- const dockInputTransientSignature = dockInputCandidate
291
- ? dockInputSnapshot?.transientSignature ?? transientSignature
292
- : transientSignature;
293
- // Invariant: pending Steering and live Working rows share one non-selectable
294
- // viewport tail; only non-working status, widgets, input, and footer stay docked.
295
- 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;
296
300
  const dockRows = dock.rows;
297
301
  const selectableDocumentRowCount = document.rows.length;
298
302
  const dockStartRow = height - dockRows.length + 1;
@@ -308,28 +312,27 @@ export class OwnedUiSessionShellRoot {
308
312
  && snapshot !== undefined
309
313
  && snapshot.width === width
310
314
  && snapshot.height === height
311
- && snapshot.documentRows === (dockInputSnapshot?.documentRows ?? scrollRows)
312
- && snapshot.transientSignature === dockInputTransientSignature
313
- && snapshot.promptAnchors === (dockInputSnapshot?.promptAnchors ?? document.promptAnchors)
315
+ && snapshot.documentRows === document.rows
316
+ && snapshot.transientSignature === transientSignature
317
+ && snapshot.promptAnchors === document.promptAnchors
314
318
  && snapshot.dockLength === dockRows.length
315
- && snapshot.inputSurface === (dockInputSnapshot?.inputSurface ?? this.#inputSurface)
316
- && snapshot.viewportRevision === (dockInputSnapshot?.viewportRevision ?? this.#viewportController.presentationRevision)
317
- && snapshot.selectionRevision === (dockInputSnapshot?.selectionRevision ?? this.#viewportController.selectionRevision)
319
+ && snapshot.inputSurface === inputSurface
320
+ && snapshot.viewportRevision === viewportRevision
321
+ && snapshot.selectionRevision === selectionRevision
318
322
  ? this.#viewportController.composeDockOnly(dockRows, width, height)
319
323
  : null;
320
- // Performance: a spinner tick or queue update can race the input-triggered
321
- // render. Recompute the complete transient signature before reusing rows.
322
- if (frame !== null && dockInputTransientSignature !== transientSignature)
323
- frame = null;
324
324
  if (frame === null) {
325
325
  frame = this.#viewportController.compose({
326
- 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],
327
329
  ...(this.#viewportController.transcriptPointerSelecting
328
330
  ? { paintDocumentRow: heldNativeHyperlinkStyle }
329
331
  : {}),
330
332
  // Invariant: selection and copying stop at the real document tail; Steering,
331
333
  // fitting alignment, and live Working remain transient presentation chrome.
332
334
  selectableDocumentRowCount,
335
+ ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
333
336
  bottomAlignedTailRowCount: statusRows.length,
334
337
  dockRows,
335
338
  promptAnchors: document.promptAnchors,
@@ -347,13 +350,16 @@ export class OwnedUiSessionShellRoot {
347
350
  this.#visibleViewportSnapshot = {
348
351
  width,
349
352
  height,
350
- documentRows: scrollRows,
353
+ documentRows: document.rows,
351
354
  transientSignature,
352
355
  promptAnchors: document.promptAnchors,
353
356
  dockLength: dockRows.length,
354
- inputSurface: this.#inputSurface,
355
- viewportRevision: this.#viewportController.presentationRevision,
356
- 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,
357
363
  };
358
364
  this.#onViewportFrame?.(frame);
359
365
  return frame.rows;
@@ -361,6 +367,25 @@ export class OwnedUiSessionShellRoot {
361
367
  viewportFrameDescriptor() {
362
368
  return this.#viewportController.frame?.descriptor ?? null;
363
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
+ }
364
389
  /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
365
390
  viewportTransientTailRowCount() {
366
391
  return this.#viewportController.frame?.hits.transientTail.length ?? 0;
@@ -482,6 +507,10 @@ export class OwnedUiSessionShellRoot {
482
507
  ...resourceRows,
483
508
  ];
484
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;
485
514
  for (let index = 0; index < this.#transcriptOrder.length; index += 1) {
486
515
  const id = this.#transcriptOrder[index];
487
516
  const block = this.#blocksById.get(id);
@@ -504,6 +533,9 @@ export class OwnedUiSessionShellRoot {
504
533
  rows.push("");
505
534
  }
506
535
  const firstRow = rows.length;
536
+ if (liveTailStartRow === undefined && block !== undefined && block.status !== "finalized") {
537
+ liveTailStartRow = firstRow;
538
+ }
507
539
  rows.push(...blockRows);
508
540
  if (block?.kind === "user" && blockRows[0] !== undefined) {
509
541
  promptAnchors.push({
@@ -535,7 +567,7 @@ export class OwnedUiSessionShellRoot {
535
567
  text: diagnostic.message,
536
568
  payload: {},
537
569
  }, width, this.#cwd)));
538
- const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors) };
570
+ const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors), liveTailStartRow };
539
571
  if (this.#extensionHeader === null) {
540
572
  this.#documentLayouts.set(width, layout);
541
573
  // Performance: the custom viewport commonly probes full width and reserved-rail width.
@@ -603,7 +635,7 @@ export class OwnedUiSessionShellRoot {
603
635
  }
604
636
  const id = this.#appendAnchoredWorkflowComponent(width => [
605
637
  "",
606
- ...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width, this.#outputPad),
638
+ ...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width),
607
639
  ]);
608
640
  this.#workflowStatusMessages.set(id, message);
609
641
  this.#lastWorkflowStatusId = id;
@@ -614,10 +646,8 @@ export class OwnedUiSessionShellRoot {
614
646
  return;
615
647
  }
616
648
  this.#lastWorkflowStatusId = undefined;
617
- const prefix = message.kind === "warning" ? "Warning" : message.kind === "error" ? "Error" : undefined;
618
- const color = message.kind === "accent" ? "accent" : message.kind;
619
- const text = prefix === undefined ? message.message : `${prefix}: ${message.message}`;
620
- this.#appendAnchoredWorkflowComponent(() => ["", ` ${piTheme().fg(color, text)}`]);
649
+ const presentation = { kind: message.kind, message: message.message };
650
+ this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
621
651
  }
622
652
  appendWorkflowResult(result) {
623
653
  if (result.messages !== undefined) {
@@ -639,7 +669,7 @@ export class OwnedUiSessionShellRoot {
639
669
  }
640
670
  if (result.command === "hotkeys" && result.outcome === "completed") {
641
671
  this.#lastWorkflowStatusId = undefined;
642
- const hotkeys = createPiShellHotkeys();
672
+ const hotkeys = createPiShellHotkeys(this.editor.keybindingConfig(), bindings => this.#extensionRenderers.getShortcuts?.(bindings) ?? [], this.#customViewport ? "a1" : "pi");
643
673
  this.#appendAnchoredWorkflowComponent(width => hotkeys.render(width), () => hotkeys.dispose?.());
644
674
  return;
645
675
  }
@@ -658,26 +688,10 @@ export class OwnedUiSessionShellRoot {
658
688
  }
659
689
  if (result.outcome === "completed" && (result.command === "quit" || result.command === "compact"))
660
690
  return;
661
- if (result.command === "new" && result.outcome === "completed") {
662
- this.appendWorkflowMessage({ kind: "accent", message: result.message });
663
- return;
664
- }
665
- if (result.command === "name" && result.outcome === "completed") {
691
+ if (result.outcome === "completed" && (result.command === "new" || result.command === "name" || result.command === "debug")) {
666
692
  this.#lastWorkflowStatusId = undefined;
667
- this.#appendAnchoredWorkflowComponent(() => [
668
- ...(result.detail ? ["", ` ${piTheme().fg("warning", `Warning: ${result.detail}`)}`] : []),
669
- "",
670
- ` ${piTheme().fg("dim", result.message)}`,
671
- ]);
672
- return;
673
- }
674
- if (result.command === "debug" && result.outcome === "completed") {
675
- this.#lastWorkflowStatusId = undefined;
676
- this.#appendAnchoredWorkflowComponent(() => [
677
- "",
678
- ` ${piTheme().fg("accent", result.message)}`,
679
- ...(result.detail ? [` ${piTheme().fg("muted", result.detail)}`] : []),
680
- ]);
693
+ const presentation = { kind: result.command, message: result.message, ...(result.detail === undefined ? {} : { detail: result.detail }) };
694
+ this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
681
695
  return;
682
696
  }
683
697
  if (result.command === "arminsayshi" && result.outcome === "completed") {
@@ -782,9 +796,7 @@ export class OwnedUiSessionShellRoot {
782
796
  this.appendWorkflowStatus(message);
783
797
  return;
784
798
  }
785
- this.#lastWorkflowStatusId = undefined;
786
- const prefix = type === "warning" ? "Warning" : "Error";
787
- this.#appendAnchoredWorkflowComponent(() => ["", ` ${piTheme().fg(type, `${prefix}: ${message}`)}`]);
799
+ this.appendWorkflowMessage({ kind: type, message });
788
800
  }
789
801
  extensionFooterData() {
790
802
  return {
@@ -826,8 +838,7 @@ export class OwnedUiSessionShellRoot {
826
838
  handleInput(data) {
827
839
  this.#dockInputCandidate = this.#customViewport && this.#dockInputReuseEnabled
828
840
  && classifyPiTuiInput(data, this.#inputSurfaceCoordination) === "safe";
829
- if (this.#dockInputCandidate)
830
- this.#captureDockInputSnapshot();
841
+ this.#dockInputRevision = this.#dockInputCandidate ? this.#viewportController.presentationRevision : undefined;
831
842
  try {
832
843
  this.#inputSurface.handleInput?.(data);
833
844
  }
@@ -836,23 +847,6 @@ export class OwnedUiSessionShellRoot {
836
847
  throw error;
837
848
  }
838
849
  }
839
- #captureDockInputSnapshot() {
840
- const width = Math.max(1, this.#componentRuntime.getColumns());
841
- const steeringRows = this.#renderQueued(width);
842
- const statusRows = this.#renderStatus(width);
843
- const transientSignature = transientRowsSignature(steeringRows, statusRows);
844
- const snapshot = this.#visibleViewportSnapshot;
845
- this.#dockInputSnapshot = snapshot === undefined
846
- ? undefined
847
- : {
848
- documentRows: snapshot.documentRows,
849
- transientSignature,
850
- promptAnchors: snapshot.promptAnchors,
851
- inputSurface: this.#inputSurface,
852
- viewportRevision: this.#viewportController.presentationRevision,
853
- selectionRevision: this.#viewportController.selectionRevision,
854
- };
855
- }
856
850
  invalidate() {
857
851
  this.header.invalidate();
858
852
  this.resources.invalidate();
@@ -881,7 +875,7 @@ export class OwnedUiSessionShellRoot {
881
875
  this.#documentLayouts.clear();
882
876
  this.#visibleViewportSnapshot = undefined;
883
877
  this.#dockInputCandidate = false;
884
- this.#dockInputSnapshot = undefined;
878
+ this.#dockInputRevision = undefined;
885
879
  if (this.#inputSurface !== this.editor)
886
880
  this.#inputSurface.dispose?.();
887
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>;
@@ -1,3 +1,4 @@
1
+ import { PromptHistoryController } from "./prompt-history-controller.js";
1
2
  import { PRODUCT_TEXT } from "../../../product-identity.js";
2
3
  import { boundedCleanup } from "../../../foundation/terminal-cleanup/index.js";
3
4
  import { assertOwnedUiCommand, assertPromptImages, ImageAttachmentError } from "../../../contracts/owned-ui/index.js";
@@ -21,6 +22,7 @@ export class OwnedUiSessionShell {
21
22
  #unsubscribe;
22
23
  #unsubscribePromptSuggestions;
23
24
  #promptSuggestions;
25
+ #promptHistory = null;
24
26
  #extensionBridge;
25
27
  #stopped;
26
28
  #resolveStopped;
@@ -78,6 +80,8 @@ export class OwnedUiSessionShell {
78
80
  replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
79
81
  }),
80
82
  enableDockInputReuse: options.inputPresentation?.viewportReuse !== false,
83
+ persistentHistory: this.#customViewport && options.promptHistory !== undefined,
84
+ ...(options.promptHistory === undefined ? {} : { historyEditor: options.promptHistory.editor }),
81
85
  onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
82
86
  onPasteRejected: error => this.#reportSubmissionError(error),
83
87
  onInterrupt: () => { void this.interrupt(); },
@@ -98,6 +102,7 @@ export class OwnedUiSessionShell {
98
102
  onInputSurfaceChanged: () => {
99
103
  this.root.clearViewportPointerState();
100
104
  promptSuggestionController?.invalidate();
105
+ this.#promptHistory?.synchronize();
101
106
  },
102
107
  onCopyText: text => {
103
108
  runtime?.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
@@ -134,6 +139,7 @@ export class OwnedUiSessionShell {
134
139
  }, this.backend.agentDir, {
135
140
  getMessageRenderer: customType => this.backend.pinnedMessageRenderer(customType),
136
141
  getToolDefinition: toolName => this.backend.pinnedToolDefinition(toolName),
142
+ getShortcuts: bindings => this.backend.pinnedShortcutDescriptions(bindings),
137
143
  }, options.sessionLayout, {
138
144
  resolve: assetId => this.backend.resolveTranscriptImage(assetId),
139
145
  });
@@ -232,6 +238,17 @@ export class OwnedUiSessionShell {
232
238
  ? this.runtime.addPreInputListener(data => {
233
239
  if (options.inputPresentation?.coordination === false)
234
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
+ }
235
252
  // Invariant: an overlay or editor-replacement screen owns its entire pointer
236
253
  // surface. Letting the transcript pre-router inspect those reports
237
254
  // steals settings value menus and numeric +/- controls before the
@@ -309,6 +326,17 @@ export class OwnedUiSessionShell {
309
326
  this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
310
327
  } },
311
328
  });
329
+ if (this.#customViewport && options.promptHistory !== undefined) {
330
+ this.#promptHistory = new PromptHistoryController({
331
+ editor: this.root.editor,
332
+ store: options.promptHistory.store,
333
+ limit: options.promptHistory.limit,
334
+ fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
335
+ active: () => this.root.usesDefaultInputSurface(),
336
+ render: () => this.runtime.requestRender(),
337
+ failure: message => this.root.addExtensionNotification(message, "warning"),
338
+ });
339
+ }
312
340
  this.#extensionBridge = createPiExtensionUiBridge({
313
341
  runtime: {
314
342
  getColumns: () => this.runtime.viewport().columns,
@@ -419,6 +447,7 @@ export class OwnedUiSessionShell {
419
447
  return;
420
448
  this.#started = true;
421
449
  this.runtime.start();
450
+ this.#promptHistory?.start();
422
451
  this.#syncTerminalProgress(this.view());
423
452
  if (this.#customViewport)
424
453
  this.#setPointerReporting(true);
@@ -472,6 +501,16 @@ export class OwnedUiSessionShell {
472
501
  for (const item of this.#waitingImages.values())
473
502
  item.controller.abort();
474
503
  }
504
+ #rememberInput(text, kind) {
505
+ this.root.editor.addToHistory(text);
506
+ if (this.#promptHistory !== null) {
507
+ const reusable = this.root.prepareHistoryText(text);
508
+ if (reusable.length === 0)
509
+ this.#promptHistory.rememberRecovery(text);
510
+ else
511
+ this.#promptHistory.capture(reusable, kind, this.#cwd, this.backend.sessionId);
512
+ }
513
+ }
475
514
  async #submit(text) {
476
515
  this.#promptSuggestions?.invalidate();
477
516
  const displayInput = text.trim();
@@ -486,7 +525,7 @@ export class OwnedUiSessionShell {
486
525
  const excludeFromContext = input.startsWith("!!");
487
526
  const command = input.slice(excludeFromContext ? 2 : 1).trim();
488
527
  if (command) {
489
- this.root.editor.addToHistory(displayInput);
528
+ this.#rememberInput(displayInput, "bash");
490
529
  try {
491
530
  const result = await this.backend.executeBashWorkflow(command, excludeFromContext);
492
531
  const workflow = {
@@ -508,7 +547,7 @@ export class OwnedUiSessionShell {
508
547
  }
509
548
  }
510
549
  if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
511
- this.root.editor.addToHistory(displayInput);
550
+ this.#rememberInput(displayInput, "steer");
512
551
  this.#compactionQueue.push({
513
552
  text: input,
514
553
  draft: displayInput,
@@ -520,7 +559,7 @@ export class OwnedUiSessionShell {
520
559
  return { outcome: "completed", diagnostic: null };
521
560
  }
522
561
  const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
523
- this.root.editor.addToHistory(displayInput);
562
+ this.#rememberInput(displayInput, type);
524
563
  this.root.resumeViewportFollowing();
525
564
  return this.#execute({
526
565
  type,
@@ -633,7 +672,7 @@ export class OwnedUiSessionShell {
633
672
  const prepared = this.root.preparePromptSubmission(displayInput);
634
673
  assertPromptImages(prepared.images);
635
674
  const text = prepared.text.trim();
636
- this.root.editor.addToHistory(displayInput);
675
+ this.#rememberInput(displayInput, "follow-up");
637
676
  if (this.root.editor.getText() === draft)
638
677
  this.root.editor.setText("");
639
678
  this.root.resumeViewportFollowing();
@@ -766,8 +805,7 @@ export class OwnedUiSessionShell {
766
805
  showLoginMethodSelector(providerReference) {
767
806
  const method = this.backend.pinnedLoginMethodOptions(providerReference);
768
807
  if (method.options.length === 0) {
769
- this.root.appendWorkflowResult({ command: "login", outcome: "failed", message: `No authentication methods available for ${providerReference}` });
770
- this.runtime.requestRender();
808
+ this.showLoginProviderSelector(undefined, providerReference);
771
809
  return;
772
810
  }
773
811
  if (method.options.length === 1) {
@@ -809,6 +847,20 @@ export class OwnedUiSessionShell {
809
847
  initialFilterMode: context.filterMode,
810
848
  ...(initialSelectedId === undefined ? {} : { initialSelectedId }),
811
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
+ },
812
864
  onCancel: close,
813
865
  onSelect: entryId => {
814
866
  close();
@@ -836,10 +888,10 @@ export class OwnedUiSessionShell {
836
888
  this.root.setInputSurface(component);
837
889
  this.runtime.requestRender();
838
890
  }
839
- showLoginProviderSelector(authType) {
891
+ showLoginProviderSelector(authType, initialSearchInput) {
840
892
  const options = this.backend.pinnedLoginOptions(authType);
841
893
  if (options.length === 0) {
842
- 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.");
843
895
  this.runtime.requestRender();
844
896
  return;
845
897
  }
@@ -852,8 +904,9 @@ export class OwnedUiSessionShell {
852
904
  void this.runWorkflow({ command: "login", argument: "", selection: id });
853
905
  }, () => {
854
906
  close();
855
- this.showLoginAuthTypeSelector();
856
- });
907
+ if (authType !== undefined)
908
+ this.showLoginAuthTypeSelector();
909
+ }, initialSearchInput);
857
910
  this.root.setInputSurface(component);
858
911
  this.runtime.requestRender();
859
912
  }
@@ -934,6 +987,24 @@ export class OwnedUiSessionShell {
934
987
  return this.runWorkflow({ command: "quit", argument: "" });
935
988
  }
936
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
+ }
937
1008
  if (request.command === "scoped-models" && request.selection === undefined && request.confirmed === undefined) {
938
1009
  this.showScopedModelsSelector();
939
1010
  return { outcome: "completed", diagnostic: null };
@@ -974,8 +1045,13 @@ export class OwnedUiSessionShell {
974
1045
  return { outcome: "completed", diagnostic: null };
975
1046
  }
976
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
+ }
977
1054
  this.root.resetExtensionUi();
978
- this.root.resetWorkflowPresentation();
979
1055
  }
980
1056
  const shareSurface = request.command === "share"
981
1057
  ? createPiShellOperationLoader({
@@ -1026,6 +1102,8 @@ export class OwnedUiSessionShell {
1026
1102
  return { outcome: "failed", diagnostic: `Owned controller missing for ${request.command}` };
1027
1103
  }
1028
1104
  if (request.command === "reload" && result.outcome === "completed") {
1105
+ this.root.resetWorkflowPresentation();
1106
+ this.root.editor.reloadKeybindings();
1029
1107
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
1030
1108
  }
1031
1109
  this.root.appendWorkflowResult(result);
@@ -1157,6 +1235,8 @@ export class OwnedUiSessionShell {
1157
1235
  failures.push(error);
1158
1236
  } };
1159
1237
  let pasteCleanup = Promise.resolve();
1238
+ let historyCleanup = Promise.resolve(true);
1239
+ attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
1160
1240
  attempt(() => { pasteCleanup = this.root.disposePendingPastes(); });
1161
1241
  attempt(() => this.root.clearViewportPointerState());
1162
1242
  attempt(() => this.#setPointerReporting(false, true));
@@ -1182,6 +1262,9 @@ export class OwnedUiSessionShell {
1182
1262
  attempt(() => this.#extensionBridge.dispose());
1183
1263
  // Invariant: terminal restoration precedes any potentially stalled backend teardown.
1184
1264
  await this.runtime.dispose().catch(error => failures.push(error));
1265
+ const historySaved = await historyCleanup.catch(() => false);
1266
+ if (!historySaved)
1267
+ this.runtime.writeAfterStop("Prompt history could not finish saving before exit.\n");
1185
1268
  await boundedCleanup(() => pasteCleanup).catch(error => failures.push(error));
1186
1269
  await boundedCleanup(() => this.backend.unbindExtensionUi()).catch(error => failures.push(error));
1187
1270
  if (failures.length > 0)
@@ -1219,6 +1302,7 @@ export class OwnedUiSessionShell {
1219
1302
  this.root.resetPendingPastes();
1220
1303
  this.#promptSuggestions?.invalidate();
1221
1304
  this.#sessionGeneration = this.backend.sessionGeneration;
1305
+ this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
1222
1306
  this.#activeLoginDialog = undefined;
1223
1307
  this.#extensionBridge.reset();
1224
1308
  // Invariant: a replaced session takes its transient viewport and owned-route state with it.
@@ -1377,7 +1461,7 @@ export class OwnedUiSessionShell {
1377
1461
  if (isWorkflowRoute(name))
1378
1462
  return this.runWorkflow({ command: name, argument });
1379
1463
  // Compatibility: unknown slash input, prompt templates, skills, and extension commands remain Pi prompt input.
1380
- this.root.editor.addToHistory(text);
1464
+ this.#rememberInput(text, "slash");
1381
1465
  this.root.resumeViewportFollowing();
1382
1466
  return this.#execute({
1383
1467
  type: this.view().lifecycle === "busy" ? "steer" : "prompt",
@@ -1523,6 +1607,7 @@ export class OwnedUiSessionShell {
1523
1607
  catch {
1524
1608
  // Security: dispatch might already have reached the provider. Never retry automatically.
1525
1609
  this.root.editor.addToHistory(draft);
1610
+ this.#promptHistory?.rememberRecovery(draft);
1526
1611
  this.#reportSubmissionError(undefined, "Submission failed; delivery is uncertain. Check the conversation before retrying. Press Up to recover the draft.");
1527
1612
  return { outcome: "failed", diagnostic: "submission delivery is uncertain" };
1528
1613
  }
@@ -1540,6 +1625,7 @@ export class OwnedUiSessionShell {
1540
1625
  }
1541
1626
  #recoverSubmission(draft, revision, error) {
1542
1627
  this.root.editor.addToHistory(draft);
1628
+ this.#promptHistory?.rememberRecovery(draft);
1543
1629
  // Concurrency: never overwrite input typed (even typed and cleared) after this submission.
1544
1630
  if (revision === this.#editorRevision && this.root.editor.getText().length === 0)
1545
1631
  this.root.editor.setText(draft);
@@ -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;
@@ -60,6 +60,10 @@ export class SessionViewportController {
60
60
  get presentationRevision() {
61
61
  return this.#presentationRevision;
62
62
  }
63
+ /** Latest reported coordinates; exposed without rendering for frame provenance and evidence. */
64
+ get pointerPosition() {
65
+ return this.#pointerPosition;
66
+ }
63
67
  get frame() {
64
68
  return this.#viewport.frame;
65
69
  }
@@ -159,6 +163,23 @@ export class SessionViewportController {
159
163
  this.#requestHyperlinkCleanup();
160
164
  this.#requestRender(true);
161
165
  }
166
+ // Invariant: content shortcuts never reach the prompt, even before the first frame
167
+ // or when scrolling is a no-op. Plain Home/End remain editor line navigation.
168
+ if (allowWheel && this.#editor.matchesTerminalKey(data, "ctrl+home")) {
169
+ if (this.#viewport.scrollTo(0, now)) {
170
+ this.#scheduleActivityExpiry();
171
+ this.#requestRender();
172
+ }
173
+ return { data: "", consumed: true };
174
+ }
175
+ if (allowWheel && this.#editor.matchesTerminalKey(data, "ctrl+end")) {
176
+ if (!this.#viewport.followingEnd) {
177
+ this.#viewport.scrollToEnd(now);
178
+ this.#scheduleActivityExpiry();
179
+ this.#requestRender();
180
+ }
181
+ return { data: "", consumed: true };
182
+ }
162
183
  if (this.#viewport.frame === null) {
163
184
  // Invariant: a not-yet-painted A1 screen is not Pi's selection surface.
164
185
  return routeMouseInput(data, event => {
@@ -183,21 +204,6 @@ export class SessionViewportController {
183
204
  }
184
205
  }
185
206
  }
186
- if (allowWheel && this.#editor.matchesTerminalKey(data, "home")) {
187
- if (this.#viewport.scrollTo(0, now)) {
188
- this.#scheduleActivityExpiry();
189
- this.#requestRender();
190
- }
191
- return { data: "", consumed: true };
192
- }
193
- if (allowWheel && this.#editor.matchesTerminalKey(data, "end")) {
194
- if (!this.#viewport.followingEnd) {
195
- this.#viewport.scrollToEnd(now);
196
- this.#scheduleActivityExpiry();
197
- this.#requestRender();
198
- }
199
- return { data: "", consumed: true };
200
- }
201
207
  if (allowWheel && (SHIFT_UP_INPUTS.has(data) || SHIFT_DOWN_INPUTS.has(data))) {
202
208
  const scrolled = SHIFT_UP_INPUTS.has(data)
203
209
  ? this.#viewport.scrollToPreviousPrompt(now)