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

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 (95) 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/cli/dispatch.js +1 -1
  6. package/dist/composition/owned-ui.js +2 -1
  7. package/dist/features/launch/profile-paths.d.ts +2 -0
  8. package/dist/features/launch/profile-paths.js +11 -0
  9. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  10. package/dist/features/owned-ui/settings-app.js +1 -1
  11. package/dist/features/prompt-history/service.d.ts +31 -1
  12. package/dist/features/prompt-history/service.js +294 -129
  13. package/dist/features/prompt-history/store.d.ts +2 -1
  14. package/dist/features/prompt-history/store.js +17 -4
  15. package/dist/features/prompt-history/worker.js +6 -3
  16. package/dist/foundation/launch-context/index.d.ts +59 -0
  17. package/dist/foundation/launch-context/index.js +136 -0
  18. package/dist/foundation/launch-guardian/main.js +6 -4
  19. package/dist/foundation/release/bootstrap.d.ts +6 -1
  20. package/dist/foundation/release/bootstrap.js +59 -13
  21. package/dist/foundation/release/cohort-state.d.ts +1 -0
  22. package/dist/foundation/release/cohort-state.js +1 -0
  23. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  24. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  25. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  26. package/dist/foundation/release/dependency-certification.js +208 -0
  27. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  28. package/dist/foundation/release/dependency-layer.js +6 -37
  29. package/dist/foundation/release/release-gc.js +74 -46
  30. package/dist/foundation/release/release-store.d.ts +2 -0
  31. package/dist/foundation/release/release-store.js +11 -3
  32. package/dist/foundation/release/release.d.ts +3 -1
  33. package/dist/foundation/release/release.js +7 -3
  34. package/dist/foundation/release/restart-certification.d.ts +1 -0
  35. package/dist/foundation/release/restart-certification.js +17 -1
  36. package/dist/foundation/release/update-recovery.d.ts +2 -0
  37. package/dist/foundation/release/update-recovery.js +4 -0
  38. package/dist/foundation/release/update.js +1 -1
  39. package/dist/foundation/release/warmup.d.ts +1 -1
  40. package/dist/foundation/release/warmup.js +18 -6
  41. package/dist/foundation/startup/startup-runtime.js +13 -11
  42. package/dist/foundation/supervision/main.js +8 -7
  43. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  44. package/dist/integrations/pi/components/owned-editor-ux.js +105 -30
  45. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
  46. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  47. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  48. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  49. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  51. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  52. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  53. package/dist/integrations/pi/engine/adapter.d.ts +33 -1
  54. package/dist/integrations/pi/engine/adapter.js +309 -112
  55. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  56. package/dist/integrations/pi/engine/changelog.js +63 -0
  57. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  58. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  59. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  60. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  61. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  62. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
  63. package/dist/integrations/pi/session-ui/prompt-chips.js +74 -37
  64. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  65. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  66. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +26 -1
  67. package/dist/integrations/pi/session-ui/session-shell-root.js +70 -75
  68. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  69. package/dist/integrations/pi/session-ui/session-shell.js +67 -13
  70. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -2
  71. package/dist/integrations/pi/session-ui/session-viewport-controller.js +25 -20
  72. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  73. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  74. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +9 -2
  75. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +87 -43
  76. package/dist/native/darwin-arm64/manifest.json +1 -1
  77. package/dist/native/linux-x64/manifest.json +1 -1
  78. package/dist/native/win32-x64/manifest.json +2 -2
  79. package/dist/native/win32-x64/process-guardian.exe +0 -0
  80. package/dist/product-identity.d.ts +1 -1
  81. package/dist/product-identity.js +2 -35
  82. package/dist/product-identity.json +1 -23
  83. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  84. package/dist/ui/components/transcript-viewport.js +23 -2
  85. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  86. package/dist/ui/components/visible-hyperlinks.js +1 -0
  87. package/dist/ui/settings/declarations.js +1 -0
  88. package/dist/ui/settings/sections.js +11 -1
  89. package/docs/architecture/boundaries.md +2 -1
  90. package/docs/architecture/internal-naming.md +91 -0
  91. package/docs/architecture/resource-and-data-policy.md +9 -4
  92. package/docs/architecture/toolchain.md +15 -1
  93. package/docs/features/prompt-history.md +39 -11
  94. package/docs/manual-code-streaming-cleanup.md +88 -0
  95. package/package.json +8 -3
@@ -53,10 +53,12 @@ export class OwnedUiSessionShell {
53
53
  #lastEscapeTime = 0;
54
54
  #activeLoginDialog;
55
55
  #sessionGeneration;
56
+ #sessionBindingGeneration;
56
57
  #suggestionModelKey;
57
58
  constructor(options) {
58
59
  this.backend = options.backend;
59
60
  this.#sessionGeneration = this.backend.sessionGeneration;
61
+ this.#sessionBindingGeneration = this.backend.sessionBindingGeneration;
60
62
  this.#suggestionModelKey = modelKey(this.backend.view());
61
63
  this.#cwd = options.cwd;
62
64
  this.#routeHost = options.routeHost ?? null;
@@ -73,7 +75,7 @@ export class OwnedUiSessionShell {
73
75
  getColumns: () => runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
74
76
  getRows: () => runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
75
77
  requestRender: force => runtime?.requestRender(force),
76
- requestHyperlinkCleanup: () => damageTerminal?.requestHyperlinkCleanup(),
78
+ requestHyperlinkCleanup: rows => damageTerminal?.requestHyperlinkCleanup(rows),
77
79
  onViewportFrame: frame => damageTerminal?.arm(frame.descriptor, {
78
80
  overlayActive: runtime?.hasOverlay() ?? false,
79
81
  selectionActive: this.root.hasActiveSelection(),
@@ -139,6 +141,7 @@ export class OwnedUiSessionShell {
139
141
  }, this.backend.agentDir, {
140
142
  getMessageRenderer: customType => this.backend.pinnedMessageRenderer(customType),
141
143
  getToolDefinition: toolName => this.backend.pinnedToolDefinition(toolName),
144
+ getShortcuts: bindings => this.backend.pinnedShortcutDescriptions(bindings),
142
145
  }, options.sessionLayout, {
143
146
  resolve: assetId => this.backend.resolveTranscriptImage(assetId),
144
147
  });
@@ -237,6 +240,17 @@ export class OwnedUiSessionShell {
237
240
  ? this.runtime.addPreInputListener(data => {
238
241
  if (options.inputPresentation?.coordination === false)
239
242
  this.#streamPresentation.noteImmediatePresentation();
243
+ // Compatibility: Pi's fullscreen renderer also intercepts plain Home/End.
244
+ // Deliver them to the focused owned input before that outer scroll handler;
245
+ // overlays retain Pi's normal dispatch, and comparison profiles never enter here.
246
+ if (!this.runtime.hasOverlay() && (this.root.editor.matchesTerminalKey(data, "home")
247
+ || this.root.editor.matchesTerminalKey(data, "end"))) {
248
+ if (this.root.usesDefaultInputSurface())
249
+ this.root.handleViewportPreInput(data, true);
250
+ this.root.handleInput(data);
251
+ this.runtime.requestRender();
252
+ return { consume: true };
253
+ }
240
254
  // Invariant: an overlay or editor-replacement screen owns its entire pointer
241
255
  // surface. Letting the transcript pre-router inspect those reports
242
256
  // steals settings value menus and numeric +/- controls before the
@@ -322,7 +336,6 @@ export class OwnedUiSessionShell {
322
336
  fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
323
337
  active: () => this.root.usesDefaultInputSurface(),
324
338
  render: () => this.runtime.requestRender(),
325
- failure: message => this.root.addExtensionNotification(message, "warning"),
326
339
  });
327
340
  }
328
341
  this.#extensionBridge = createPiExtensionUiBridge({
@@ -793,8 +806,7 @@ export class OwnedUiSessionShell {
793
806
  showLoginMethodSelector(providerReference) {
794
807
  const method = this.backend.pinnedLoginMethodOptions(providerReference);
795
808
  if (method.options.length === 0) {
796
- this.root.appendWorkflowResult({ command: "login", outcome: "failed", message: `No authentication methods available for ${providerReference}` });
797
- this.runtime.requestRender();
809
+ this.showLoginProviderSelector(undefined, providerReference);
798
810
  return;
799
811
  }
800
812
  if (method.options.length === 1) {
@@ -836,6 +848,20 @@ export class OwnedUiSessionShell {
836
848
  initialFilterMode: context.filterMode,
837
849
  ...(initialSelectedId === undefined ? {} : { initialSelectedId }),
838
850
  onLabelChange: context.appendLabelChange,
851
+ onCopy: text => {
852
+ if (!text) {
853
+ this.root.appendWorkflowResult({ command: "tree", outcome: "failed", message: "Selected entry has no text to copy" });
854
+ this.runtime.requestRender();
855
+ return;
856
+ }
857
+ void this.backend.copyWorkflowText(text).then(() => {
858
+ this.root.appendWorkflowStatus("Copied selected message to clipboard");
859
+ this.runtime.requestRender();
860
+ }).catch(error => {
861
+ this.root.appendWorkflowResult({ command: "tree", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
862
+ this.runtime.requestRender();
863
+ });
864
+ },
839
865
  onCancel: close,
840
866
  onSelect: entryId => {
841
867
  close();
@@ -863,10 +889,10 @@ export class OwnedUiSessionShell {
863
889
  this.root.setInputSurface(component);
864
890
  this.runtime.requestRender();
865
891
  }
866
- showLoginProviderSelector(authType) {
892
+ showLoginProviderSelector(authType, initialSearchInput) {
867
893
  const options = this.backend.pinnedLoginOptions(authType);
868
894
  if (options.length === 0) {
869
- this.root.appendWorkflowStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
895
+ this.root.appendWorkflowStatus(authType === "oauth" ? "No subscription providers available." : authType === "api_key" ? "No API key providers available." : "No login providers available.");
870
896
  this.runtime.requestRender();
871
897
  return;
872
898
  }
@@ -879,8 +905,9 @@ export class OwnedUiSessionShell {
879
905
  void this.runWorkflow({ command: "login", argument: "", selection: id });
880
906
  }, () => {
881
907
  close();
882
- this.showLoginAuthTypeSelector();
883
- });
908
+ if (authType !== undefined)
909
+ this.showLoginAuthTypeSelector();
910
+ }, initialSearchInput);
884
911
  this.root.setInputSurface(component);
885
912
  this.runtime.requestRender();
886
913
  }
@@ -961,6 +988,24 @@ export class OwnedUiSessionShell {
961
988
  return this.runWorkflow({ command: "quit", argument: "" });
962
989
  }
963
990
  async runWorkflow(request) {
991
+ if (request.command === "login" && request.selection !== undefined) {
992
+ const setup = this.backend.pinnedAmbientAuthentication(request.selection);
993
+ if (setup) {
994
+ const close = () => {
995
+ this.root.setInputSurface(null);
996
+ this.runtime.requestRender();
997
+ };
998
+ const dialog = createPiShellLoginDialog({
999
+ getColumns: () => this.runtime.viewport().columns,
1000
+ getRows: () => this.runtime.viewport().rows,
1001
+ requestRender: () => this.runtime.requestRender(),
1002
+ }, setup.providerId, close, setup.providerName, setup.title);
1003
+ dialog.showInfo(setup.message, [], true);
1004
+ this.root.setInputSurface(dialog);
1005
+ this.runtime.requestRender();
1006
+ return { outcome: "completed", diagnostic: null };
1007
+ }
1008
+ }
964
1009
  if (request.command === "scoped-models" && request.selection === undefined && request.confirmed === undefined) {
965
1010
  this.showScopedModelsSelector();
966
1011
  return { outcome: "completed", diagnostic: null };
@@ -1001,8 +1046,13 @@ export class OwnedUiSessionShell {
1001
1046
  return { outcome: "completed", diagnostic: null };
1002
1047
  }
1003
1048
  if (request.command === "reload") {
1049
+ const blocked = this.backend.reloadBlockedResult();
1050
+ if (blocked) {
1051
+ this.root.appendWorkflowResult(blocked);
1052
+ this.runtime.requestRender();
1053
+ return workflowAdapterResult(blocked);
1054
+ }
1004
1055
  this.root.resetExtensionUi();
1005
- this.root.resetWorkflowPresentation();
1006
1056
  }
1007
1057
  const shareSurface = request.command === "share"
1008
1058
  ? createPiShellOperationLoader({
@@ -1053,6 +1103,8 @@ export class OwnedUiSessionShell {
1053
1103
  return { outcome: "failed", diagnostic: `Owned controller missing for ${request.command}` };
1054
1104
  }
1055
1105
  if (request.command === "reload" && result.outcome === "completed") {
1106
+ this.root.resetWorkflowPresentation();
1107
+ this.root.editor.reloadKeybindings();
1056
1108
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
1057
1109
  }
1058
1110
  this.root.appendWorkflowResult(result);
@@ -1211,9 +1263,7 @@ export class OwnedUiSessionShell {
1211
1263
  attempt(() => this.#extensionBridge.dispose());
1212
1264
  // Invariant: terminal restoration precedes any potentially stalled backend teardown.
1213
1265
  await this.runtime.dispose().catch(error => failures.push(error));
1214
- const historySaved = await historyCleanup.catch(() => false);
1215
- if (!historySaved)
1216
- this.runtime.writeAfterStop("Prompt history could not finish saving before exit.\n");
1266
+ await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
1217
1267
  await boundedCleanup(() => pasteCleanup).catch(error => failures.push(error));
1218
1268
  await boundedCleanup(() => this.backend.unbindExtensionUi()).catch(error => failures.push(error));
1219
1269
  if (failures.length > 0)
@@ -1251,7 +1301,11 @@ export class OwnedUiSessionShell {
1251
1301
  this.root.resetPendingPastes();
1252
1302
  this.#promptSuggestions?.invalidate();
1253
1303
  this.#sessionGeneration = this.backend.sessionGeneration;
1254
- this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
1304
+ // Invariant: delivery recovery invalidates callbacks, not same-session local recall or its draft.
1305
+ if (this.#sessionBindingGeneration !== this.backend.sessionBindingGeneration) {
1306
+ this.#sessionBindingGeneration = this.backend.sessionBindingGeneration;
1307
+ this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
1308
+ }
1255
1309
  this.#activeLoginDialog = undefined;
1256
1310
  this.#extensionBridge.reset();
1257
1311
  // Invariant: a replaced session takes its transient viewport and owned-route state with it.
@@ -5,8 +5,8 @@ export interface SessionViewportControllerOptions {
5
5
  readonly enabled: boolean;
6
6
  readonly editor: PiShellEditorPort;
7
7
  readonly requestRender: (force?: boolean) => void;
8
- readonly requestHyperlinkCleanup?: () => void;
9
- /** Whether mutable editor URL chips need an immediate forced repaint on deletion. */
8
+ readonly requestHyperlinkCleanup?: (rows?: readonly number[]) => void;
9
+ /** Whether mutable editor URL chips need targeted link-cell cleanup on deletion. */
10
10
  readonly hasEditorLinks?: () => boolean;
11
11
  }
12
12
  export interface SessionViewportInputResult {
@@ -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
  }
@@ -85,8 +89,8 @@ export class SessionViewportController {
85
89
  if (this.#hoveredHyperlinkKey !== undefined && next !== this.#hoveredHyperlinkKey) {
86
90
  // Platform: Windows Terminal can leave its solid native-hover underline behind
87
91
  // when a following viewport moves the link away from a stationary pointer.
88
- this.#requestHyperlinkCleanup();
89
- this.#requestRender(true);
92
+ this.#requestHyperlinkCleanup([this.#pointerPosition.row]);
93
+ this.#requestRender();
90
94
  }
91
95
  this.#hoveredHyperlinkKey = next;
92
96
  }
@@ -157,7 +161,24 @@ export class SessionViewportController {
157
161
  // Platform: URL chip deletion must overwrite terminal link cells in the same frame.
158
162
  if (EDITOR_LINK_DELETION_INPUTS.has(data) && this.#hasEditorLinks()) {
159
163
  this.#requestHyperlinkCleanup();
160
- this.#requestRender(true);
164
+ this.#requestRender();
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 };
161
182
  }
162
183
  if (this.#viewport.frame === null) {
163
184
  // Invariant: a not-yet-painted A1 screen is not Pi's selection surface.
@@ -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)
@@ -236,8 +242,7 @@ export class SessionViewportController {
236
242
  const nextHyperlink = this.#hyperlinkKeyAt(frame, event.column, event.row);
237
243
  if (this.#hoveredHyperlinkKey !== undefined && nextHyperlink !== this.#hoveredHyperlinkKey) {
238
244
  // Platform: overwrite Windows Terminal's cached native-hover underline exactly once.
239
- this.#requestHyperlinkCleanup();
240
- forceRepaint = true;
245
+ this.#requestHyperlinkCleanup(previousPointer === undefined ? undefined : [previousPointer.row]);
241
246
  repaint = true;
242
247
  }
243
248
  this.#hoveredHyperlinkKey = nextHyperlink;
@@ -0,0 +1,5 @@
1
+ /** Normalize clipboard text and classify its compact label using the pinned Pi 0.84.2 paste policy. */
2
+ export declare function prepareTextPaste(source: string): {
3
+ readonly text: string;
4
+ readonly label?: string;
5
+ };
@@ -0,0 +1,16 @@
1
+ /** Normalize clipboard text and classify its compact label using the pinned Pi 0.84.2 paste policy. */
2
+ export function prepareTextPaste(source) {
3
+ // Compatibility: extended-key terminals can encode literal controls inside bracketed paste.
4
+ const decoded = source.replace(/\x1b\[(\d+);5u/g, (sequence, value) => {
5
+ const code = Number(value);
6
+ if (code >= 65 && code <= 90)
7
+ return String.fromCharCode(code - 64);
8
+ if (code >= 97 && code <= 122)
9
+ return String.fromCharCode(code - 96);
10
+ return sequence;
11
+ });
12
+ const text = decoded.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x09\x0b-\x1f]/g, "");
13
+ const lines = text.split("\n").length;
14
+ const label = lines > 10 ? `+${lines} lines` : text.length > 1000 ? `${text.length} chars` : undefined;
15
+ return { text, ...(label === undefined ? {} : { label }) };
16
+ }
@@ -12,6 +12,12 @@ export interface PiTuiDamageFrameDescriptor {
12
12
  readonly rowStart: number;
13
13
  readonly rowEnd: number;
14
14
  } | null;
15
+ /**
16
+ * Visible rows owned by the block that is currently streaming. A live block repaints
17
+ * every row it owns when it reflows, so the allowed damage follows the declared tail
18
+ * instead of assuming a one-row tail.
19
+ */
20
+ readonly liveTailRows?: number;
15
21
  readonly verticalShiftRows: number;
16
22
  readonly safeVerticalShift: boolean;
17
23
  readonly cause: string;
@@ -34,6 +40,7 @@ export interface DamageAwareTerminalOptions {
34
40
  /** The shell supplies visible-row analysis; the terminal layer owns no UI parser. */
35
41
  readonly inspectHyperlinks: (content: string) => {
36
42
  readonly ranges: readonly unknown[];
43
+ readonly hasExplicitLink: boolean;
37
44
  readonly signature: string;
38
45
  readonly replaySafe: boolean;
39
46
  readonly width: number;
@@ -56,8 +63,8 @@ export declare class DamageAwareTerminalAdapter implements PiTuiTerminalPort {
56
63
  get kittyProtocolActive(): boolean;
57
64
  get lastDecision(): PiTuiDamageDecision;
58
65
  get hyperlinkCleanupPending(): boolean;
59
- /** Latches cleanup through composition/coalescing until a complete write covers it. */
60
- requestHyperlinkCleanup(): void;
66
+ /** Latches the former link rows, including rows whose replacement contains no link. */
67
+ requestHyperlinkCleanup(rows?: readonly number[]): void;
61
68
  arm(descriptor: PiTuiDamageFrameDescriptor, safety: PiTuiDamageFrameSafety): void;
62
69
  start(onInput: (data: string) => void, onResize: () => void): void;
63
70
  stop(): void;
@@ -15,6 +15,8 @@ export class DamageAwareTerminalAdapter {
15
15
  #rows = new Map();
16
16
  #links = new Map();
17
17
  #cleanupRevision = 0;
18
+ #cleanupRows = new Set();
19
+ #cleanupNeedsCompleteFrame = false;
18
20
  #cleanedRevision = 0;
19
21
  #recoveryRevision = 0;
20
22
  #epoch = 0;
@@ -38,8 +40,23 @@ export class DamageAwareTerminalAdapter {
38
40
  get kittyProtocolActive() { return this.inner.kittyProtocolActive; }
39
41
  get lastDecision() { return this.#decision; }
40
42
  get hyperlinkCleanupPending() { return this.#cleanupRevision > this.#cleanedRevision; }
41
- /** Latches cleanup through composition/coalescing until a complete write covers it. */
42
- requestHyperlinkCleanup() { this.#cleanupRevision += 1; }
43
+ /** Latches the former link rows, including rows whose replacement contains no link. */
44
+ requestHyperlinkCleanup(rows) {
45
+ this.#cleanupRevision += 1;
46
+ if (rows !== undefined) {
47
+ for (const row of rows) {
48
+ if (Number.isSafeInteger(row) && row >= 1 && row <= this.rows)
49
+ this.#cleanupRows.add(row);
50
+ }
51
+ }
52
+ else {
53
+ // Invariant: no known pointer is not proof of no host-hover decoration. Unscoped
54
+ // removal/invalidation repairs every previously linked row, not every screen row.
55
+ for (const [row, state] of this.#links)
56
+ if (state.ranges.length > 0)
57
+ this.#cleanupRows.add(row);
58
+ }
59
+ }
43
60
  arm(descriptor, safety) {
44
61
  this.#armed = { descriptor, safety };
45
62
  }
@@ -56,6 +73,8 @@ export class DamageAwareTerminalAdapter {
56
73
  this.#epoch += 1;
57
74
  this.#armed = undefined;
58
75
  this.#cleanupRevision = this.#cleanedRevision = this.#recoveryRevision = 0;
76
+ this.#cleanupRows.clear();
77
+ this.#cleanupNeedsCompleteFrame = false;
59
78
  this.#lastConsumedFrameId = 0;
60
79
  this.#invalidateRows();
61
80
  this.inner.stop();
@@ -77,25 +96,19 @@ export class DamageAwareTerminalAdapter {
77
96
  this.#invalidatePresentation();
78
97
  if (unknownPaint && !this.hyperlinkCleanupPending && this.options.inspectHyperlinks(data).ranges.length > 0) {
79
98
  this.requestHyperlinkCleanup();
99
+ this.#cleanupNeedsCompleteFrame = true;
80
100
  }
81
- if (parsed !== null && !this.hyperlinkCleanupPending) {
82
- for (const row of parsed.rows) {
83
- const previous = this.#links.get(row.row);
84
- if (previous === undefined || this.#rows.get(row.row) === row.content
85
- || (previous.replaySafe && previous.ranges.length === 0))
86
- continue;
87
- const next = this.options.inspectHyperlinks(row.content);
88
- if (previous.ranges.length > 0 && previous.signature !== next.signature) {
89
- this.requestHyperlinkCleanup();
90
- break;
91
- }
92
- }
93
- }
101
+ // Invariant: a deliberate same-geometry reset discards presented link state. Ordinary streamed
102
+ // row changes are not cleanup requests: candidate-looking text is not terminal state.
103
+ if (sameGeometry && parsed?.structuralPrefix === "\u001b[2J"
104
+ && hasCompleteScreenRows(parsed.rows, this.rows) && !this.hyperlinkCleanupPending
105
+ && !this.#hasSameHyperlinkState(parsed.rows))
106
+ this.requestHyperlinkCleanup();
94
107
  const decision = this.#decide(armed, parsed);
108
+ const coveredRevision = this.#cleanupRevision;
95
109
  const cleanup = this.hyperlinkCleanupPending && parsed !== null
96
110
  && (armed === undefined || (armed.descriptor.width === this.columns && armed.descriptor.height === this.rows))
97
- ? this.#completeCleanupFrame(parsed) : null;
98
- const coveredRevision = this.#cleanupRevision;
111
+ ? this.#boundedCleanupFrame(parsed) : null;
99
112
  const epoch = this.#epoch;
100
113
  let output = data;
101
114
  if (cleanup !== null) {
@@ -107,8 +120,9 @@ export class DamageAwareTerminalAdapter {
107
120
  }
108
121
  else {
109
122
  this.#decision = decision;
110
- if (decision.reason === "suppressed-redundant-clear" && parsed !== null)
111
- output = buildClearlessWrite(parsed);
123
+ if (decision.reason === "suppressed-redundant-clear" && parsed !== null) {
124
+ output = buildClearlessWrite(parsed, this.#rows);
125
+ }
112
126
  else if (decision.transformed && armed !== undefined && parsed !== null) {
113
127
  output = buildDamageWrite(parsed, armed.descriptor, new Set(decision.paintedRows), this.#rows);
114
128
  }
@@ -117,11 +131,16 @@ export class DamageAwareTerminalAdapter {
117
131
  if (epoch !== this.#epoch
118
132
  || (armed !== undefined && this.#lastConsumedFrameId !== armed.descriptor.frameId))
119
133
  return;
120
- // Invariant: cache invalidation follows forwarded bytes, not a clear which
121
- // was stripped from the input. A synthesized cleanup publishes every row.
134
+ // Invariant: cache invalidation follows forwarded bytes, not a suppressed clear.
135
+ // A bounded cleanup preserves prior rows and publishes all changed or invalidated rows.
122
136
  this.#rememberFrame(armed?.descriptor, cleanup ?? parsed, output);
123
- if (cleanup !== null)
137
+ if (cleanup !== null) {
124
138
  this.#cleanedRevision = Math.max(this.#cleanedRevision, coveredRevision);
139
+ if (this.#cleanupRevision === coveredRevision) {
140
+ this.#cleanupRows.clear();
141
+ this.#cleanupNeedsCompleteFrame = false;
142
+ }
143
+ }
125
144
  this.#scheduleCleanupRecovery();
126
145
  }
127
146
  moveBy(lines) { this.inner.moveBy(lines); }
@@ -175,7 +194,7 @@ export class DamageAwareTerminalAdapter {
175
194
  return { ...base, reason: "grammar-mismatch" };
176
195
  if (parsed.structuralPrefix.length > 0
177
196
  || hasUnsafeTerminalContent(parsed, this.options.inspectHyperlinks, descriptor.transcript)
178
- || this.#hasLinkRisk(descriptor.transcript) || this.#hasUnsafeRows(descriptor.transcript)) {
197
+ || this.#hasExplicitLinkRisk(descriptor.transcript) || this.#hasUnsafeRows(descriptor.transcript)) {
179
198
  return { ...base, reason: "unsafe-terminal-content" };
180
199
  }
181
200
  const { rowStart, rowEnd } = descriptor.transcript;
@@ -204,10 +223,11 @@ export class DamageAwareTerminalAdapter {
204
223
  if (descriptor.dock !== null && row.row >= descriptor.dock.rowStart && row.row <= descriptor.dock.rowEnd)
205
224
  painted.add(row.row);
206
225
  }
207
- // Rationale: ordinary followed prose may change one active tail source row, expose
208
- // the shifted suffix, and restyle one sticky boundary row. Larger changes indicate
209
- // Markdown/theme/overlay reflow and fail closed.
210
- if (painted.size > descriptor.verticalShiftRows + 2 + countDockRows(parsed.rows, descriptor.dock)) {
226
+ // Rationale: a followed frame may repaint the live tail it declares, expose the shifted
227
+ // suffix, and restyle one sticky boundary row. A frame that declares no tail keeps the
228
+ // historical one-row allowance. Anything beyond that reaches settled rows and fails closed.
229
+ const liveTailRows = Math.max(1, descriptor.liveTailRows ?? 1);
230
+ if (painted.size > descriptor.verticalShiftRows + liveTailRows + 1 + countDockRows(parsed.rows, descriptor.dock)) {
211
231
  return { ...base, reason: "excessive-real-damage" };
212
232
  }
213
233
  return {
@@ -248,11 +268,9 @@ export class DamageAwareTerminalAdapter {
248
268
  return next?.replaySafe === true && (previous?.signature ?? "") === next.signature;
249
269
  });
250
270
  }
251
- #hasLinkRisk(region) {
271
+ #hasExplicitLinkRisk(region) {
252
272
  for (const [row, state] of this.#links) {
253
- if (region !== undefined && (row < region.rowStart || row > region.rowEnd))
254
- continue;
255
- if (state.ranges.length > 0)
273
+ if (row >= region.rowStart && row <= region.rowEnd && state.hasExplicitLink)
256
274
  return true;
257
275
  }
258
276
  return false;
@@ -264,14 +282,33 @@ export class DamageAwareTerminalAdapter {
264
282
  }
265
283
  return false;
266
284
  }
267
- #completeCleanupFrame(parsed) {
268
- const desired = parsed.structuralPrefix.length === 0 ? new Map(this.#rows) : new Map();
269
- for (const row of parsed.rows)
270
- desired.set(row.row, row.content);
271
- if (desired.size !== this.rows)
285
+ #boundedCleanupFrame(parsed) {
286
+ const sameGeometry = this.#cacheWidth === this.columns && this.#cacheHeight === this.rows;
287
+ const complete = hasCompleteScreenRows(parsed.rows, this.rows);
288
+ if (this.#cleanupNeedsCompleteFrame && !complete)
272
289
  return null;
290
+ const suppressReset = sameGeometry && !this.#cleanupNeedsCompleteFrame
291
+ && parsed.structuralPrefix === "\u001b[2J" && complete;
292
+ const structuralPrefix = suppressReset ? "" : parsed.structuralPrefix;
293
+ const desired = structuralPrefix.length === 0 ? new Map(this.#rows) : new Map();
294
+ const dirty = new Set([...this.#cleanupRows].filter(row => row <= this.rows));
295
+ for (const row of parsed.rows) {
296
+ if (row.row > this.rows)
297
+ return null;
298
+ desired.set(row.row, row.content);
299
+ if (structuralPrefix.length > 0 || this.#cleanupNeedsCompleteFrame
300
+ || this.#rows.get(row.row) !== row.content)
301
+ dirty.add(row.row);
302
+ }
303
+ // Invariant: validate all incoming rows before dropping an apparent redundant reset. Unknown
304
+ // content must pass through unchanged, never become a partially transformed frame.
305
+ for (const row of parsed.rows) {
306
+ const state = row.content === this.#rows.get(row.row) ? this.#links.get(row.row) : this.options.inspectHyperlinks(row.content);
307
+ if (state?.replaySafe !== true || state.width > this.columns)
308
+ return null;
309
+ }
273
310
  const rows = [];
274
- for (let row = 1; row <= this.rows; row += 1) {
311
+ for (const row of [...dirty].sort((left, right) => left - right)) {
275
312
  const content = desired.get(row);
276
313
  if (content === undefined)
277
314
  return null;
@@ -280,7 +317,8 @@ export class DamageAwareTerminalAdapter {
280
317
  return null;
281
318
  rows.push({ row, content, segment: `\u001b[${row};1H\u001b[2K${content}` });
282
319
  }
283
- return { structuralPrefix: "\u001b[2J", rows, cursorSuffix: parsed.cursorSuffix };
320
+ // Invariant: only preserve a structural clear supplied by Pi; cleanup never manufactures one.
321
+ return { structuralPrefix, rows, cursorSuffix: parsed.cursorSuffix };
284
322
  }
285
323
  #scheduleCleanupRecovery() {
286
324
  if (!this.hyperlinkCleanupPending || this.#recoveryRevision === this.#cleanupRevision
@@ -296,8 +334,13 @@ export class DamageAwareTerminalAdapter {
296
334
  });
297
335
  }
298
336
  #invalidatePresentation() {
299
- if (this.#hasLinkRisk() && !this.hyperlinkCleanupPending)
337
+ if ([...this.#links.values()].some(state => state.ranges.length > 0) && !this.hyperlinkCleanupPending) {
300
338
  this.requestHyperlinkCleanup();
339
+ }
340
+ // Invariant: after unknown paint or geometry loss, exact former row locations are no longer
341
+ // authoritative. Wait for a complete safe frame instead of guessing a partial repair.
342
+ if (this.hyperlinkCleanupPending)
343
+ this.#cleanupNeedsCompleteFrame = true;
301
344
  this.#invalidateRows();
302
345
  }
303
346
  #invalidateRows() {
@@ -345,7 +388,7 @@ function hasUnsafeTerminalContent(parsed, inspect, linkRegion) {
345
388
  if (linkRegion !== undefined && (row.row < linkRegion.rowStart || row.row > linkRegion.rowEnd))
346
389
  return false;
347
390
  const state = inspect(row.content);
348
- return !state.replaySafe || state.ranges.length > 0;
391
+ return !state.replaySafe || state.hasExplicitLink;
349
392
  });
350
393
  }
351
394
  function buildDamageWrite(parsed, descriptor, paintedRows, previousRows) {
@@ -363,12 +406,13 @@ function buildDamageWrite(parsed, descriptor, paintedRows, previousRows) {
363
406
  }).join("");
364
407
  return `${BEGIN_SYNCHRONIZED_OUTPUT}${regionShift}${rowPaint}${parsed.cursorSuffix}`;
365
408
  }
366
- /** Keeps the clear and all current rows in the same synchronized transaction. */
409
+ /** Keeps required cleanup rows and current content in one synchronized transaction. */
367
410
  function buildCompleteWrite(parsed) {
368
411
  return `${BEGIN_SYNCHRONIZED_OUTPUT}${parsed.structuralPrefix}${parsed.rows.map(row => row.segment).join("")}${parsed.cursorSuffix}`;
369
412
  }
370
- function buildClearlessWrite(parsed) {
371
- return `${BEGIN_SYNCHRONIZED_OUTPUT}${parsed.rows.map(row => row.segment).join("")}${parsed.cursorSuffix}`;
413
+ function buildClearlessWrite(parsed, previousRows) {
414
+ const changed = parsed.rows.filter(row => previousRows.get(row.row) !== row.content);
415
+ return `${BEGIN_SYNCHRONIZED_OUTPUT}${changed.map(row => row.segment).join("")}${parsed.cursorSuffix}`;
372
416
  }
373
417
  function hasCompleteScreenRows(rows, height) {
374
418
  return rows.length === height && rows.every((row, index) => row.row === index + 1);
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-11T15:30:41.585Z",
8
+ "builtAt": "2026-09-12T13:59:03.140Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-11T15:30:45.869Z",
8
+ "builtAt": "2026-09-12T13:58:54.058Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-11T15:31:16.823Z",
8
+ "builtAt": "2026-09-12T13:59:34.009Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "78359342ae74e12b31ce05aff4a6b06f25d6e9777b081822c860604d674ec08d",
11
+ "sha256": "15e00144629345d7f00b110494775b80c65e6e575e6758d88b533c25b57d35d5",
12
12
  "size": 177664
13
13
  },
14
14
  "provenance": {
@@ -1,4 +1,4 @@
1
- declare const ENVIRONMENT_KEYS: readonly ["certificationTarball", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "fixture", "fixtureInput", "fixtureToken", "inputAcknowledgement", "immutableWarmup", "internalPackaging", "launchProfile", "nativePi", "paneId", "piParityIntentionalMutation", "piPortRoot", "piSourceLedgerPath", "piSourceScanRoot", "probeTrace", "processGuardianPath", "profileHome", "protocolVersion", "releaseDigest", "releaseId", "releaseLayers", "releaseRoot", "releaseRunnerLabel", "runtimeDir", "structuredFlowLimits", "startupTrace", "terminalArgumentsJson", "terminalExecutable", "terminalSessionId"];
1
+ declare const ENVIRONMENT_KEYS: readonly ["configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "paneId", "probeTrace", "processGuardianPath", "profileHome", "runtimeDir", "startupTrace", "terminalSessionId"];
2
2
  declare const FILESYSTEM_KEYS: readonly ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
3
3
  declare const STATE_KEYS: readonly ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile"];
4
4
  declare const ENDPOINT_KEYS: readonly ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];