@timurproko/a1 0.1.8-dev.182 → 0.1.8-dev.214

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 (142) hide show
  1. package/bin/cli.js +2 -2
  2. package/bin/sync-pi-tui-proxy.js +1 -1
  3. package/bin/ui.js +1 -1
  4. package/dist/cli/dispatch.js +3 -3
  5. package/dist/cli/packages.js +1 -1
  6. package/dist/cli/version-stats.js +3 -3
  7. package/dist/composition/owned-ui.js +1 -1
  8. package/dist/composition/settings-route-host.js +6 -4
  9. package/dist/features/launch/development-launch.js +1 -1
  10. package/dist/features/owned-ui/customization.d.ts +2 -0
  11. package/dist/features/owned-ui/customization.js +2 -0
  12. package/dist/features/owned-ui/diagnostics.d.ts +1 -0
  13. package/dist/features/owned-ui/diagnostics.js +1 -0
  14. package/dist/features/owned-ui/project-trust-prompt.d.ts +11 -6
  15. package/dist/features/owned-ui/project-trust-prompt.js +134 -17
  16. package/dist/features/owned-ui/settings-app.js +47 -50
  17. package/dist/features/workspace/reducer.d.ts +1 -0
  18. package/dist/features/workspace/reducer.js +1 -0
  19. package/dist/features/workspace/router.d.ts +1 -0
  20. package/dist/features/workspace/router.js +1 -0
  21. package/dist/features/workspace/store.d.ts +1 -0
  22. package/dist/features/workspace/store.js +1 -0
  23. package/dist/foundation/launch-guardian/main.js +1 -1
  24. package/dist/foundation/native-host-protocol/codec.d.ts +2 -0
  25. package/dist/foundation/native-host-protocol/codec.js +2 -0
  26. package/dist/foundation/process-containment/linux-process-inspector.d.ts +1 -0
  27. package/dist/foundation/process-containment/linux-process-inspector.js +1 -0
  28. package/dist/foundation/process-containment/native-guardian-containment.d.ts +1 -0
  29. package/dist/foundation/process-containment/native-guardian-containment.js +1 -0
  30. package/dist/foundation/process-containment/windows-process-inspector.d.ts +1 -0
  31. package/dist/foundation/process-containment/windows-process-inspector.js +1 -0
  32. package/dist/foundation/protocol/client.d.ts +1 -0
  33. package/dist/foundation/protocol/client.js +1 -0
  34. package/dist/foundation/protocol/messages.d.ts +1 -0
  35. package/dist/foundation/protocol/messages.js +1 -0
  36. package/dist/foundation/release/bootstrap.js +6 -6
  37. package/dist/foundation/release/cohort-state.d.ts +1 -0
  38. package/dist/foundation/release/cohort-state.js +1 -0
  39. package/dist/foundation/release/release-gc.js +1 -1
  40. package/dist/foundation/release/release.js +1 -1
  41. package/dist/foundation/release/update-transaction.d.ts +1 -0
  42. package/dist/foundation/release/update-transaction.js +1 -0
  43. package/dist/foundation/release/update.js +12 -12
  44. package/dist/foundation/storage/control-store.d.ts +1 -0
  45. package/dist/foundation/storage/control-store.js +1 -0
  46. package/dist/foundation/structured-agent-runtime/backpressure.d.ts +1 -0
  47. package/dist/foundation/structured-agent-runtime/backpressure.js +1 -0
  48. package/dist/foundation/structured-agent-runtime/commands.d.ts +1 -0
  49. package/dist/foundation/structured-agent-runtime/commands.js +1 -0
  50. package/dist/foundation/structured-agent-runtime/reconnection.d.ts +1 -0
  51. package/dist/foundation/structured-agent-runtime/reconnection.js +1 -0
  52. package/dist/foundation/structured-agent-runtime/state.d.ts +1 -0
  53. package/dist/foundation/structured-agent-runtime/state.js +1 -0
  54. package/dist/foundation/supervision/main.js +1 -1
  55. package/dist/foundation/supervision/server.d.ts +1 -0
  56. package/dist/foundation/supervision/server.js +4 -6
  57. package/dist/integrations/pi/components/conformance.d.ts +1 -0
  58. package/dist/integrations/pi/components/conformance.js +1 -0
  59. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  60. package/dist/integrations/pi/components/shell-footer-status.js +8 -7
  61. package/dist/integrations/pi/components/shell-shared-facade.js +1 -1
  62. package/dist/integrations/pi/engine/adapter.d.ts +7 -0
  63. package/dist/integrations/pi/engine/adapter.js +37 -35
  64. package/dist/integrations/pi/engine/conformance.d.ts +2 -0
  65. package/dist/integrations/pi/engine/conformance.js +2 -0
  66. package/dist/integrations/pi/engine/http-dispatcher.js +1 -1
  67. package/dist/integrations/pi/engine/model-auth-integration.d.ts +1 -0
  68. package/dist/integrations/pi/engine/model-auth-integration.js +1 -0
  69. package/dist/integrations/pi/engine/package-integration.js +1 -1
  70. package/dist/integrations/pi/engine/project-trust-preflight.js +1 -1
  71. package/dist/integrations/pi/engine/resource-extension-integration.d.ts +1 -0
  72. package/dist/integrations/pi/engine/resource-extension-integration.js +1 -0
  73. package/dist/integrations/pi/engine/session-integration.d.ts +1 -0
  74. package/dist/integrations/pi/engine/session-integration.js +2 -1
  75. package/dist/integrations/pi/engine/settings-effects.d.ts +13 -2
  76. package/dist/integrations/pi/engine/settings-effects.js +62 -42
  77. package/dist/integrations/pi/engine/settings-integration.js +4 -4
  78. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -0
  79. package/dist/integrations/pi/engine/workflow-controllers.js +1 -0
  80. package/dist/integrations/pi/session-ui/index.d.ts +1 -0
  81. package/dist/integrations/pi/session-ui/index.js +1 -0
  82. package/dist/integrations/pi/session-ui/prompt-chips.js +1 -1
  83. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +30 -2
  84. package/dist/integrations/pi/session-ui/session-shell-root.js +133 -72
  85. package/dist/integrations/pi/session-ui/session-shell.d.ts +9 -2
  86. package/dist/integrations/pi/session-ui/session-shell.js +112 -37
  87. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  88. package/dist/integrations/pi/session-ui/session-viewport-controller.js +44 -11
  89. package/dist/integrations/pi/session-ui/stream-presentation-coalescer.d.ts +29 -0
  90. package/dist/integrations/pi/session-ui/stream-presentation-coalescer.js +79 -0
  91. package/dist/integrations/pi/session-ui/system-clipboard.js +6 -6
  92. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  93. package/dist/integrations/pi/tui-runtime/adapter.js +138 -8
  94. package/dist/integrations/pi/tui-runtime/contracts.d.ts +25 -0
  95. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +63 -0
  96. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +240 -0
  97. package/dist/integrations/pi/tui-runtime/index.d.ts +2 -0
  98. package/dist/integrations/pi/tui-runtime/index.js +2 -0
  99. package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.d.ts +39 -0
  100. package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.js +139 -0
  101. package/dist/native/darwin-arm64/manifest.json +1 -1
  102. package/dist/native/linux-x64/manifest.json +1 -1
  103. package/dist/native/win32-x64/manifest.json +2 -2
  104. package/dist/native/win32-x64/process-guardian.exe +0 -0
  105. package/dist/ui/apps/host.js +5 -9
  106. package/dist/ui/apps/registry.d.ts +1 -0
  107. package/dist/ui/apps/registry.js +1 -0
  108. package/dist/ui/components/dialog-panel.js +5 -3
  109. package/dist/ui/components/frame.d.ts +1 -0
  110. package/dist/ui/components/frame.js +1 -0
  111. package/dist/ui/components/index.d.ts +1 -0
  112. package/dist/ui/components/index.js +1 -0
  113. package/dist/ui/components/line-input.d.ts +1 -6
  114. package/dist/ui/components/line-input.js +4 -9
  115. package/dist/ui/components/list-view.d.ts +0 -6
  116. package/dist/ui/components/list-view.js +14 -7
  117. package/dist/ui/components/progress-status.d.ts +2 -0
  118. package/dist/ui/components/progress-status.js +4 -0
  119. package/dist/ui/components/scrollbar.js +1 -1
  120. package/dist/ui/components/shortcuts.d.ts +1 -0
  121. package/dist/ui/components/shortcuts.js +1 -0
  122. package/dist/ui/components/spans.d.ts +0 -5
  123. package/dist/ui/components/spans.js +3 -8
  124. package/dist/ui/components/surface.d.ts +0 -5
  125. package/dist/ui/components/surface.js +1 -5
  126. package/dist/ui/components/text-selection.d.ts +10 -1
  127. package/dist/ui/components/text-selection.js +96 -29
  128. package/dist/ui/components/text.js +3 -3
  129. package/dist/ui/components/transcript-viewport.d.ts +42 -0
  130. package/dist/ui/components/transcript-viewport.js +287 -39
  131. package/dist/ui/settings/sections.d.ts +0 -2
  132. package/dist/ui/settings/sections.js +10 -16
  133. package/dist/ui/settings/session.js +5 -4
  134. package/dist/ui/settings/store.d.ts +1 -0
  135. package/dist/ui/settings/store.js +2 -1
  136. package/docs/architecture/code-documentation.md +77 -0
  137. package/docs/architecture/project-structure.md +1 -1
  138. package/docs/architecture/ui-reference-provenance.md +23 -3
  139. package/docs/ci-release-runbook.md +30 -4
  140. package/docs/manual-owned-ui-checkpoint.md +28 -1
  141. package/docs/manual-terminal-colour-check.md +4 -3
  142. package/package.json +7 -2
@@ -2,11 +2,13 @@ import { PRODUCT_TEXT } from "../../../product-identity.js";
2
2
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../../ui/components/index.js";
3
3
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
4
4
  import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
5
- import { PiTuiRuntimeAdapter, } from "../tui-runtime/index.js";
5
+ import { DamageAwareTerminalAdapter, PiTuiRuntimeAdapter, classifyPiTuiInput, } from "../tui-runtime/index.js";
6
6
  import { canonicalizeClipboardImage } from "./clipboard-image.js";
7
+ import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
7
8
  import { preloadSystemClipboard, readSystemClipboardContent, writeSystemClipboardText, } from "./system-clipboard.js";
8
9
  import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
9
10
  export { OwnedUiSessionShellRoot } from "./session-shell-root.js";
11
+ /** Coordinates backend, owned presentation, and Pi TUI lifecycles for one interactive session. */
10
12
  export class OwnedUiSessionShell {
11
13
  backend;
12
14
  root;
@@ -26,6 +28,8 @@ export class OwnedUiSessionShell {
26
28
  #disposed = false;
27
29
  #pointerReporting = false;
28
30
  #customViewport;
31
+ #damageTerminal;
32
+ #streamPresentation;
29
33
  #removeViewportPreInput;
30
34
  #unsubscribeSettings;
31
35
  #unbindPiSettings;
@@ -52,11 +56,19 @@ export class OwnedUiSessionShell {
52
56
  this.#resolveStopped = resolve;
53
57
  });
54
58
  let runtime;
59
+ let damageTerminal;
60
+ let streamPresentation;
55
61
  let pendingClipboardWrite = Promise.resolve();
56
62
  this.root = new OwnedUiSessionShellRoot(this.backend.view(), options.cwd, {
57
63
  getColumns: () => runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
58
64
  getRows: () => runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
59
65
  requestRender: force => runtime?.requestRender(force),
66
+ onViewportFrame: frame => damageTerminal?.arm(frame.descriptor, {
67
+ overlayActive: runtime?.hasOverlay() ?? false,
68
+ selectionActive: this.root.hasActiveSelection(),
69
+ replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
70
+ }),
71
+ enableDockInputReuse: options.inputPresentation?.viewportReuse !== false,
60
72
  onSubmit: text => { void this.submit(text); },
61
73
  onInterrupt: () => { void this.interrupt(); },
62
74
  onClear: () => { void this.clearOrExit(); },
@@ -91,7 +103,7 @@ export class OwnedUiSessionShell {
91
103
  }
92
104
  }
93
105
  catch {
94
- // Treat an unavailable or malformed image as text-capable clipboard input.
106
+ // Compatibility: treat an unavailable or malformed image as text-capable clipboard input.
95
107
  }
96
108
  const text = await options.clipboard.readText();
97
109
  return text === null ? null : { kind: "text", text };
@@ -105,7 +117,7 @@ export class OwnedUiSessionShell {
105
117
  }, options.sessionLayout, {
106
118
  resolve: assetId => this.backend.resolveTranscriptImage(assetId),
107
119
  });
108
- // Bare A1 owns a bounded viewport and therefore always runs on the alternate
120
+ // Invariant: bare A1 owns a bounded viewport and therefore always runs on the alternate
109
121
  // fullscreen surface. The pinned comparison profiles still honor Pi's mode.
110
122
  const tuiMode = this.#customViewport
111
123
  ? "fullscreen"
@@ -113,12 +125,41 @@ export class OwnedUiSessionShell {
113
125
  const runtimeOptions = {
114
126
  root: this.root,
115
127
  mode: tuiMode,
116
- ...(this.#customViewport ? {} : { layoutRoot: this.root.layoutRoot() }),
128
+ ...(this.#customViewport ? {
129
+ decorateTerminal: (terminal) => {
130
+ damageTerminal = new DamageAwareTerminalAdapter(terminal, {
131
+ regionalScroll: process.env.TERM !== "dumb",
132
+ onResize: () => streamPresentation?.noteImmediatePresentation(),
133
+ });
134
+ return damageTerminal;
135
+ },
136
+ ...(options.inputPresentation?.coordination === false ? {} : {
137
+ inputCoordination: {
138
+ classify: (data, focusedOverlay) => classifyPiTuiInput(data, focusedOverlay ?? this.root.inputCoordinationSurface()),
139
+ onReceipt: () => streamPresentation?.noteImmediatePresentation(),
140
+ ...(options.inputPresentation?.scheduler === undefined
141
+ ? {}
142
+ : { scheduler: options.inputPresentation.scheduler }),
143
+ },
144
+ }),
145
+ } : { layoutRoot: this.root.layoutRoot() }),
146
+ ...(options.inputPresentation?.onEvent === undefined ? {} : {
147
+ inputDiagnostics: {
148
+ onEvent: options.inputPresentation.onEvent,
149
+ ...(options.inputPresentation.now === undefined ? {} : { now: options.inputPresentation.now }),
150
+ },
151
+ }),
117
152
  ...(options.terminal === undefined ? {} : { terminal: options.terminal }),
118
153
  hardwareCursor: this.backend.view().terminal.hardwareCursor,
119
154
  };
120
155
  runtime = new PiTuiRuntimeAdapter(runtimeOptions);
121
156
  this.runtime = runtime;
157
+ this.#damageTerminal = damageTerminal ?? null;
158
+ const presentationInterval = options.streamPresentation?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
159
+ streamPresentation = options.streamPresentation?.scheduler === undefined
160
+ ? new StreamPresentationCoalescer(() => this.runtime.requestRender(), presentationInterval)
161
+ : new StreamPresentationCoalescer(() => this.runtime.requestRender(), presentationInterval, options.streamPresentation.scheduler);
162
+ this.#streamPresentation = streamPresentation;
122
163
  const initialPiSettings = this.backend.pinnedSettingsSnapshot();
123
164
  this.runtime.setHardwareCursor(initialPiSettings.showHardwareCursor);
124
165
  this.runtime.setClearOnShrink(initialPiSettings.clearOnShrink);
@@ -147,7 +188,9 @@ export class OwnedUiSessionShell {
147
188
  });
148
189
  this.#removeViewportPreInput = this.#customViewport
149
190
  ? this.runtime.addPreInputListener(data => {
150
- // An overlay or editor-replacement screen owns its entire pointer
191
+ if (options.inputPresentation?.coordination === false)
192
+ this.#streamPresentation.noteImmediatePresentation();
193
+ // Invariant: an overlay or editor-replacement screen owns its entire pointer
151
194
  // surface. Letting the transcript pre-router inspect those reports
152
195
  // steals settings value menus and numeric +/- controls before the
153
196
  // settings app can receive them.
@@ -228,7 +271,7 @@ export class OwnedUiSessionShell {
228
271
  requestRender: () => this.runtime.requestRender(),
229
272
  },
230
273
  agentDir: this.backend.agentDir,
231
- setInputSurface: component => this.root.setInputSurface(component),
274
+ setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
232
275
  showOverlay: (component, overlayOptions) => this.runtime.showOverlay(component, overlayOptions),
233
276
  listenInput: handler => this.runtime.addInputListener(handler),
234
277
  replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
@@ -242,7 +285,7 @@ export class OwnedUiSessionShell {
242
285
  setEditorText: text => this.root.editor.setText(text),
243
286
  pasteToEditor: text => this.root.editor.insertText(text),
244
287
  addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
245
- setCustomEditor: component => this.root.setInputSurface(component),
288
+ setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
246
289
  getFooterData: () => this.root.extensionFooterData(),
247
290
  getToolsExpanded: () => this.root.toolsExpanded,
248
291
  setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
@@ -255,7 +298,7 @@ export class OwnedUiSessionShell {
255
298
  });
256
299
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
257
300
  this.#unsubscribe = this.backend.onEvent(event => {
258
- // A streamed chunk names one block, and touching only that block is what keeps the
301
+ // Performance: a streamed chunk names one block, and touching only that block is what keeps the
259
302
  // cost of a chunk the same in a long session as in a new one. Everything else
260
303
  // resynchronizes the view, which is cheap next to re-reading the transcript.
261
304
  if (event.type === "agent-run-started")
@@ -278,6 +321,9 @@ export class OwnedUiSessionShell {
278
321
  view() {
279
322
  return this.backend.view();
280
323
  }
324
+ damagePresentationDecision() {
325
+ return this.#damageTerminal?.lastDecision ?? null;
326
+ }
281
327
  start() {
282
328
  if (this.#started)
283
329
  return;
@@ -367,7 +413,7 @@ export class OwnedUiSessionShell {
367
413
  this.root.editor.setText("");
368
414
  return { outcome: "completed", diagnostic: null };
369
415
  }
370
- // Match Pi: with an empty editor, two escapes inside 500 ms open the
416
+ // Compatibility: match Pi: with an empty editor, two escapes inside 500 ms open the
371
417
  // configured session navigator. The first escape intentionally does not
372
418
  // interrupt or mutate the prompt.
373
419
  const action = this.backend.pinnedSettingsSnapshot().doubleEscapeAction;
@@ -486,7 +532,12 @@ export class OwnedUiSessionShell {
486
532
  onCancel?.();
487
533
  },
488
534
  });
489
- const handle = this.runtime.showOverlay(component, { width: "70%", maxHeight: "80%", anchor: "center" });
535
+ const handle = this.runtime.showOverlay(component, {
536
+ width: "70%",
537
+ maxHeight: "80%",
538
+ anchor: "center",
539
+ inputCoordination: "owned",
540
+ });
490
541
  this.#dialogHandle = handle;
491
542
  return handle;
492
543
  }
@@ -686,7 +737,7 @@ export class OwnedUiSessionShell {
686
737
  return;
687
738
  }
688
739
  if (callback === "onTuiModeChange") {
689
- // The custom bare-A1 surface is permanently fullscreen; this callback
740
+ // Invariant: the custom bare-A1 surface is permanently fullscreen; this callback
690
741
  // remains available only to pinned comparison profiles.
691
742
  if (this.#customViewport)
692
743
  return;
@@ -907,12 +958,7 @@ export class OwnedUiSessionShell {
907
958
  this.root.appendDaxnuts();
908
959
  }
909
960
  }
910
- /**
911
- * Turns terminal pointer reporting on for a screen that reads the pointer, and off for
912
- * every path that ends it. While it is on the terminal hands A1 the wheel and the
913
- * button instead of scrolling and selecting itself, so leaving it on outlives the
914
- * screen that wanted it and takes the terminal's own scrolling and selection with it.
915
- */
961
+ // Invariant: pointer reporting is disabled on every path that ends the owning screen.
916
962
  #setPointerReporting(enabled, forceOff = false) {
917
963
  const effective = forceOff ? false : this.#customViewport || enabled;
918
964
  if (this.#pointerReporting === effective)
@@ -929,13 +975,16 @@ export class OwnedUiSessionShell {
929
975
  this.root.clearViewportPointerState();
930
976
  this.#setPointerReporting(false, true);
931
977
  this.#removeViewportPreInput();
978
+ this.#streamPresentation.dispose();
932
979
  this.#unsubscribeSettings();
933
980
  const exitMode = this.backend.disposed
934
981
  ? this.#fullscreenExitOutput
935
982
  : this.backend.pinnedSettingsSnapshot().fullscreenExitOutput;
936
983
  const exitTranscript = this.root.exitTranscript(this.runtime.viewport().columns);
937
- const sessionFile = this.backend.currentSessionFile();
938
- const resumeHint = sessionFile === null ? "" : `Resume: ${formatSessionResumeCommand(sessionFile)}`;
984
+ const resume = this.backend.currentSessionResumeMetadata();
985
+ const resumeHint = resume === null
986
+ ? ""
987
+ : `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
939
988
  const fullscreenExitText = this.runtime.mode !== "fullscreen"
940
989
  ? ""
941
990
  : exitMode === "resume-hint"
@@ -952,13 +1001,18 @@ export class OwnedUiSessionShell {
952
1001
  if (fullscreenExitText.length > 0)
953
1002
  this.runtime.writeAfterStop(`${fullscreenExitText}\n`);
954
1003
  }
955
- /**
956
- * Applies one transcript block without re-reading the session. The listeners still hear
957
- * the view they would have heard, so nothing downstream can tell the difference.
958
- */
1004
+ // Invariant: incremental block updates notify listeners with the same complete view contract.
959
1005
  #syncBlock(block) {
960
1006
  this.root.applyTranscriptBlock(block);
961
- this.runtime.requestRender();
1007
+ if (this.#customViewport && block.status === "live" && isCoalescedStreamBlock(block.kind)) {
1008
+ this.#streamPresentation.request();
1009
+ }
1010
+ else if (this.#streamPresentation.pending) {
1011
+ this.#streamPresentation.flush();
1012
+ }
1013
+ else {
1014
+ this.#streamPresentation.presentImmediate();
1015
+ }
962
1016
  const view = this.view();
963
1017
  for (const listener of this.#listeners)
964
1018
  listener(view);
@@ -970,12 +1024,13 @@ export class OwnedUiSessionShell {
970
1024
  this.runtime.setTerminalProgress(this.#terminalProgressEnabled && view.lifecycle === "busy");
971
1025
  }
972
1026
  #syncView() {
1027
+ this.#streamPresentation.noteImmediatePresentation();
973
1028
  const view = this.view();
974
1029
  if (this.backend.sessionGeneration !== this.#sessionGeneration) {
975
1030
  this.#sessionGeneration = this.backend.sessionGeneration;
976
1031
  this.#activeLoginDialog = undefined;
977
1032
  this.#extensionBridge.reset();
978
- // A replaced session takes its transient viewport and owned-route state with it.
1033
+ // Invariant: a replaced session takes its transient viewport and owned-route state with it.
979
1034
  this.root.resetViewport();
980
1035
  this.#dialogHandle?.hide();
981
1036
  this.#dialogHandle = undefined;
@@ -1001,10 +1056,10 @@ export class OwnedUiSessionShell {
1001
1056
  return { outcome: "failed", diagnostic: "runtime is not active" };
1002
1057
  this.#dialogHandle?.hide();
1003
1058
  this.#dialogSource = "route";
1004
- // Any-event reporting: hover and drag are what the screen is driven by, and
1059
+ // Protocol: any-event reporting: hover and drag are what the screen is driven by, and
1005
1060
  // it also stops the terminal treating a drag as a text selection.
1006
1061
  this.#setPointerReporting(true);
1007
- // The interrupt chord is global, so it is watched on raw input rather than
1062
+ // Protocol: the interrupt chord is global, so it is watched on raw input rather than
1008
1063
  // through the overlay: the pinned shell handles that key before an overlay
1009
1064
  // ever sees it, which is why an owned screen must not rely on being asked.
1010
1065
  let armedAt = 0;
@@ -1020,7 +1075,7 @@ export class OwnedUiSessionShell {
1020
1075
  }
1021
1076
  armedAt = now;
1022
1077
  this.runtime.requestRender();
1023
- // The presented screen owns the chord, so the pinned shell never sees a
1078
+ // Invariant: the presented screen owns the chord, so the pinned shell never sees a
1024
1079
  // stray interrupt while it is up.
1025
1080
  return { consume: true };
1026
1081
  });
@@ -1034,7 +1089,7 @@ export class OwnedUiSessionShell {
1034
1089
  this.#dialogId = undefined;
1035
1090
  this.#dialogSource = undefined;
1036
1091
  };
1037
- // Fullscreen Pi owns a fallback text-selection layer before focused overlay
1092
+ // Compatibility: fullscreen Pi owns a fallback text-selection layer before focused overlay
1038
1093
  // components see pointer input. Route every mouse report to the owned screen
1039
1094
  // at the pre-input boundary so dropdowns, value hover, and numeric +/- work,
1040
1095
  // and consume even unhandled reports so settings content is never selected.
@@ -1072,7 +1127,12 @@ export class OwnedUiSessionShell {
1072
1127
  closeSurface();
1073
1128
  void this.shutdown();
1074
1129
  });
1075
- this.#dialogHandle = this.runtime.showOverlay(component, { width: "100%", maxHeight: "100%", anchor: "top-left" });
1130
+ this.#dialogHandle = this.runtime.showOverlay(component, {
1131
+ width: "100%",
1132
+ maxHeight: "100%",
1133
+ anchor: "top-left",
1134
+ inputCoordination: "owned",
1135
+ });
1076
1136
  this.#dialogId = surface.id;
1077
1137
  return { outcome: "completed", diagnostic: null };
1078
1138
  }
@@ -1080,7 +1140,7 @@ export class OwnedUiSessionShell {
1080
1140
  if (!this.runtime.active)
1081
1141
  return;
1082
1142
  if (dialog === null) {
1083
- // Locally owned routes (notably /settings) are independent of backend
1143
+ // Invariant: locally owned routes (notably /settings) are independent of backend
1084
1144
  // lifecycle/status events and remain open while an agent is working.
1085
1145
  if (this.#dialogSource !== "backend")
1086
1146
  return;
@@ -1108,7 +1168,12 @@ export class OwnedUiSessionShell {
1108
1168
  this.#dialogSource = undefined;
1109
1169
  },
1110
1170
  });
1111
- this.#dialogHandle = this.runtime.showOverlay(component, { width: "70%", maxHeight: "80%", anchor: "center" });
1171
+ this.#dialogHandle = this.runtime.showOverlay(component, {
1172
+ width: "70%",
1173
+ maxHeight: "80%",
1174
+ anchor: "center",
1175
+ inputCoordination: "owned",
1176
+ });
1112
1177
  this.#dialogId = dialog.id;
1113
1178
  }
1114
1179
  async #slashCommand(text) {
@@ -1120,7 +1185,7 @@ export class OwnedUiSessionShell {
1120
1185
  return this.#openOwnedRoute(name);
1121
1186
  if (isWorkflowRoute(name))
1122
1187
  return this.runWorkflow({ command: name, argument });
1123
- // Unknown slash input, prompt templates, skills, and extension commands remain Pi prompt input.
1188
+ // Compatibility: unknown slash input, prompt templates, skills, and extension commands remain Pi prompt input.
1124
1189
  this.root.editor.addToHistory(text);
1125
1190
  this.root.resumeViewportFollowing();
1126
1191
  return this.#execute({
@@ -1259,14 +1324,24 @@ export class OwnedUiSessionShell {
1259
1324
  return `pi-shell-${prefix}-${this.#sequence}`;
1260
1325
  }
1261
1326
  }
1262
- export function formatSessionResumeCommand(sessionFile) {
1263
- return `${PRODUCT_TEXT.commandName} --session ${quoteCommandArgument(sessionFile)}`;
1327
+ function isCoalescedStreamBlock(kind) {
1328
+ return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
1329
+ }
1330
+ export function formatSessionResumeCommand(metadata) {
1331
+ const args = [PRODUCT_TEXT.commandName];
1332
+ if (!metadata.usesDefaultSessionDir)
1333
+ args.push("--session-dir", quoteCommandArgument(metadata.sessionDir));
1334
+ args.push("--session", metadata.sessionId);
1335
+ return args.join(" ");
1264
1336
  }
1265
1337
  export function quoteCommandArgument(value) {
1266
- if (process.platform === "win32")
1267
- return `"${value.replaceAll("\"", "\"\"")}"`;
1338
+ if (value.length > 0 && !/[^a-zA-Z0-9_\-./~:@]/.test(value))
1339
+ return value;
1268
1340
  return `'${value.replaceAll("'", `'\\''`)}'`;
1269
1341
  }
1342
+ function dim(value) {
1343
+ return `\u001b[2m${value}\u001b[22m`;
1344
+ }
1270
1345
  function modelReference(model) {
1271
1346
  if (typeof model !== "object" || model === null)
1272
1347
  return "";
@@ -24,11 +24,16 @@ export declare class SessionViewportController {
24
24
  get config(): OwnedUiViewportSettings;
25
25
  get editorPointerSelecting(): boolean;
26
26
  get transcriptPointerSelecting(): boolean;
27
+ get hasSelection(): boolean;
28
+ get selectionRevision(): number;
29
+ get presentationRevision(): number;
30
+ get frame(): TranscriptViewportFrame | null;
27
31
  setEditorPointerFrame(frame: {
28
32
  readonly rowStart: number;
29
33
  readonly rowEnd: number;
30
34
  } | undefined): void;
31
35
  compose(input: TranscriptViewportFrameInput): TranscriptViewportFrame;
36
+ composeDockOnly(dockRows: readonly string[], width: number, height: number): TranscriptViewportFrame | null;
32
37
  /** Returns true when document wrapping may have changed. */
33
38
  setConfig(config: OwnedUiViewportSettings): boolean;
34
39
  resumeFollowing(): void;
@@ -16,21 +16,26 @@ export class SessionViewportController {
16
16
  scrollbarSpeed: "normal",
17
17
  };
18
18
  #dragGrabOffset = null;
19
- /** A left-button sequence begun in status/editor/footer chrome is swallowed. */
19
+ // Invariant: a left-button sequence begun in dock chrome remains owned by the dock.
20
20
  #dockPointerSuppressed = false;
21
- /** True only while the editor owns a held left mouse button. */
21
+ // Invariant: editor selection remains true only while its left button is held.
22
22
  #editorPointerSelecting = false;
23
- /** Last composed terminal rows occupied by the default prompt editor. */
23
+ // Invariant: pointer routing uses the most recently composed editor rows.
24
24
  #editorPointerFrame;
25
25
  #selectionAutoScrollTimer;
26
26
  #selectionAutoScrollPointer;
27
27
  #pointerPosition;
28
28
  #hoveredHyperlinkKey;
29
+ #lastRequestedSelectionRevision = -1;
30
+ #presentationRevision = 0;
29
31
  #activityTimer;
30
32
  constructor(options) {
31
33
  this.#enabled = options.enabled;
32
34
  this.#editor = options.editor;
33
- this.#requestRender = options.requestRender;
35
+ this.#requestRender = force => {
36
+ this.#presentationRevision += 1;
37
+ options.requestRender(force);
38
+ };
34
39
  this.#hasEditorLinks = options.hasEditorLinks ?? (() => false);
35
40
  }
36
41
  get config() {
@@ -42,16 +47,36 @@ export class SessionViewportController {
42
47
  get transcriptPointerSelecting() {
43
48
  return this.#viewport.selectionActive;
44
49
  }
50
+ get hasSelection() {
51
+ return this.#viewport.hasSelection;
52
+ }
53
+ get selectionRevision() {
54
+ return this.#viewport.selectionRevision;
55
+ }
56
+ get presentationRevision() {
57
+ return this.#presentationRevision;
58
+ }
59
+ get frame() {
60
+ return this.#viewport.frame;
61
+ }
45
62
  setEditorPointerFrame(frame) {
46
63
  this.#editorPointerFrame = frame;
47
64
  }
48
65
  compose(input) {
49
66
  this.#viewport.setConfig(this.#config);
67
+ const selectionRevision = this.#viewport.selectionRevision;
50
68
  const frame = this.#viewport.compose(input);
69
+ // Concurrency: if selection changed during composition, this frame truthfully carries
70
+ // the older revision and exactly one ordinary follow-up render publishes the latest.
71
+ if (this.#viewport.selectionRevision !== selectionRevision
72
+ && this.#lastRequestedSelectionRevision < this.#viewport.selectionRevision) {
73
+ this.#lastRequestedSelectionRevision = this.#viewport.selectionRevision;
74
+ this.#requestRender();
75
+ }
51
76
  if (this.#pointerPosition !== undefined && !this.#viewport.selectionActive && !this.#editorPointerSelecting) {
52
77
  const next = this.#hyperlinkKeyAt(frame, this.#pointerPosition.column, this.#pointerPosition.row);
53
78
  if (this.#hoveredHyperlinkKey !== undefined && next !== this.#hoveredHyperlinkKey) {
54
- // Windows Terminal can leave its solid native-hover underline behind
79
+ // Platform: Windows Terminal can leave its solid native-hover underline behind
55
80
  // when a following viewport moves the link away from a stationary pointer.
56
81
  this.#requestRender(true);
57
82
  }
@@ -59,6 +84,9 @@ export class SessionViewportController {
59
84
  }
60
85
  return frame;
61
86
  }
87
+ composeDockOnly(dockRows, width, height) {
88
+ return this.#viewport.composeDockOnly(dockRows, width, height);
89
+ }
62
90
  /** Returns true when document wrapping may have changed. */
63
91
  setConfig(config) {
64
92
  const appearanceChanged = config.scrollbarAppearance !== this.#config.scrollbarAppearance;
@@ -84,6 +112,7 @@ export class SessionViewportController {
84
112
  this.#requestRender();
85
113
  }
86
114
  reset() {
115
+ this.#presentationRevision += 1;
87
116
  this.#clearActivityTimer();
88
117
  this.#stopSelectionAutoScroll();
89
118
  this.#editorPointerSelecting = false;
@@ -93,6 +122,7 @@ export class SessionViewportController {
93
122
  this.#viewport.reset();
94
123
  }
95
124
  clearPointerState() {
125
+ this.#presentationRevision += 1;
96
126
  this.#clearActivityTimer();
97
127
  this.#dragGrabOffset = null;
98
128
  this.#dockPointerSuppressed = false;
@@ -107,15 +137,15 @@ export class SessionViewportController {
107
137
  handlePreInput(data, allowWheel = true, now = Date.now()) {
108
138
  if (!this.#enabled)
109
139
  return { data, consumed: false };
110
- // Pi components share one keybinding manager. Restore bare A1's aliases
140
+ // Compatibility: Pi components share one keybinding manager. Restore bare A1's aliases
111
141
  // before the focused vanilla editor handles this input.
112
142
  this.#editor.activateKeybindings();
113
- // Handle the physical paste chord at the pre-input boundary. Windows
143
+ // Platform: handle the physical paste chord at the pre-input boundary. Windows
114
144
  // terminals vary between forwarding Ctrl+V and terminal-owned bracketed paste.
115
145
  if (this.#editor.matchesTerminalKey(data, "ctrl+v") && this.#editor.pasteClipboard()) {
116
146
  return { data: "", consumed: true };
117
147
  }
118
- // URL chip deletion must overwrite terminal link cells in the same frame.
148
+ // Platform: URL chip deletion must overwrite terminal link cells in the same frame.
119
149
  if (EDITOR_LINK_DELETION_INPUTS.has(data) && this.#hasEditorLinks())
120
150
  this.#requestRender(true);
121
151
  if (this.#viewport.frame === null)
@@ -180,7 +210,7 @@ export class SessionViewportController {
180
210
  if (!this.#viewport.selectionActive && !this.#editorPointerSelecting) {
181
211
  const nextHyperlink = this.#hyperlinkKeyAt(frame, event.column, event.row);
182
212
  if (this.#hoveredHyperlinkKey !== undefined && nextHyperlink !== this.#hoveredHyperlinkKey) {
183
- // A forced redraw overwrites Windows Terminal's cached solid hover
213
+ // Platform: a forced redraw overwrites Windows Terminal's cached solid hover
184
214
  // underline exactly once when the pointer leaves or changes links.
185
215
  forceRepaint = true;
186
216
  }
@@ -308,7 +338,7 @@ export class SessionViewportController {
308
338
  }
309
339
  if (this.#viewport.releaseSelection()) {
310
340
  this.#stopSelectionAutoScroll();
311
- // Restore OSC 8 links only after the held-button selection paint has
341
+ // Platform: restore OSC 8 links only after the held-button selection paint has
312
342
  // ended, then overwrite any terminal-cached hover cells immediately.
313
343
  forceRepaint = true;
314
344
  repaint = true;
@@ -329,8 +359,10 @@ export class SessionViewportController {
329
359
  });
330
360
  if (activity)
331
361
  this.#scheduleActivityExpiry();
332
- if (repaint)
362
+ if (repaint) {
363
+ this.#lastRequestedSelectionRevision = this.#viewport.selectionRevision;
333
364
  this.#requestRender(forceRepaint);
365
+ }
334
366
  return routed;
335
367
  }
336
368
  #hyperlinkKeyAt(frame, column, row) {
@@ -370,6 +402,7 @@ export class SessionViewportController {
370
402
  this.#stopSelectionAutoScroll();
371
403
  return;
372
404
  }
405
+ this.#lastRequestedSelectionRevision = this.#viewport.selectionRevision;
373
406
  this.#requestRender();
374
407
  this.#scheduleActivityExpiry();
375
408
  this.#selectionAutoScrollTimer = setTimeout(() => this.#selectionAutoScrollTick(), SELECTION_AUTO_SCROLL_INTERVAL_MS);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 33 ms (30.3 fps): baseline chunks arrive every 5 ms, while this cadence
3
+ * retains immediate first paint/input and explicit final-state flushing.
4
+ */
5
+ export declare const STREAM_PRESENTATION_INTERVAL_MS = 33;
6
+ export interface StreamPresentationScheduler {
7
+ now(): number;
8
+ setTimeout(callback: () => void, delayMs: number): ReturnType<typeof setTimeout>;
9
+ clearTimeout(timer: ReturnType<typeof setTimeout>): void;
10
+ }
11
+ /** Bounds terminal presentation requests while semantic root state remains current. */
12
+ export declare class StreamPresentationCoalescer {
13
+ #private;
14
+ readonly present: () => void;
15
+ readonly intervalMs: number;
16
+ readonly scheduler: StreamPresentationScheduler;
17
+ constructor(present: () => void, intervalMs?: number, scheduler?: StreamPresentationScheduler);
18
+ get pending(): boolean;
19
+ request(): void;
20
+ /** Cancels a superseded stream frame; the immediate input/overlay/resize path renders current state. */
21
+ preempt(): void;
22
+ /** Records an immediate input/status/overlay frame and cancels an obsolete stream timer. */
23
+ noteImmediatePresentation(): void;
24
+ /** Presents current semantic state immediately regardless of pending stream work. */
25
+ presentImmediate(): void;
26
+ /** Presents the newest semantic state immediately only when a stream frame is pending. */
27
+ flush(): void;
28
+ dispose(): void;
29
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 33 ms (30.3 fps): baseline chunks arrive every 5 ms, while this cadence
3
+ * retains immediate first paint/input and explicit final-state flushing.
4
+ */
5
+ export const STREAM_PRESENTATION_INTERVAL_MS = 33;
6
+ const SYSTEM_SCHEDULER = {
7
+ now: Date.now,
8
+ setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
9
+ clearTimeout: timer => clearTimeout(timer),
10
+ };
11
+ /** Bounds terminal presentation requests while semantic root state remains current. */
12
+ export class StreamPresentationCoalescer {
13
+ present;
14
+ intervalMs;
15
+ scheduler;
16
+ #timer;
17
+ #lastPresentationAt = Number.NEGATIVE_INFINITY;
18
+ #disposed = false;
19
+ constructor(present, intervalMs = STREAM_PRESENTATION_INTERVAL_MS, scheduler = SYSTEM_SCHEDULER) {
20
+ this.present = present;
21
+ this.intervalMs = intervalMs;
22
+ this.scheduler = scheduler;
23
+ if (!Number.isSafeInteger(intervalMs) || intervalMs < 1)
24
+ throw new RangeError("stream presentation interval must be positive");
25
+ }
26
+ get pending() { return this.#timer !== undefined; }
27
+ request() {
28
+ if (this.#disposed || this.#timer !== undefined)
29
+ return;
30
+ const elapsed = this.scheduler.now() - this.#lastPresentationAt;
31
+ if (elapsed >= this.intervalMs) {
32
+ this.#presentNow();
33
+ return;
34
+ }
35
+ this.#timer = this.scheduler.setTimeout(() => {
36
+ this.#timer = undefined;
37
+ this.#presentNow();
38
+ }, Math.max(0, this.intervalMs - elapsed));
39
+ }
40
+ /** Cancels a superseded stream frame; the immediate input/overlay/resize path renders current state. */
41
+ preempt() {
42
+ if (this.#timer === undefined)
43
+ return;
44
+ this.scheduler.clearTimeout(this.#timer);
45
+ this.#timer = undefined;
46
+ }
47
+ /** Records an immediate input/status/overlay frame and cancels an obsolete stream timer. */
48
+ noteImmediatePresentation() {
49
+ if (this.#disposed)
50
+ return;
51
+ this.preempt();
52
+ this.#lastPresentationAt = this.scheduler.now();
53
+ }
54
+ /** Presents current semantic state immediately regardless of pending stream work. */
55
+ presentImmediate() {
56
+ if (this.#disposed)
57
+ return;
58
+ this.noteImmediatePresentation();
59
+ this.present();
60
+ }
61
+ /** Presents the newest semantic state immediately only when a stream frame is pending. */
62
+ flush() {
63
+ if (this.#disposed || this.#timer === undefined)
64
+ return;
65
+ this.scheduler.clearTimeout(this.#timer);
66
+ this.#timer = undefined;
67
+ this.#presentNow();
68
+ }
69
+ dispose() {
70
+ this.preempt();
71
+ this.#disposed = true;
72
+ }
73
+ #presentNow() {
74
+ if (this.#disposed)
75
+ return;
76
+ this.#lastPresentationAt = this.scheduler.now();
77
+ this.present();
78
+ }
79
+ }