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

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 (110) hide show
  1. package/README.md +40 -11
  2. package/dist/composition/owned-ui.d.ts +2 -0
  3. package/dist/composition/owned-ui.js +20 -3
  4. package/dist/contracts/owned-ui/index.d.ts +2 -0
  5. package/dist/contracts/owned-ui/index.js +2 -0
  6. package/dist/contracts/owned-ui/model.d.ts +38 -3
  7. package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
  8. package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
  9. package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
  10. package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
  11. package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
  12. package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
  13. package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
  14. package/dist/contracts/owned-ui/validation.js +45 -2
  15. package/dist/contracts/presentation/index.d.ts +8 -0
  16. package/dist/features/owned-ui/settings-app.js +1 -1
  17. package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
  18. package/dist/features/prompt-history/image-sidecar.js +130 -0
  19. package/dist/features/prompt-history/index.d.ts +2 -0
  20. package/dist/features/prompt-history/index.js +1 -0
  21. package/dist/features/prompt-history/paths.d.ts +1 -0
  22. package/dist/features/prompt-history/paths.js +2 -1
  23. package/dist/features/prompt-history/store.d.ts +1 -1
  24. package/dist/features/prompt-history/store.js +46 -2
  25. package/dist/features/prompt-history/worker.js +1 -1
  26. package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
  27. package/dist/features/prompt-suggestions/diagnostics.js +82 -0
  28. package/dist/features/prompt-suggestions/index.d.ts +1 -0
  29. package/dist/features/prompt-suggestions/index.js +1 -0
  30. package/dist/foundation/release/bootstrap.d.ts +2 -0
  31. package/dist/foundation/release/bootstrap.js +54 -18
  32. package/dist/foundation/release/index.d.ts +1 -0
  33. package/dist/foundation/release/index.js +1 -0
  34. package/dist/foundation/release/update-launch.d.ts +6 -0
  35. package/dist/foundation/release/update-launch.js +17 -0
  36. package/dist/foundation/release/update.d.ts +2 -0
  37. package/dist/foundation/release/update.js +20 -21
  38. package/dist/foundation/supervision/main.js +5 -2
  39. package/dist/foundation/supervision/server.js +40 -13
  40. package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
  41. package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
  42. package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
  43. package/dist/integrations/pi/components/prompt-input-port.js +1 -0
  44. package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
  45. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  46. package/dist/integrations/pi/components/shell-footer-status.js +9 -5
  47. package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
  48. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
  49. package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
  51. package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
  52. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
  53. package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
  54. package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
  55. package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
  56. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
  57. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
  60. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  61. package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
  62. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  63. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  64. package/dist/integrations/pi/engine/adapter.js +181 -84
  65. package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
  66. package/dist/integrations/pi/engine/tool-rendering.js +122 -0
  67. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
  68. package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
  69. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
  70. package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
  71. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
  72. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
  73. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
  74. package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
  75. package/dist/integrations/pi/session-ui/session-shell.js +51 -31
  76. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
  77. package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
  78. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  79. package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
  80. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
  81. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
  82. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  83. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
  84. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
  85. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
  86. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/linux-x64/manifest.json +1 -1
  89. package/dist/native/win32-x64/manifest.json +2 -2
  90. package/dist/native/win32-x64/process-guardian.exe +0 -0
  91. package/dist/product-identity.d.ts +1 -1
  92. package/dist/product-identity.js +1 -1
  93. package/dist/product-identity.json +1 -0
  94. package/dist/ui/components/index.d.ts +1 -0
  95. package/dist/ui/components/index.js +1 -0
  96. package/dist/ui/components/line-input.d.ts +2 -3
  97. package/dist/ui/components/line-input.js +4 -20
  98. package/dist/ui/components/mouse.d.ts +1 -1
  99. package/dist/ui/components/mouse.js +4 -3
  100. package/dist/ui/components/prompt-input.d.ts +32 -0
  101. package/dist/ui/components/prompt-input.js +54 -0
  102. package/docs/architecture/boundaries.md +1 -0
  103. package/docs/architecture/history-editor-provenance.md +5 -1
  104. package/docs/architecture/project-structure.md +1 -0
  105. package/docs/architecture/prompt-suggestions.md +68 -0
  106. package/docs/architecture/toolchain.md +1 -1
  107. package/docs/architecture/ui-reference-provenance.md +23 -0
  108. package/docs/ci-release-runbook.md +45 -7
  109. package/docs/features/modal-content-interaction.md +37 -0
  110. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
- import { PROMPT_GLYPH, backgroundSgrSpan, caretCell, composeSubmittedPromptRows, displayWidth, faint, formatSubmittedPromptTime, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, progressStatusText, submittedPromptLayout, stripAnsi, } from "../../../ui/components/index.js";
1
+ import { acceptsTranscriptUpdate } from "../../../contracts/owned-ui/index.js";
2
+ import { backgroundSgrSpan, caretCell, composeSubmittedPromptRows, PromptInput, promptArrow, displayWidth, faint, formatSubmittedPromptTime, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, progressStatusText, submittedPromptLayout, stripAnsi, } from "../../../ui/components/index.js";
2
3
  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, renderPiShellCommandMessage, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
4
+ 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, isPiPromptStyleCompaction, piShellVisibleWidth, piShellTruncateToWidth, piTheme, renderPiShellCommandMessage, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
4
5
  import { PiTuiRuntimeAdapter, classifyPiTuiInput, } from "../tui-runtime/index.js";
5
6
  import { PromptChipStore } from "./prompt-chips.js";
6
7
  import { SessionViewportController } from "./session-viewport-controller.js";
@@ -66,7 +67,7 @@ export class OwnedUiSessionShellRoot {
66
67
  this.#submittedPromptComposer = this.#customViewport
67
68
  ? {
68
69
  layout: submittedPromptLayout,
69
- compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, style)
70
+ compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, { ...style, prefix: text => promptArrow(text, piTheme()) })
70
71
  .map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor)),
71
72
  }
72
73
  : undefined;
@@ -77,10 +78,13 @@ export class OwnedUiSessionShellRoot {
77
78
  this.#onViewportFrame = handlers.onViewportFrame;
78
79
  this.#onInputSurfaceChanged = handlers.onInputSurfaceChanged;
79
80
  this.#dockInputReuseEnabled = handlers.enableDockInputReuse ?? true;
80
- this.header = createPiShellHeader(startup);
81
+ this.header = createPiShellHeader({
82
+ ...startup,
83
+ ...(this.#customViewport ? { getKeybindings: () => this.editor.keybindingConfig() } : {}),
84
+ });
81
85
  this.resources = createPiShellLoadedResources(startup.resources ?? [], startup.expanded ?? false);
82
86
  this.#status = createPiShellStatus(view, progressStatusText, handlers);
83
- this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd);
87
+ this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi");
84
88
  this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned");
85
89
  this.editor = createPiShellEditor({
86
90
  ...handlers,
@@ -122,7 +126,10 @@ export class OwnedUiSessionShellRoot {
122
126
  onToolsExpand: () => this.#setToolsExpanded(!this.#toolsExpanded),
123
127
  ...(this.#customViewport ? {
124
128
  promptPresentation: {
125
- prefix: PROMPT_GLYPH,
129
+ input: new PromptInput({ fg: (token, text) => piTheme().fg(token, text) }, {
130
+ measure: piShellVisibleWidth,
131
+ truncate: piShellTruncateToWidth,
132
+ }),
126
133
  styleSuggestion: faint,
127
134
  styleSuggestionCaret: caretCell,
128
135
  },
@@ -209,6 +216,12 @@ export class OwnedUiSessionShellRoot {
209
216
  prepareHistoryText(text) {
210
217
  return this.#promptChips.prepareHistoryText(text);
211
218
  }
219
+ rehydrateHistoryText(text, resolveImage) {
220
+ return this.#promptChips.rehydrateHistoryText(text, resolveImage);
221
+ }
222
+ imageChipAttachmentsFromEditor(text) {
223
+ return this.#promptChips.imageChipAttachments(text);
224
+ }
212
225
  preparePromptSubmission(text) {
213
226
  return this.#promptChips.prepareSubmission(text);
214
227
  }
@@ -224,17 +237,26 @@ export class OwnedUiSessionShellRoot {
224
237
  this.resetPendingPastes();
225
238
  return this.#promptChips.dispose();
226
239
  }
227
- canPreparePromptSuggestion() {
228
- return this.usesDefaultInputSurface()
229
- && this.#view.dialog === null
230
- && this.#view.overlay === null
231
- && this.editor.getText().length === 0;
232
- }
233
- canPresentPromptSuggestion() {
234
- return this.canPreparePromptSuggestion()
235
- && this.#view.lifecycle === "ready"
236
- && this.editor.canPresentPromptSuggestion();
237
- }
240
+ promptSuggestionPrepareBlockReason() {
241
+ if (!this.usesDefaultInputSurface())
242
+ return "replacement-input";
243
+ if (this.#view.dialog !== null || this.#view.overlay !== null)
244
+ return "modal";
245
+ if (this.editor.getText().length > 0)
246
+ return "draft";
247
+ return null;
248
+ }
249
+ canPreparePromptSuggestion() { return this.promptSuggestionPrepareBlockReason() === null; }
250
+ promptSuggestionPresentationBlockReason() {
251
+ const reason = this.promptSuggestionPrepareBlockReason();
252
+ if (reason !== null)
253
+ return reason;
254
+ if (this.#view.lifecycle !== "ready")
255
+ return "not-ready";
256
+ return this.editor.promptSuggestionBlockReason?.()
257
+ ?? (this.editor.canPresentPromptSuggestion() ? null : "presentation-unavailable");
258
+ }
259
+ canPresentPromptSuggestion() { return this.promptSuggestionPresentationBlockReason() === null; }
238
260
  setPromptSuggestion(text) {
239
261
  this.editor.setPromptSuggestion(text);
240
262
  }
@@ -250,7 +272,8 @@ export class OwnedUiSessionShellRoot {
250
272
  this.#syncTranscript(view.transcript);
251
273
  this.editor.setSubmitEnabled(view.lifecycle !== "stopping" && view.lifecycle !== "stopped" && view.lifecycle !== "failed");
252
274
  this.editor.setThinkingLevel(view.thinkingLevel);
253
- this.invalidate();
275
+ // Performance: semantic updates already invalidate affected transcript blocks. Chrome is a separate authority.
276
+ this.#invalidateChrome();
254
277
  }
255
278
  /**
256
279
  * Applies one block: its component is created or updated in place and the order grows
@@ -260,13 +283,12 @@ export class OwnedUiSessionShellRoot {
260
283
  applyTranscriptBlock(block) {
261
284
  const current = this.#blocksById.get(block.id);
262
285
  // Invariant: a full-view/final presentation may preempt queued partials. Never revive an older revision.
263
- if (current !== undefined && (current.revision > block.revision || current.status === "finalized" && block.status === "live"))
286
+ if (current !== undefined && !acceptsTranscriptUpdate(current, block))
264
287
  return;
265
288
  this.#blocksById.set(block.id, block);
266
289
  const component = this.#transcript.get(block.id);
267
290
  if (component === undefined) {
268
- const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer, this.#outputPad, !this.#thinkingVisible, this.#mermaidRenderingMode, this.#showImages, this.#imageWidthCells, this.#imageAssets);
269
- created.setExpanded(this.#toolsExpanded);
291
+ const created = this.#mountTranscript(block);
270
292
  this.#transcript.set(block.id, created);
271
293
  this.#transcriptOrder.push(block.id);
272
294
  }
@@ -278,6 +300,20 @@ export class OwnedUiSessionShellRoot {
278
300
  this.#renderedRows.delete(block.id);
279
301
  this.#documentLayouts.clear();
280
302
  }
303
+ #mountTranscript(block) {
304
+ const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer, this.#outputPad, !this.#thinkingVisible, this.#mermaidRenderingMode, this.#showImages, this.#imageWidthCells, this.#imageAssets, { ...this.#componentRuntime, changed: () => {
305
+ if (this.#transcript.get(block.id) !== created)
306
+ return;
307
+ this.#renderedRows.delete(block.id);
308
+ this.#documentLayouts.clear();
309
+ this.#visibleViewportSnapshot = undefined;
310
+ this.#dockInputCandidate = false;
311
+ this.#dockInputRevision = undefined;
312
+ this.#componentRuntime.requestRender();
313
+ } });
314
+ created.setExpanded(this.#toolsExpanded);
315
+ return created;
316
+ }
281
317
  render(width) {
282
318
  if (!this.#customViewport) {
283
319
  this.#viewportController.setEditorPointerFrame(undefined);
@@ -297,20 +333,27 @@ export class OwnedUiSessionShellRoot {
297
333
  const dockInputCandidate = this.#dockInputCandidate;
298
334
  // Concurrency: input may change after a keyboard receipt but before its paint.
299
335
  // Capture only the live inputs this composition will actually represent.
300
- const viewportRevision = this.#viewportController.presentationRevision;
301
- const selectionRevision = this.#viewportController.selectionRevision;
302
- const pointerPosition = this.#viewportController.pointerPosition;
303
336
  const inputSurface = this.#inputSurface;
304
337
  const dockRows = dock.rows;
305
338
  const selectableDocumentRowCount = document.rows.length;
306
339
  const dockStartRow = height - dockRows.length + 1;
307
340
  const editorOffset = dock.editorOffset;
341
+ this.#viewportController.setInputSurfaceFrame(this.usesDefaultInputSurface() ? undefined : {
342
+ component: this.#inputSurface,
343
+ columnStart: 1,
344
+ columnEnd: width,
345
+ rowStart: Math.max(1, dockStartRow + editorOffset),
346
+ rowEnd: Math.min(height, dockStartRow + editorOffset + dock.inputRows - 1),
347
+ });
308
348
  this.#viewportController.setEditorPointerFrame(this.usesDefaultInputSurface()
309
349
  ? {
310
350
  rowStart: dockStartRow + editorOffset,
311
351
  rowEnd: dockStartRow + editorOffset + dock.inputRows - 1,
312
352
  }
313
353
  : undefined);
354
+ const viewportRevision = this.#viewportController.presentationRevision;
355
+ const selectionRevision = this.#viewportController.selectionRevision;
356
+ const pointerPosition = this.#viewportController.pointerPosition;
314
357
  this.#dockInputCandidate = false;
315
358
  let frame = dockInputCandidate
316
359
  && snapshot !== undefined
@@ -407,14 +450,31 @@ export class OwnedUiSessionShellRoot {
407
450
  noteCompletedAssistantMessage() {
408
451
  this.#viewportController.noteCompletedAssistantMessage();
409
452
  }
453
+ /** A new session binding owns new mounts even when it reuses semantic invocation ids. */
454
+ resetTranscript() {
455
+ for (const component of this.#transcript.values())
456
+ component.dispose?.();
457
+ this.#transcript.clear();
458
+ this.#blocksById.clear();
459
+ this.#transcriptOrder = [];
460
+ this.#renderedRows.clear();
461
+ this.#documentLayouts.clear();
462
+ }
410
463
  resetViewport() {
411
464
  this.#viewportController.reset();
412
465
  }
413
466
  clearViewportPointerState() {
414
467
  this.#viewportController.clearPointerState();
415
468
  }
416
- handleViewportPreInput(data, allowWheel = true, now = Date.now()) {
417
- return this.#viewportController.handlePreInput(data, allowWheel, now);
469
+ setViewportOverlaySurfaces(surfaces) {
470
+ this.#viewportController.setOverlaySurfaces(surfaces);
471
+ }
472
+ viewportInputGeometryReady(width, height) {
473
+ const frame = this.#viewportController.frame;
474
+ return this.#viewportController.inputGeometryReady && frame?.descriptor.width === width && frame.descriptor.height === height;
475
+ }
476
+ handleViewportPreInput(data, allowWheel = true, now = Date.now(), editorActive = this.usesDefaultInputSurface()) {
477
+ return this.#viewportController.handlePreInput(data, allowWheel, now, editorActive);
418
478
  }
419
479
  #renderDock(width) {
420
480
  return this.#renderDockLayout(width).rows;
@@ -425,13 +485,15 @@ export class OwnedUiSessionShellRoot {
425
485
  const transientRows = [...queued, ...statusRows];
426
486
  const aboveWidgets = this.#renderWidgets("aboveEditor", width);
427
487
  const input = this.#inputSurface.render(width);
488
+ // Invariant: pointer rows describe the body, not the autocomplete block now preceding it.
489
+ const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
428
490
  const belowWidgets = this.#renderWidgets("belowEditor", width);
429
491
  const footer = this.#renderFooter(width);
430
492
  const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
431
493
  return {
432
494
  rows: [...transientRows, ...rowsWithoutTransient],
433
- editorOffset: transientRows.length + aboveWidgets.length,
434
- inputRows: input.length,
495
+ editorOffset: transientRows.length + aboveWidgets.length + (body?.rowOffset ?? 0),
496
+ inputRows: body?.rowCount ?? input.length,
435
497
  };
436
498
  }
437
499
  #renderQueued(width) {
@@ -520,14 +582,16 @@ export class OwnedUiSessionShellRoot {
520
582
  const block = this.#blocksById.get(id);
521
583
  if (!this.#thinkingVisible && block?.kind === "thinking")
522
584
  continue;
585
+ const promptLike = block !== undefined
586
+ && (block.kind === "user" || this.#customViewport && isPiPromptStyleCompaction(block));
523
587
  const blockWidth = this.#customViewport
524
- && block?.kind === "user"
588
+ && promptLike
525
589
  && this.#viewportController.config.scrollbarAppearance !== "hidden"
526
590
  && width > 1
527
591
  ? width - 1
528
592
  : width;
529
593
  const blockRows = this.#blockRows(id, block, blockWidth);
530
- if (block?.kind === "user") {
594
+ if (promptLike) {
531
595
  // Compatibility: the first natural prompt gets one breathing row at the document top.
532
596
  // Once scrolling advances, the prompt itself reaches row zero and then
533
597
  // becomes sticky there, so the spacer is never pinned with it.
@@ -541,7 +605,7 @@ export class OwnedUiSessionShellRoot {
541
605
  liveTailStartRow = firstRow;
542
606
  }
543
607
  rows.push(...blockRows);
544
- if (block?.kind === "user" && blockRows[0] !== undefined) {
608
+ if (promptLike && blockRows[0] !== undefined) {
545
609
  promptAnchors.push({
546
610
  id: block.id,
547
611
  firstRow,
@@ -596,14 +660,15 @@ export class OwnedUiSessionShellRoot {
596
660
  return render();
597
661
  let byWidth = this.#renderedRows.get(id);
598
662
  const cached = byWidth?.get(width);
599
- if (cached?.revision === block.revision)
663
+ const presentationRevision = component.presentationRevision ?? 0;
664
+ if (cached?.revision === block.revision && cached.presentationRevision === presentationRevision)
600
665
  return cached.rows;
601
666
  const rows = render();
602
667
  if (byWidth === undefined) {
603
668
  byWidth = new Map();
604
669
  this.#renderedRows.set(id, byWidth);
605
670
  }
606
- byWidth.set(width, { revision: block.revision, rows });
671
+ byWidth.set(width, { revision: block.revision, presentationRevision, rows });
607
672
  // Invariant: bare A1 probes full width before reserving the overflowing rail column;
608
673
  // retain both widths without turning resize history into an unbounded cache.
609
674
  while (byWidth.size > 2)
@@ -619,7 +684,7 @@ export class OwnedUiSessionShellRoot {
619
684
  const block = this.#blocksById.get(id);
620
685
  if (block === undefined || (!this.#thinkingVisible && block.kind === "thinking"))
621
686
  continue;
622
- if (rows.length > 0 && block.kind === "user")
687
+ if (rows.length > 0 && (block.kind === "user" || this.#customViewport && isPiPromptStyleCompaction(block)))
623
688
  rows.push("");
624
689
  rows.push(...this.#blockRows(id, block, width).map(sanitizeExitTranscriptRow));
625
690
  }
@@ -830,6 +895,7 @@ export class OwnedUiSessionShellRoot {
830
895
  return;
831
896
  }
832
897
  this.#dockInputCandidate = false;
898
+ this.#viewportController.invalidateInputSurface();
833
899
  this.#inputSurface.setFocused?.(false);
834
900
  if (disposePrevious && this.#inputSurface !== this.editor)
835
901
  this.#inputSurface.dispose?.();
@@ -852,10 +918,15 @@ export class OwnedUiSessionShellRoot {
852
918
  }
853
919
  }
854
920
  invalidate() {
855
- this.header.invalidate();
856
- this.resources.invalidate();
921
+ this.#renderedRows.clear();
922
+ this.#documentLayouts.clear();
857
923
  for (const component of this.#transcript.values())
858
924
  component.invalidate();
925
+ this.#invalidateChrome();
926
+ }
927
+ #invalidateChrome() {
928
+ this.header.invalidate();
929
+ this.resources.invalidate();
859
930
  this.editor.invalidate();
860
931
  if (this.#inputSurface !== this.editor)
861
932
  this.#inputSurface.invalidate();
@@ -919,8 +990,7 @@ export class OwnedUiSessionShellRoot {
919
990
  for (const block of blocks) {
920
991
  const component = this.#transcript.get(block.id);
921
992
  if (component === undefined) {
922
- const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer, this.#outputPad, !this.#thinkingVisible, this.#mermaidRenderingMode, this.#showImages, this.#imageWidthCells, this.#imageAssets);
923
- created.setExpanded(this.#toolsExpanded);
993
+ const created = this.#mountTranscript(block);
924
994
  this.#transcript.set(block.id, created);
925
995
  }
926
996
  else if (component.revision !== block.revision) {
@@ -23,6 +23,7 @@ export class OwnedUiSessionShell {
23
23
  #unsubscribePromptSuggestions;
24
24
  #promptSuggestions;
25
25
  #promptHistory = null;
26
+ #promptHistoryImageSidecar;
26
27
  #extensionBridge;
27
28
  #stopped;
28
29
  #resolveStopped;
@@ -102,7 +103,6 @@ export class OwnedUiSessionShell {
102
103
  onEditorChange: () => { this.#editorRevision++; promptSuggestionController?.invalidate(); },
103
104
  onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
104
105
  onInputSurfaceChanged: () => {
105
- this.root.clearViewportPointerState();
106
106
  promptSuggestionController?.invalidate();
107
107
  this.#promptHistory?.synchronize();
108
108
  },
@@ -154,6 +154,7 @@ export class OwnedUiSessionShell {
154
154
  root: this.root,
155
155
  mode: tuiMode,
156
156
  ...(this.#customViewport ? {
157
+ onOverlayGeometry: surfaces => this.root.setViewportOverlaySurfaces(surfaces),
157
158
  decorateTerminal: (terminal) => {
158
159
  damageTerminal = new DamageAwareTerminalAdapter(terminal, {
159
160
  regionalScroll: process.env.TERM !== "dumb",
@@ -194,8 +195,10 @@ export class OwnedUiSessionShell {
194
195
  promptSuggestionController = promptSuggestionOptions === undefined ? null : new ContextualPromptSuggestionController({
195
196
  generator: promptSuggestionOptions.generator,
196
197
  enabled: promptSuggestionOptions.enabled(),
198
+ ...(promptSuggestionOptions.diagnostics === undefined ? {} : { diagnostics: promptSuggestionOptions.diagnostics }),
197
199
  surface: {
198
- canPresent: identity => this.#canPresentPromptSuggestion(identity),
200
+ canPresent: identity => this.#promptSuggestionPresentationBlockReason(identity) === null,
201
+ presentationBlockReason: identity => this.#promptSuggestionPresentationBlockReason(identity),
199
202
  present: text => {
200
203
  if (!this.root.canPresentPromptSuggestion())
201
204
  return false;
@@ -251,16 +254,13 @@ export class OwnedUiSessionShell {
251
254
  this.runtime.requestRender();
252
255
  return { consume: true };
253
256
  }
254
- // Invariant: an overlay or editor-replacement screen owns its entire pointer
255
- // surface. Letting the transcript pre-router inspect those reports
256
- // steals settings value menus and numeric +/- controls before the
257
- // settings app can receive them.
258
- if (this.runtime.hasOverlay() || !this.root.usesDefaultInputSurface()) {
259
- if (data.includes("\u001b[<"))
260
- this.root.clearViewportPointerState();
261
- return undefined;
257
+ // Invariant: geometry must belong to the painted frame, including newly opened/nested
258
+ // surfaces. Steady pointer input does not trigger a synchronous composition.
259
+ const viewport = this.runtime.viewport();
260
+ if (data.includes("\u001b[<") && !this.root.viewportInputGeometryReady(viewport.columns, viewport.rows)) {
261
+ this.runtime.renderNow();
262
262
  }
263
- const routed = this.root.handleViewportPreInput(data, true);
263
+ const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.runtime.hasFocusedOverlay());
264
264
  if (routed.copyText !== undefined) {
265
265
  this.runtime.writeControl(`\u001b]52;c;${Buffer.from(routed.copyText, "utf8").toString("base64")}\u0007`);
266
266
  }
@@ -329,6 +329,8 @@ export class OwnedUiSessionShell {
329
329
  } },
330
330
  });
331
331
  if (this.#customViewport && options.promptHistory !== undefined) {
332
+ this.#promptHistoryImageSidecar = options.promptHistory.imageSidecar;
333
+ const sidecar = this.#promptHistoryImageSidecar;
332
334
  this.#promptHistory = new PromptHistoryController({
333
335
  editor: this.root.editor,
334
336
  store: options.promptHistory.store,
@@ -336,6 +338,7 @@ export class OwnedUiSessionShell {
336
338
  fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
337
339
  active: () => this.root.usesDefaultInputSurface(),
338
340
  render: () => this.runtime.requestRender(),
341
+ rehydrate: value => this.root.rehydrateHistoryText(value, id => sidecar?.readAttachment(id) ?? null),
339
342
  });
340
343
  }
341
344
  this.#extensionBridge = createPiExtensionUiBridge({
@@ -385,19 +388,21 @@ export class OwnedUiSessionShell {
385
388
  }
386
389
  if (event.type === "assistant-message-completed") {
387
390
  this.root.noteCompletedAssistantMessage();
388
- if (event.model !== null) {
389
- this.#promptSuggestions?.consider({
390
- sessionId: event.sessionId,
391
- sessionGeneration: event.sessionGeneration,
392
- runSequence: event.runSequence,
393
- responseSequence: event.responseSequence,
394
- model: event.model,
395
- }, event.successful
396
- && event.stopReason === "stop"
397
- && !event.toolContinuation
398
- && event.assistantMessageCount >= 2
399
- && this.root.canPreparePromptSuggestion());
400
- }
391
+ const identity = {
392
+ sessionId: event.sessionId,
393
+ sessionGeneration: event.sessionGeneration,
394
+ runSequence: event.runSequence,
395
+ responseSequence: event.responseSequence,
396
+ model: event.model,
397
+ };
398
+ if (event.model === null)
399
+ this.#promptSuggestions?.skip(identity, "no-model");
400
+ else
401
+ this.#promptSuggestions?.consider({ ...identity, model: event.model }, !event.successful ? "failed-response"
402
+ : event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
403
+ : event.stopReason !== "stop" ? "incomplete-response"
404
+ : event.assistantMessageCount < 2 ? "early-conversation"
405
+ : this.root.promptSuggestionPrepareBlockReason());
401
406
  }
402
407
  const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
403
408
  const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.sessionGeneration
@@ -432,13 +437,14 @@ export class OwnedUiSessionShell {
432
437
  view() {
433
438
  return this.backend.view();
434
439
  }
435
- #canPresentPromptSuggestion(identity) {
440
+ #promptSuggestionPresentationBlockReason(identity) {
436
441
  const view = this.view();
437
- return !this.#disposed
438
- && identity.sessionId === view.sessionId
439
- && identity.sessionGeneration === this.backend.sessionGeneration
440
- && modelKey(view) === `${identity.model.providerId}/${identity.model.modelId}`
441
- && this.root.canPresentPromptSuggestion();
442
+ if (this.#disposed)
443
+ return "disposed";
444
+ if (identity.sessionId !== view.sessionId || identity.sessionGeneration !== this.backend.sessionGeneration
445
+ || modelKey(view) !== `${identity.model.providerId}/${identity.model.modelId}`)
446
+ return "stale-identity";
447
+ return this.root.promptSuggestionPresentationBlockReason();
442
448
  }
443
449
  damagePresentationDecision() {
444
450
  return this.#damageTerminal?.lastDecision ?? null;
@@ -508,8 +514,21 @@ export class OwnedUiSessionShell {
508
514
  const reusable = this.root.prepareHistoryText(text);
509
515
  if (reusable.length === 0)
510
516
  this.#promptHistory.rememberRecovery(text);
511
- else
517
+ else {
518
+ this.#persistImageSidecars(text);
512
519
  this.#promptHistory.capture(reusable, kind, this.#cwd, this.backend.sessionId);
520
+ }
521
+ }
522
+ }
523
+ // Rationale: write the image chip payloads referenced by an editor draft to the sidecar
524
+ // directory before their history row commits. Missing sidecar dependency = silent skip; the
525
+ // recall path treats an absent sidecar the same as a corrupt one.
526
+ #persistImageSidecars(text) {
527
+ const sidecar = this.#promptHistoryImageSidecar;
528
+ if (sidecar === undefined)
529
+ return;
530
+ for (const record of this.root.imageChipAttachmentsFromEditor(text)) {
531
+ sidecar.write(record.id, { tag: record.tag, data: record.image.data, mimeType: record.image.mimeType, savedAt: new Date().toISOString() });
513
532
  }
514
533
  }
515
534
  async #submit(text) {
@@ -1304,6 +1323,7 @@ export class OwnedUiSessionShell {
1304
1323
  // Invariant: delivery recovery invalidates callbacks, not same-session local recall or its draft.
1305
1324
  if (this.#sessionBindingGeneration !== this.backend.sessionBindingGeneration) {
1306
1325
  this.#sessionBindingGeneration = this.backend.sessionBindingGeneration;
1326
+ this.root.resetTranscript();
1307
1327
  this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
1308
1328
  }
1309
1329
  this.#activeLoginDialog = undefined;
@@ -1,3 +1,4 @@
1
+ import { type PiTuiPointerSurface } from "../tui-runtime/index.js";
1
2
  import type { OwnedUiViewportSettings } from "../../../contracts/owned-ui/index.js";
2
3
  import { type TranscriptViewportFrame, type TranscriptViewportFrameInput } from "../../../ui/components/index.js";
3
4
  import type { PiShellEditorPort } from "../components/index.js";
@@ -38,6 +39,11 @@ export declare class SessionViewportController {
38
39
  readonly rowStart: number;
39
40
  readonly rowEnd: number;
40
41
  } | undefined): void;
42
+ /** Replacement input changed before its new bounds have been painted. */
43
+ invalidateInputSurface(): void;
44
+ setInputSurfaceFrame(surface: PiTuiPointerSurface | undefined): void;
45
+ setOverlaySurfaces(surfaces: readonly PiTuiPointerSurface[] | null): void;
46
+ get inputGeometryReady(): boolean;
41
47
  compose(input: Omit<TranscriptViewportFrameInput, "pointerPosition">): TranscriptViewportFrame;
42
48
  composeDockOnly(dockRows: readonly string[], width: number, height: number): TranscriptViewportFrame | null;
43
49
  /** Returns true when document wrapping may have changed. */
@@ -46,5 +52,5 @@ export declare class SessionViewportController {
46
52
  noteCompletedAssistantMessage(): void;
47
53
  reset(): void;
48
54
  clearPointerState(): void;
49
- handlePreInput(data: string, allowWheel?: boolean, now?: number): SessionViewportInputResult;
55
+ handlePreInput(data: string, allowWheel?: boolean, now?: number, editorActive?: boolean): SessionViewportInputResult;
50
56
  }