@timurproko/a1 0.2.2-dev.611 → 0.2.2

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 (67) hide show
  1. package/README.md +16 -8
  2. package/bin/pinned-pi-public.js +11 -4
  3. package/dist/app/session-shell/index.d.ts +1 -1
  4. package/dist/app/session-shell/paste-executor.js +6 -1
  5. package/dist/app/session-shell/prompt-chips.d.ts +8 -0
  6. package/dist/app/session-shell/prompt-chips.js +58 -13
  7. package/dist/app/session-shell/session-shell-root.d.ts +21 -3
  8. package/dist/app/session-shell/session-shell-root.js +110 -17
  9. package/dist/app/session-shell/session-shell.d.ts +1 -1
  10. package/dist/app/session-shell/session-shell.js +76 -31
  11. package/dist/app/session-shell/session-viewport-controller.d.ts +13 -0
  12. package/dist/app/session-shell/session-viewport-controller.js +28 -1
  13. package/dist/cli/version-stats.js +4 -7
  14. package/dist/composition/owned-ui.d.ts +7 -0
  15. package/dist/composition/owned-ui.js +71 -3
  16. package/dist/contracts/owned-ui/image-attachments.d.ts +7 -4
  17. package/dist/contracts/owned-ui/image-attachments.js +20 -7
  18. package/dist/contracts/owned-ui/index.d.ts +1 -1
  19. package/dist/contracts/owned-ui/index.js +1 -1
  20. package/dist/contracts/owned-ui/prompt-chips.d.ts +2 -2
  21. package/dist/contracts/owned-ui/prompt-chips.js +9 -6
  22. package/dist/features/owned-ui/project-trust-prompt.js +39 -3
  23. package/dist/features/owned-ui/release-note-state.d.ts +13 -0
  24. package/dist/features/owned-ui/release-note-state.js +109 -0
  25. package/dist/features/owned-ui/release-notes.d.ts +10 -0
  26. package/dist/features/owned-ui/release-notes.js +73 -0
  27. package/dist/features/owned-ui/resources/release-notes.json +9 -0
  28. package/dist/foundation/release/latest-release.d.ts +55 -0
  29. package/dist/foundation/release/latest-release.js +166 -0
  30. package/dist/foundation/release/update.js +9 -0
  31. package/dist/integrations/pi/components/index.d.ts +1 -1
  32. package/dist/integrations/pi/components/index.js +1 -1
  33. package/dist/integrations/pi/components/prompt-chip-presentation.d.ts +3 -0
  34. package/dist/integrations/pi/components/prompt-chip-presentation.js +14 -0
  35. package/dist/integrations/pi/components/shell-footer-status.js +30 -9
  36. package/dist/integrations/pi/components/shell-presenters-info.d.ts +36 -0
  37. package/dist/integrations/pi/components/shell-presenters-info.js +77 -1
  38. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +0 -6
  39. package/dist/integrations/pi/components/shell-presenters-transcript.js +0 -19
  40. package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -1
  41. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  42. package/dist/integrations/pi/components/submitted-prompt-adapter.js +13 -4
  43. package/dist/integrations/pi/engine/adapter.d.ts +12 -0
  44. package/dist/integrations/pi/engine/adapter.js +13 -0
  45. package/dist/integrations/pi/engine/session-runtime.d.ts +1 -0
  46. package/dist/integrations/pi/engine/session-runtime.js +2 -1
  47. package/dist/native/darwin-arm64/manifest.json +1 -1
  48. package/dist/native/linux-x64/manifest.json +1 -1
  49. package/dist/native/win32-x64/manifest.json +2 -2
  50. package/dist/native/win32-x64/process-guardian.exe +0 -0
  51. package/dist/product-identity.d.ts +1 -1
  52. package/dist/product-identity.js +1 -1
  53. package/dist/product-identity.json +1 -0
  54. package/dist/ui/settings/declarations.d.ts +25 -1
  55. package/dist/ui/settings/declarations.js +19 -1
  56. package/dist/ui/settings/migrations.js +14 -0
  57. package/docs/architecture/internal-naming.md +2 -2
  58. package/docs/architecture/resource-and-data-policy.md +11 -0
  59. package/docs/architecture/toolchain.md +1 -1
  60. package/docs/architecture/ui-reference-provenance.md +1 -0
  61. package/docs/ci-release-runbook.md +154 -70
  62. package/docs/local-worktree-cleanup.md +21 -4
  63. package/docs/manual-owned-ui-checkpoint.md +1 -0
  64. package/docs/openspec-archive-automation.md +1 -1
  65. package/docs/releases/0.2.2.md +47 -0
  66. package/docs/validation.md +13 -3
  67. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { acceptsTranscriptUpdate } from "../../contracts/owned-ui/index.js";
1
+ import { acceptsTranscriptUpdate, ImageAttachmentError } from "../../contracts/owned-ui/index.js";
2
2
  import { caretCell } from "../../ui/components/line-input.js";
3
3
  import { PromptInput, promptArrow } from "../../ui/components/prompt-input.js";
4
4
  import { composeSubmittedPromptRows, formatSubmittedPromptTime, submittedPromptLayout, } from "../../ui/components/submitted-prompt.js";
@@ -10,8 +10,8 @@ import { createPiExtensionUiBridge } from "../../integrations/pi/components/shel
10
10
  import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
11
11
  import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
12
12
  import { createPiShellArmin, createPiShellAuthProviderSelector, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
13
- import { createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellCommandMessage, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
14
- import { createPiShellTranscriptComponent, isPiPromptStyleCompaction, paintPiSubmittedPromptTimestamp, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
13
+ import { createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellCommandMessage, renderPiShellPackageUpdateNotice, renderPiShellReleaseUpdateBanner, renderPiShellReleaseUpdateNotice, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
14
+ import { createPiShellTranscriptComponent, isPiPromptStyleCompaction, paintPiSubmittedPromptTimestamp, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
15
15
  import { onPiThemeChange, PINNED_PI_LAYOUT, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
16
16
  import { piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, } from "../../integrations/pi/components/shell-shared-facade.js";
17
17
  import { classifyPiTuiInput, } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
@@ -62,6 +62,9 @@ export class OwnedUiSessionShellRoot {
62
62
  #lastWorkflowStatusId;
63
63
  // Invariant: the notice is dock chrome, never transcript content; the custom viewport alone uses it.
64
64
  #dockNotice;
65
+ // Invariant: bare A1 docks the release notice above the live status; closing it lasts for this session only.
66
+ #releaseNoticeDismissed = false;
67
+ #releaseCloseTarget;
65
68
  #copyAcknowledgement;
66
69
  #copyAcknowledgementTimer;
67
70
  #inputSurface;
@@ -80,6 +83,7 @@ export class OwnedUiSessionShellRoot {
80
83
  #extensionStatuses = new Map();
81
84
  #extensionWorkingMessage;
82
85
  #extensionWorkingVisible = true;
86
+ #imageSubmissionsSending = 0;
83
87
  constructor(view, cwd, handlers, startup = {}, agentDir, extensionRenderers = {
84
88
  getMessageRenderer: () => undefined,
85
89
  getToolDefinition: () => undefined,
@@ -88,7 +92,8 @@ export class OwnedUiSessionShellRoot {
88
92
  this.#customViewport = sessionLayout === "custom-viewport";
89
93
  this.#promptChips = new PromptChipStore({ isolated: this.#customViewport,
90
94
  ...(handlers.pasteDiagnostics === undefined ? {} : { onEvent: handlers.pasteDiagnostics }),
91
- ...(handlers.pastePreparation === undefined ? {} : { preparation: handlers.pastePreparation }) });
95
+ ...(handlers.pastePreparation === undefined ? {} : { preparation: handlers.pastePreparation }),
96
+ ...(handlers.promptImageLimit === undefined ? {} : { imageLimit: handlers.promptImageLimit }) });
92
97
  this.#submittedPromptComposer = this.#customViewport
93
98
  ? {
94
99
  layout: submittedPromptLayout,
@@ -150,18 +155,19 @@ export class OwnedUiSessionShellRoot {
150
155
  if (rowIndex === 0)
151
156
  this.#editorHyperlinks.reset();
152
157
  const plain = stripAnsi(row);
158
+ const dimmed = this.#promptChips.unsentRanges(plain).reduce((result, range) => backgroundSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), "\u001b[2m", "\u001b[22m", piShellVisibleWidth), row);
153
159
  const ranges = this.#promptChips.hyperlinkRanges(plain);
154
160
  if (ranges.length === 0 || !this.#editorHyperlinks.takeCleanup())
155
- return row;
156
- const linkResetAndTail = `\u001b]8;;\u001b\\\u001b[24m${" ".repeat(Math.max(0, width - piShellVisibleWidth(row)))}`;
161
+ return dimmed;
162
+ const linkResetAndTail = `\u001b]8;;\u001b\\\u001b[24m${" ".repeat(Math.max(0, width - piShellVisibleWidth(dimmed)))}`;
157
163
  // Platform: VS15 is zero-column and default-ignorable. It breaks Windows Terminal's
158
164
  // plain-text URL detector only in the held-button paint; semantic text stays exact.
159
165
  const paintRow = this.#viewportController.editorPointerSelecting
160
- ? row.replaceAll("https://", "https:\uFE0E//").replaceAll("http://", "http:\uFE0E//")
161
- : row;
166
+ ? dimmed.replaceAll("https://", "https:\uFE0E//").replaceAll("http://", "http:\uFE0E//")
167
+ : dimmed;
162
168
  const decorated = this.#viewportController.editorPointerSelecting
163
169
  ? ranges.reduce((result, range) => backgroundSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), "\u001b[4:4m", "\u001b[24m", piShellVisibleWidth), paintRow)
164
- : ranges.filter(range => this.#editorHyperlinks.take(range.target)).reduce((result, range) => hyperlinkSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), range.target, piShellVisibleWidth), row);
170
+ : ranges.filter(range => this.#editorHyperlinks.take(range.target)).reduce((result, range) => hyperlinkSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), range.target, piShellVisibleWidth), dimmed);
165
171
  // Platform: Windows Terminal can retain stale native dotted-link cells when a mutable
166
172
  // prompt replaces a longer URL. Explicit non-link spaces overwrite that tail.
167
173
  return `${decorated}${linkResetAndTail}`;
@@ -265,6 +271,9 @@ export class OwnedUiSessionShellRoot {
265
271
  prepareHistoryText(text) {
266
272
  return this.#promptChips.prepareHistoryText(text);
267
273
  }
274
+ omitUnsentPromptImages(text) {
275
+ return this.#promptChips.omitUnsentImages(text);
276
+ }
268
277
  rehydrateHistoryText(text, resolveImage) {
269
278
  return this.#promptChips.rehydrateHistoryText(text, resolveImage);
270
279
  }
@@ -274,6 +283,12 @@ export class OwnedUiSessionShellRoot {
274
283
  preparePromptSubmission(text) {
275
284
  return this.#promptChips.prepareSubmission(text);
276
285
  }
286
+ reconcilePromptImageLimitNotice(text = this.editor.getText()) {
287
+ if (this.#dockNotice?.errorCode !== "image-count" || !this.#promptChips.imageLimitState(text).corrected)
288
+ return false;
289
+ this.#dismissDockNotice();
290
+ return true;
291
+ }
277
292
  hasPendingPastes(text) { return this.#promptChips.hasPending(text); }
278
293
  waitForPromptPastes(text, signal) {
279
294
  return this.#promptChips.waitForPastes(text, signal, () => this.editor.getText());
@@ -316,7 +331,12 @@ export class OwnedUiSessionShellRoot {
316
331
  || view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
317
332
  this.#documentLayouts.clear();
318
333
  }
334
+ const promptAccepted = this.#view.lifecycle !== "busy" && view.lifecycle === "busy";
319
335
  this.#view = view;
336
+ if (promptAccepted && this.#imageSubmissionsSending > 0) {
337
+ this.#imageSubmissionsSending = 0;
338
+ this.#syncWorkingOverride();
339
+ }
320
340
  this.#status.update(view);
321
341
  this.#footer.update(this.#viewWithExtensionStatuses(view));
322
342
  this.#queued.update(view.editor.queuedSubmissions);
@@ -384,7 +404,11 @@ export class OwnedUiSessionShellRoot {
384
404
  : Math.max(1, width - 1);
385
405
  const document = this.#renderDocumentLayout(documentWidth);
386
406
  const steeringRows = this.#renderQueued(documentWidth);
387
- const statusRows = this.#renderStatus(documentWidth);
407
+ const releaseBanner = this.#renderReleaseBanner(documentWidth);
408
+ // Rationale: the release notice rides the bottom-aligned tail directly above Working, so it
409
+ // never displaces transcript content and scrolls away with the live status.
410
+ // Invariant: when idle the banner's last row sits on the line Working would occupy.
411
+ const statusRows = [...releaseBanner?.rows ?? [], ...this.#renderStatus(documentWidth)];
388
412
  const transientSignature = transientRowsSignature(steeringRows, statusRows);
389
413
  const snapshot = this.#visibleViewportSnapshot;
390
414
  // Invariant: paint-only feedback may cover either viewport or dock cells, so
@@ -459,6 +483,7 @@ export class OwnedUiSessionShellRoot {
459
483
  }
460
484
  else
461
485
  this.#dockOnlyViewportCompositions += 1;
486
+ this.#locateReleaseClose(frame, releaseBanner, document.rows.length + steeringRows.length + statusRows.length, statusRows.length);
462
487
  this.#visibleViewportSnapshot = {
463
488
  width,
464
489
  height,
@@ -598,6 +623,32 @@ export class OwnedUiSessionShellRoot {
598
623
  if (requestRender)
599
624
  this.#componentRuntime.requestRender();
600
625
  }
626
+ #renderReleaseBanner(width) {
627
+ if (this.#releaseNoticeDismissed)
628
+ return undefined;
629
+ const diagnostic = this.#view.diagnostics.findLast(item => item.code === "release-update");
630
+ const release = diagnostic === undefined ? null : parseReleaseUpdateDiagnostic(diagnostic.message);
631
+ if (release === null)
632
+ return undefined;
633
+ return renderPiShellReleaseUpdateBanner(release, width, this.#viewportController.tailControlHovered(this.#releaseCloseTarget));
634
+ }
635
+ #locateReleaseClose(frame, banner, tailEnd, tailLength) {
636
+ // Invariant: the tail is the document suffix, so the control's document row is fixed relative to its
637
+ // end; alignment gap rows precede it and the viewport scroll decides whether it is on screen.
638
+ const bannerStart = tailEnd + frame.descriptor.transientAlignmentGapRows - tailLength - frame.scrollTop + 1;
639
+ const rowStart = banner === undefined ? 0 : Math.max(1, bannerStart + banner.close.rowStart);
640
+ const rowEnd = banner === undefined ? 0 : Math.min(frame.hits.viewportHeight, bannerStart + banner.close.rowEnd);
641
+ this.#releaseCloseTarget = banner !== undefined && rowStart <= rowEnd
642
+ ? { rowStart, rowEnd, columnStart: banner.close.columnStart, columnEnd: banner.close.columnEnd }
643
+ : undefined;
644
+ this.#viewportController.setTailControl(this.#releaseCloseTarget === undefined ? undefined : {
645
+ ...this.#releaseCloseTarget,
646
+ activate: () => {
647
+ this.#releaseNoticeDismissed = true;
648
+ this.#releaseCloseTarget = undefined;
649
+ },
650
+ });
651
+ }
601
652
  #renderDockNotice(width) {
602
653
  const notice = this.#dockNotice;
603
654
  if (notice === undefined)
@@ -734,6 +785,14 @@ export class OwnedUiSessionShellRoot {
734
785
  });
735
786
  }
736
787
  }
788
+ // Invariant: the A1 release notice precedes the extension-package notice, matching pinned Pi's order.
789
+ // Bare A1 docks it in the viewport tail instead of the document.
790
+ rows.push(...diagnostics
791
+ .filter(diagnostic => !this.#customViewport && diagnostic.code === "release-update")
792
+ .flatMap(diagnostic => {
793
+ const release = parseReleaseUpdateDiagnostic(diagnostic.message);
794
+ return release === null ? [] : renderPiShellReleaseUpdateNotice(release, width);
795
+ }));
737
796
  rows.push(...diagnostics
738
797
  .filter(diagnostic => diagnostic.code === "package-updates")
739
798
  .flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
@@ -748,6 +807,7 @@ export class OwnedUiSessionShellRoot {
748
807
  : createPiShellChangelog(diagnostic.message).render(width)));
749
808
  rows.push(...diagnostics
750
809
  .filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "project-trust"
810
+ && diagnostic.code !== "release-update"
751
811
  && diagnostic.code !== "package-updates" && diagnostic.code !== "changelog-collapsed"
752
812
  && diagnostic.code !== "changelog-expanded")
753
813
  .slice(-3)
@@ -842,13 +902,17 @@ export class OwnedUiSessionShellRoot {
842
902
  this.#workflowStatusMessages.set(id, message);
843
903
  this.#lastWorkflowStatusId = id;
844
904
  }
845
- appendWorkflowMessage(message) {
905
+ appendWorkflowMessage(message, errorCode) {
846
906
  if (message.kind === "status") {
847
907
  this.appendWorkflowStatus(message.message);
848
908
  return;
849
909
  }
850
910
  if (this.#customViewport && (message.kind === "warning" || message.kind === "error")) {
851
- this.#dockNotice = { kind: message.kind, message: message.message };
911
+ this.#dockNotice = {
912
+ kind: errorCode === "image-count" ? "warning" : message.kind,
913
+ message: message.message,
914
+ ...(errorCode === undefined ? {} : { errorCode }),
915
+ };
852
916
  this.#invalidateChrome();
853
917
  return;
854
918
  }
@@ -856,10 +920,10 @@ export class OwnedUiSessionShellRoot {
856
920
  const presentation = { kind: message.kind, message: message.message };
857
921
  this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
858
922
  }
859
- appendWorkflowResult(result) {
923
+ appendWorkflowResult(result, errorCode) {
860
924
  if (result.messages !== undefined) {
861
925
  for (const message of result.messages)
862
- this.appendWorkflowMessage(message);
926
+ this.appendWorkflowMessage(message, errorCode);
863
927
  return;
864
928
  }
865
929
  if (result.messageKind === "silent" || (result.outcome === "cancelled" && result.messageKind === undefined))
@@ -891,7 +955,7 @@ export class OwnedUiSessionShellRoot {
891
955
  this.appendWorkflowMessage({
892
956
  kind: result.messageKind === "warning" ? "warning" : "error",
893
957
  message: result.message,
894
- });
958
+ }, errorCode);
895
959
  return;
896
960
  }
897
961
  if (result.outcome === "completed" && (result.command === "quit" || result.command === "compact"))
@@ -968,9 +1032,29 @@ export class OwnedUiSessionShellRoot {
968
1032
  setExtensionWorking(message, visible = this.#extensionWorkingVisible) {
969
1033
  this.#extensionWorkingMessage = message;
970
1034
  this.#extensionWorkingVisible = visible;
971
- this.#status.setWorkingOverride(visible ? message : undefined);
1035
+ this.#syncWorkingOverride();
972
1036
  this.invalidate();
973
1037
  }
1038
+ beginImageSubmissionStatus() {
1039
+ this.#imageSubmissionsSending++;
1040
+ this.#syncWorkingOverride();
1041
+ this.invalidate();
1042
+ let active = true;
1043
+ return () => {
1044
+ if (!active)
1045
+ return;
1046
+ active = false;
1047
+ this.#imageSubmissionsSending = Math.max(0, this.#imageSubmissionsSending - 1);
1048
+ this.#syncWorkingOverride();
1049
+ this.invalidate();
1050
+ };
1051
+ }
1052
+ #syncWorkingOverride() {
1053
+ const sending = this.#imageSubmissionsSending > 0;
1054
+ this.#status.setWorkingOverride(sending
1055
+ ? "Sending…"
1056
+ : this.#extensionWorkingVisible ? this.#extensionWorkingMessage : undefined, sending);
1057
+ }
974
1058
  hotkeysPresentation() {
975
1059
  return {
976
1060
  bindings: this.editor.keybindingConfig(),
@@ -1002,7 +1086,7 @@ export class OwnedUiSessionShellRoot {
1002
1086
  this.#extensionWidgets.clear();
1003
1087
  this.#extensionStatuses.clear();
1004
1088
  this.#extensionWorkingMessage = undefined;
1005
- this.#status.setWorkingOverride(undefined);
1089
+ this.#syncWorkingOverride();
1006
1090
  this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
1007
1091
  // Invariant: an extension renderer may have drawn transcript blocks that are now unrendered by it.
1008
1092
  this.#renderedRows.clear();
@@ -1439,6 +1523,15 @@ function isPackageExtensionSource(sourceInfo) {
1439
1523
  function transientRowsSignature(steeringRows, statusRows) {
1440
1524
  return `${steeringRows.length}\u0000${steeringRows.join("\u0000")}\u0001${statusRows.length}\u0000${statusRows.join("\u0000")}`;
1441
1525
  }
1526
+ /** The release, command, and optional changelog carried by the adapter's `release-update` diagnostic. */
1527
+ function parseReleaseUpdateDiagnostic(message) {
1528
+ const [instruction, changelog] = message.split("\n");
1529
+ const match = /^New version (\S+) is available\. Run (.+)$/.exec(instruction ?? "");
1530
+ if (match === null)
1531
+ return null;
1532
+ const url = changelog === undefined ? null : /^Changelog: (\S+)$/.exec(changelog)?.[1] ?? null;
1533
+ return { version: match[1], command: match[2], changelogUrl: url };
1534
+ }
1442
1535
  /** "Share URL: <url>" with the URL made clickable; other share wordings pass through unchanged. */
1443
1536
  function linkShareUrl(message) {
1444
1537
  const match = /^Share URL: (\S+)$/.exec(message);
@@ -8,7 +8,7 @@ import { type PiTuiDamageDecision } from "../../integrations/pi/tui-runtime/dama
8
8
  import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
9
9
  import type { PiTuiOverlayHandle } from "../../integrations/pi/tui-runtime/contracts.js";
10
10
  import { OwnedUiSessionShellRoot, type OwnedUiBackendPort, type OwnedUiSessionShellOptions } from "./session-shell-root.js";
11
- export { OwnedUiSessionShellRoot, type OwnedUiSessionShellOptions, type OwnedUiShellDiagnosticOptions, type OwnedUiShellEngineOptions, type OwnedUiShellHistoryOptions, type OwnedUiShellPresentationOptions, type OwnedUiShellSkillsOptions, type OwnedUiShellSuggestionOptions, } from "./session-shell-root.js";
11
+ export { OwnedUiSessionShellRoot, type OwnedUiSessionShellOptions, type OwnedUiShellDiagnosticOptions, type OwnedUiShellEngineOptions, type OwnedUiShellHistoryOptions, type OwnedUiShellPresentationOptions, type OwnedUiShellPromptImagesOptions, type OwnedUiShellSkillsOptions, type OwnedUiShellSuggestionOptions, } from "./session-shell-root.js";
12
12
  /** Coordinates backend, owned presentation, and Pi TUI lifecycles for one interactive session. */
13
13
  export declare class OwnedUiSessionShell {
14
14
  #private;
@@ -15,8 +15,8 @@ import { createPiShellEditor } from "../../integrations/pi/components/shell-edit
15
15
  import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
16
16
  import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
17
17
  import { createPiShellArmin, createPiShellAuthProviderSelector, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellSkillsSelector, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
18
- import { createPiShellChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
19
- import { createPiShellTranscriptComponent, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
18
+ import { createPiShellChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellPackageUpdateNotice, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
19
+ import { createPiShellTranscriptComponent, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
20
20
  import { onPiThemeChange, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
21
21
  import { DamageAwareTerminalAdapter } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
22
22
  import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
@@ -36,7 +36,9 @@ export class OwnedUiSessionShell {
36
36
  #unsubscribe;
37
37
  #unsubscribePromptSuggestions;
38
38
  #promptSuggestions;
39
+ #promptImages;
39
40
  #skills;
41
+ #unsubscribePromptImages;
40
42
  #unsubscribeSkills;
41
43
  #installedCommandSignature = "";
42
44
  #promptHistory = null;
@@ -47,6 +49,8 @@ export class OwnedUiSessionShell {
47
49
  #dialogId;
48
50
  #dialogSource;
49
51
  #routeHost;
52
+ #startupRoute;
53
+ #startupRouteTimer;
50
54
  #dialogHandle;
51
55
  #sequence = 0;
52
56
  #editorRevision = 0;
@@ -56,7 +60,6 @@ export class OwnedUiSessionShell {
56
60
  #disposePromise;
57
61
  #pointerReporting = false;
58
62
  // Invariant: translated startup diagnostics create at most one dock notice per kind per shell.
59
- #startupChangelogHandled = false;
60
63
  #startupTrustHandled = false;
61
64
  #customViewport;
62
65
  #responseCopy;
@@ -95,6 +98,7 @@ export class OwnedUiSessionShell {
95
98
  this.#suggestionModelKey = modelKey(this.backend.view());
96
99
  this.#cwd = cwd;
97
100
  this.#routeHost = routeHost ?? null;
101
+ this.#startupRoute = options.engine.startupRoute;
98
102
  this.#customViewport = sessionLayout === "custom-viewport";
99
103
  this.#stopped = new Promise(resolve => {
100
104
  this.#resolveStopped = resolve;
@@ -104,7 +108,8 @@ export class OwnedUiSessionShell {
104
108
  let streamPresentation;
105
109
  let pendingClipboardWrite = Promise.resolve();
106
110
  let promptSuggestionController = null;
107
- // Invariant: the collapsed skills presentation is a bare-A1 replacement; comparison profiles keep the pinned list.
111
+ // Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
112
+ this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
108
113
  this.#skills = this.#customViewport ? options.skills ?? null : null;
109
114
  const terminalCopy = terminal !== undefined || hasAsyncClipboardOutput();
110
115
  this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
@@ -180,7 +185,11 @@ export class OwnedUiSessionShell {
180
185
  onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
181
186
  onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
182
187
  onDequeue: () => this.restoreQueuedInput(),
183
- onEditorChange: () => { this.#editorRevision++; promptSuggestionController?.abortPending(); },
188
+ onEditorChange: text => {
189
+ this.#editorRevision++;
190
+ promptSuggestionController?.abortPending();
191
+ this.root.reconcilePromptImageLimitNotice(text);
192
+ },
184
193
  onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
185
194
  onInputSurfaceChanged: () => {
186
195
  promptSuggestionController?.invalidate();
@@ -217,6 +226,7 @@ export class OwnedUiSessionShell {
217
226
  },
218
227
  ...(pasteDiagnostics === undefined ? {} : { pasteDiagnostics: pasteDiagnostics }),
219
228
  ...(pastePreparation === undefined ? {} : { pastePreparation }),
229
+ ...(this.#promptImages === null ? {} : { promptImageLimit: this.#promptImages.limit }),
220
230
  skillsPresentation: () => this.#skills?.presentation() ?? "expand",
221
231
  }, {
222
232
  ...startup,
@@ -302,6 +312,10 @@ export class OwnedUiSessionShell {
302
312
  this.#unsubscribePromptSuggestions = promptSuggestionOptions === undefined
303
313
  ? () => { }
304
314
  : promptSuggestionOptions.onChange(enabled => this.#promptSuggestions?.setEnabled(enabled));
315
+ this.#unsubscribePromptImages = this.#promptImages === null ? () => { } : this.#promptImages.onChange(() => {
316
+ if (!this.#disposed && this.root.reconcilePromptImageLimitNotice())
317
+ this.runtime.requestRender();
318
+ });
305
319
  // Rationale: the same listener refreshes the menu for the A1 presentation choice and for the engine's
306
320
  // skill-command registration, both of which the owned settings manager reports through one change.
307
321
  this.#unsubscribeSkills = this.#skills === null ? () => { } : this.#skills.onChange(() => {
@@ -586,6 +600,7 @@ export class OwnedUiSessionShell {
586
600
  setImmediate(() => {
587
601
  if (this.#disposed || !this.#customViewport)
588
602
  return;
603
+ this.#presentStartupRoute();
589
604
  this.root.warmPastePreparation();
590
605
  this.#copyExecutor?.warm();
591
606
  });
@@ -698,15 +713,21 @@ export class OwnedUiSessionShell {
698
713
  // Compatibility: match interactive Pi: input during compaction is queued steering; the engine
699
714
  // shows it in the pending rows and delivers it when compaction ends.
700
715
  const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
701
- this.#rememberInput(displayInput, type);
716
+ this.#rememberInput(this.root.omitUnsentPromptImages(displayInput).trim(), type);
702
717
  this.root.resumeViewportFollowing();
703
- return this.#execute({
704
- type,
705
- correlationId: this.#correlation(type),
706
- sessionId: this.backend.sessionId,
707
- text: input,
708
- ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
709
- }, displayInput);
718
+ const finishSending = prepared.images.length === 0 ? () => { } : this.root.beginImageSubmissionStatus();
719
+ try {
720
+ return await this.#execute({
721
+ type,
722
+ correlationId: this.#correlation(type),
723
+ sessionId: this.backend.sessionId,
724
+ text: input,
725
+ ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
726
+ }, displayInput);
727
+ }
728
+ finally {
729
+ finishSending();
730
+ }
710
731
  }
711
732
  async clearOrExit(now = Date.now()) {
712
733
  this.#promptSuggestions?.abortPending();
@@ -1559,8 +1580,14 @@ export class OwnedUiSessionShell {
1559
1580
  attempt(() => this.#setPointerReporting(false, true));
1560
1581
  attempt(() => this.#removeViewportPreInput());
1561
1582
  attempt(() => this.#streamPresentation.dispose());
1583
+ attempt(() => {
1584
+ if (this.#startupRouteTimer !== undefined)
1585
+ clearTimeout(this.#startupRouteTimer);
1586
+ this.#startupRouteTimer = undefined;
1587
+ });
1562
1588
  attempt(() => this.#promptSuggestions?.dispose());
1563
1589
  attempt(() => this.#unsubscribePromptSuggestions());
1590
+ attempt(() => this.#unsubscribePromptImages());
1564
1591
  attempt(() => this.#unsubscribeSkills());
1565
1592
  attempt(() => this.#unsubscribeSettings());
1566
1593
  let fullscreenExitText = "";
@@ -1706,25 +1733,12 @@ export class OwnedUiSessionShell {
1706
1733
  }
1707
1734
  this.root.update(view);
1708
1735
  this.#syncDialog(view.dialog);
1709
- this.#presentStartupChangelog(view);
1710
1736
  this.#presentStartupTrustWarning(view);
1711
1737
  this.runtime.requestRender();
1712
1738
  for (const listener of this.#listeners)
1713
1739
  listener(view);
1714
1740
  return view;
1715
1741
  }
1716
- // Rationale: release details stay one explicit command away; startup needs only the same quiet,
1717
- // non-transcript acknowledgement that bare A1 uses for model and thinking changes.
1718
- #presentStartupChangelog(view) {
1719
- if (this.#startupChangelogHandled || !this.#customViewport)
1720
- return;
1721
- const diagnostic = view.diagnostics.find(candidate => candidate.code === "changelog-collapsed" || candidate.code === "changelog-expanded");
1722
- // Invariant: a view synchronized before the runtime starts is not the moment of arrival; start() replays it.
1723
- if (diagnostic === undefined || !this.runtime.active)
1724
- return;
1725
- this.#startupChangelogHandled = true;
1726
- this.root.appendWorkflowStatus(STARTUP_CHANGELOG_NOTICE);
1727
- }
1728
1742
  // Rationale: a cancelled or failed trust choice explains the restricted session where the
1729
1743
  // user will next type, rather than leaving a lone warning at the top of an empty viewport.
1730
1744
  #presentStartupTrustWarning(view) {
@@ -1736,7 +1750,22 @@ export class OwnedUiSessionShell {
1736
1750
  this.#startupTrustHandled = true;
1737
1751
  this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
1738
1752
  }
1739
- #openOwnedRoute(route, input) {
1753
+ #presentStartupRoute() {
1754
+ const pending = this.#startupRoute;
1755
+ if (pending === undefined || this.#disposed || !this.runtime.active)
1756
+ return;
1757
+ if (this.runtime.hasOverlay() || !this.root.usesDefaultInputSurface()) {
1758
+ this.#startupRouteTimer = setTimeout(() => {
1759
+ this.#startupRouteTimer = undefined;
1760
+ this.#presentStartupRoute();
1761
+ }, 25);
1762
+ this.#startupRouteTimer.unref?.();
1763
+ return;
1764
+ }
1765
+ this.#startupRoute = undefined;
1766
+ this.#openOwnedRoute(pending.route, pending.input, pending.onClosed);
1767
+ }
1768
+ #openOwnedRoute(route, input, onClosed) {
1740
1769
  const surface = this.#routeHost?.open(route, input) ?? null;
1741
1770
  if (surface === null)
1742
1771
  return { outcome: "failed", diagnostic: `route is unavailable: ${route}` };
@@ -1768,7 +1797,12 @@ export class OwnedUiSessionShell {
1768
1797
  return { consume: true };
1769
1798
  });
1770
1799
  let removeSurfacePreInput = () => { };
1800
+ let rendered = false;
1801
+ let closed = false;
1771
1802
  const closeSurface = () => {
1803
+ if (closed)
1804
+ return;
1805
+ closed = true;
1772
1806
  removeSurfacePreInput();
1773
1807
  removeInterruptWatch();
1774
1808
  this.#setPointerReporting(false);
@@ -1776,6 +1810,12 @@ export class OwnedUiSessionShell {
1776
1810
  this.#dialogHandle = undefined;
1777
1811
  this.#dialogId = undefined;
1778
1812
  this.#dialogSource = undefined;
1813
+ if (rendered) {
1814
+ try {
1815
+ void onClosed?.()?.catch(() => undefined);
1816
+ }
1817
+ catch { /* Rationale: a failed acknowledgement stays pending. */ }
1818
+ }
1779
1819
  };
1780
1820
  // Compatibility: fullscreen Pi owns a fallback text-selection layer before focused overlay
1781
1821
  // components see pointer input. Route every mouse report to the owned screen
@@ -1795,7 +1835,11 @@ export class OwnedUiSessionShell {
1795
1835
  });
1796
1836
  const rows = () => Math.max(1, this.runtime.viewport().rows);
1797
1837
  const component = {
1798
- render: (width) => [...surface.render(Math.max(1, width), rows())],
1838
+ render: (width) => {
1839
+ const frame = [...surface.render(Math.max(1, width), rows())];
1840
+ rendered = true;
1841
+ return frame;
1842
+ },
1799
1843
  handleInput: (data) => {
1800
1844
  const { events, rest } = parseMouseInput(data);
1801
1845
  for (const event of events)
@@ -2085,14 +2129,16 @@ export class OwnedUiSessionShell {
2085
2129
  if (revision === this.#editorRevision && this.root.editor.getText().length === 0)
2086
2130
  this.root.editor.setText(draft);
2087
2131
  const message = error instanceof ImageAttachmentError ? error.message : "Submission rejected. Check the prompt and attachments.";
2088
- this.#reportSubmissionError(error, `${message} Press Up to recover the draft.`);
2132
+ this.#reportSubmissionError(error, error instanceof ImageAttachmentError && error.code === "image-count"
2133
+ ? message
2134
+ : `${message} Press Up to recover the draft.`);
2089
2135
  return rejected(message);
2090
2136
  }
2091
2137
  #reportSubmissionError(error, message) {
2092
2138
  // Security: arbitrary provider/extension error messages can contain the entire request.
2093
2139
  try {
2094
2140
  this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message: message
2095
- ?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") });
2141
+ ?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") }, error instanceof ImageAttachmentError ? error.code : undefined);
2096
2142
  this.runtime.requestRender();
2097
2143
  }
2098
2144
  catch { /* Security: error presentation cannot create another rejected submission callback. */ }
@@ -2158,7 +2204,6 @@ function workflowAdapterResult(result) {
2158
2204
  const INTERRUPT = "\u0003";
2159
2205
  const INTERRUPT_CHORD_MS = 1_500;
2160
2206
  const RELOAD_SURFACE_MIN_VISIBLE_MS = 400;
2161
- const STARTUP_CHANGELOG_NOTICE = "Run /changelog to view the full release notes.";
2162
2207
  // Rationale: the outro is not configurable; the switch only decides whether this plan plays.
2163
2208
  const QUIT_OUTRO_EFFECT = "fall";
2164
2209
  const QUIT_OUTRO_DURATION_MS = 800;
@@ -15,6 +15,13 @@ export interface SessionViewportControllerOptions {
15
15
  readonly pasteDiagnostics?: (event: PasteEvent) => void;
16
16
  readonly nextPasteDiagnosticRequest?: () => number;
17
17
  }
18
+ /** One-based, inclusive terminal cells of a clickable viewport-tail control. */
19
+ export interface TailControlRegion {
20
+ readonly rowStart: number;
21
+ readonly rowEnd: number;
22
+ readonly columnStart: number;
23
+ readonly columnEnd: number;
24
+ }
18
25
  export interface SessionViewportInputResult {
19
26
  readonly data: string;
20
27
  readonly consumed: boolean;
@@ -45,6 +52,12 @@ export declare class SessionViewportController {
45
52
  readonly rowStart: number;
46
53
  readonly rowEnd: number;
47
54
  } | undefined): void;
55
+ /** The visible close control of a viewport-tail notice, or undefined when none is on screen. */
56
+ setTailControl(control: (TailControlRegion & {
57
+ readonly activate: () => void;
58
+ }) | undefined): void;
59
+ /** Whether the pointer currently rests on the tail control; drives its hover paint. */
60
+ tailControlHovered(target: TailControlRegion | undefined): boolean;
48
61
  /** Replacement input changed before its new bounds have been painted. */
49
62
  invalidateInputSurface(): void;
50
63
  setInputSurfaceFrame(surface: PiTuiPointerSurface | undefined): void;
@@ -3,6 +3,9 @@ import { TranscriptViewport } from "../../ui/components/transcript-viewport.js";
3
3
  import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
4
4
  import { routeMouseInput } from "../../ui/components/mouse.js";
5
5
  import { scrollForTrackPage, scrollForThumbRow, scrollbarSelectionRows, scrollbarWheelRows } from "../../ui/components/scrollbar.js";
6
+ function withinTailControl(region, column, row) {
7
+ return row >= region.rowStart && row <= region.rowEnd && column >= region.columnStart && column <= region.columnEnd;
8
+ }
6
9
  /**
7
10
  * Owns the stateful interaction policy of the bare-A1 session viewport. The
8
11
  * shell root supplies semantic rows and dock geometry; this controller owns
@@ -38,6 +41,8 @@ export class SessionViewportController {
38
41
  #pendingEditorClick;
39
42
  // Invariant: pointer routing uses the most recently composed editor rows.
40
43
  #editorPointerFrame;
44
+ // Invariant: one clickable region in the viewport tail, located by the most recent composition.
45
+ #tailControl;
41
46
  #selectionAutoScrollTimer;
42
47
  #selectionAutoScrollPointer;
43
48
  #pointerPosition;
@@ -89,6 +94,15 @@ export class SessionViewportController {
89
94
  setEditorPointerFrame(frame) {
90
95
  this.#editorPointerFrame = frame;
91
96
  }
97
+ /** The visible close control of a viewport-tail notice, or undefined when none is on screen. */
98
+ setTailControl(control) {
99
+ this.#tailControl = control;
100
+ }
101
+ /** Whether the pointer currently rests on the tail control; drives its hover paint. */
102
+ tailControlHovered(target) {
103
+ const pointer = this.#pointerPosition;
104
+ return target !== undefined && pointer !== undefined && withinTailControl(target, pointer.column, pointer.row);
105
+ }
92
106
  /** Replacement input changed before its new bounds have been painted. */
93
107
  invalidateInputSurface() {
94
108
  this.#inputSurfacePending = true;
@@ -360,6 +374,9 @@ export class SessionViewportController {
360
374
  const overSticky = overModal === undefined && hits.sticky !== null && event.row === hits.sticky.row && event.column <= hits.sticky.width;
361
375
  const overBottom = overModal === undefined && hits.bottom !== null && event.row === hits.bottom.row
362
376
  && event.column >= hits.bottom.columnStart && event.column <= hits.bottom.columnEnd;
377
+ const tailControl = this.#tailControl;
378
+ const overTailControl = overModal === undefined && tailControl !== undefined
379
+ && withinTailControl(tailControl, event.column, event.row);
363
380
  const previousPointer = this.#pointerPosition;
364
381
  const wasOverBottom = hits.bottom !== null && previousPointer !== undefined
365
382
  && previousPointer.row === hits.bottom.row
@@ -368,6 +385,9 @@ export class SessionViewportController {
368
385
  // Keep this location while the control is hidden; composition resolves its next hit region.
369
386
  this.#pointerPosition = { column: event.column, row: event.row };
370
387
  repaint ||= wasOverBottom !== overBottom;
388
+ const wasOverTailControl = tailControl !== undefined && previousPointer !== undefined
389
+ && withinTailControl(tailControl, previousPointer.column, previousPointer.row);
390
+ repaint ||= wasOverTailControl !== overTailControl;
371
391
  if (!this.#viewport.selectionActive && !this.#editorPointerSelecting) {
372
392
  const nextHyperlink = this.#hyperlinkKeyAt(frame, event.column, event.row);
373
393
  if (this.#hoveredHyperlinkKey !== undefined && nextHyperlink !== this.#hoveredHyperlinkKey) {
@@ -408,7 +428,7 @@ export class SessionViewportController {
408
428
  activity = true;
409
429
  return true;
410
430
  }
411
- return overRail || overSticky || overBottom;
431
+ return overRail || overSticky || overBottom || overTailControl;
412
432
  }
413
433
  if (event.kind === "wheel-up" || event.kind === "wheel-down") {
414
434
  if (!allowWheel || event.row < 1 || event.row > hits.viewportHeight)
@@ -442,6 +462,13 @@ export class SessionViewportController {
442
462
  this.#stopSelectionAutoScroll();
443
463
  if (this.#viewport.clearSelection())
444
464
  repaint = true;
465
+ if (overTailControl) {
466
+ this.#tailControl = undefined;
467
+ tailControl.activate();
468
+ this.#tailPointerSuppressed = true;
469
+ repaint = true;
470
+ return true;
471
+ }
445
472
  if (overBottom) {
446
473
  this.#viewport.scrollToEnd(now);
447
474
  this.#tailPointerSuppressed = true;