@timurproko/a1 0.1.8-dev.157 → 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 (171) hide show
  1. package/README.md +32 -0
  2. package/bin/cli.js +2 -2
  3. package/bin/sync-pi-tui-proxy.js +1 -1
  4. package/bin/ui.js +18 -3
  5. package/dist/cli/dispatch.js +3 -3
  6. package/dist/cli/packages.js +1 -1
  7. package/dist/cli/version-stats.js +3 -3
  8. package/dist/composition/owned-ui.d.ts +4 -1
  9. package/dist/composition/owned-ui.js +9 -6
  10. package/dist/composition/settings-route-host.js +6 -4
  11. package/dist/contracts/agent-engine/capability-ports.d.ts +2 -2
  12. package/dist/contracts/agent-engine/domain-validation.js +22 -4
  13. package/dist/contracts/agent-engine/domain.d.ts +17 -0
  14. package/dist/contracts/owned-ui/model.d.ts +8 -0
  15. package/dist/contracts/owned-ui/validation.js +11 -0
  16. package/dist/features/launch/development-launch.js +1 -1
  17. package/dist/features/owned-ui/customization.d.ts +2 -0
  18. package/dist/features/owned-ui/customization.js +2 -0
  19. package/dist/features/owned-ui/diagnostics.d.ts +1 -0
  20. package/dist/features/owned-ui/diagnostics.js +1 -0
  21. package/dist/features/owned-ui/index.d.ts +1 -0
  22. package/dist/features/owned-ui/index.js +1 -0
  23. package/dist/features/owned-ui/project-trust-prompt.d.ts +25 -0
  24. package/dist/features/owned-ui/project-trust-prompt.js +149 -0
  25. package/dist/features/owned-ui/settings-app.js +69 -44
  26. package/dist/features/workspace/reducer.d.ts +1 -0
  27. package/dist/features/workspace/reducer.js +1 -0
  28. package/dist/features/workspace/router.d.ts +1 -0
  29. package/dist/features/workspace/router.js +1 -0
  30. package/dist/features/workspace/store.d.ts +1 -0
  31. package/dist/features/workspace/store.js +1 -0
  32. package/dist/foundation/launch-guardian/main.js +1 -1
  33. package/dist/foundation/native-host-protocol/codec.d.ts +2 -0
  34. package/dist/foundation/native-host-protocol/codec.js +2 -0
  35. package/dist/foundation/process-containment/linux-process-inspector.d.ts +1 -0
  36. package/dist/foundation/process-containment/linux-process-inspector.js +1 -0
  37. package/dist/foundation/process-containment/native-guardian-containment.d.ts +1 -0
  38. package/dist/foundation/process-containment/native-guardian-containment.js +1 -0
  39. package/dist/foundation/process-containment/windows-process-inspector.d.ts +1 -0
  40. package/dist/foundation/process-containment/windows-process-inspector.js +1 -0
  41. package/dist/foundation/protocol/client.d.ts +1 -0
  42. package/dist/foundation/protocol/client.js +1 -0
  43. package/dist/foundation/protocol/messages.d.ts +1 -0
  44. package/dist/foundation/protocol/messages.js +1 -0
  45. package/dist/foundation/release/bootstrap.js +6 -6
  46. package/dist/foundation/release/cohort-state.d.ts +1 -0
  47. package/dist/foundation/release/cohort-state.js +1 -0
  48. package/dist/foundation/release/release-gc.js +1 -1
  49. package/dist/foundation/release/release.js +1 -1
  50. package/dist/foundation/release/update-transaction.d.ts +1 -0
  51. package/dist/foundation/release/update-transaction.js +1 -0
  52. package/dist/foundation/release/update.js +12 -12
  53. package/dist/foundation/storage/control-store.d.ts +1 -0
  54. package/dist/foundation/storage/control-store.js +1 -0
  55. package/dist/foundation/structured-agent-runtime/backpressure.d.ts +1 -0
  56. package/dist/foundation/structured-agent-runtime/backpressure.js +1 -0
  57. package/dist/foundation/structured-agent-runtime/commands.d.ts +1 -0
  58. package/dist/foundation/structured-agent-runtime/commands.js +1 -0
  59. package/dist/foundation/structured-agent-runtime/reconnection.d.ts +1 -0
  60. package/dist/foundation/structured-agent-runtime/reconnection.js +1 -0
  61. package/dist/foundation/structured-agent-runtime/state.d.ts +1 -0
  62. package/dist/foundation/structured-agent-runtime/state.js +1 -0
  63. package/dist/foundation/supervision/main.js +1 -1
  64. package/dist/foundation/supervision/server.d.ts +1 -0
  65. package/dist/foundation/supervision/server.js +4 -6
  66. package/dist/integrations/pi/components/conformance.d.ts +1 -0
  67. package/dist/integrations/pi/components/conformance.js +1 -0
  68. package/dist/integrations/pi/components/shell-components.d.ts +1 -0
  69. package/dist/integrations/pi/components/shell-components.js +1 -0
  70. package/dist/integrations/pi/components/shell-editor-autocomplete.js +10 -0
  71. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  72. package/dist/integrations/pi/components/shell-footer-status.js +19 -13
  73. package/dist/integrations/pi/components/shell-presenters-info.d.ts +36 -0
  74. package/dist/integrations/pi/components/shell-presenters-info.js +78 -0
  75. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -36
  76. package/dist/integrations/pi/components/shell-presenters-transcript.js +88 -128
  77. package/dist/integrations/pi/components/shell-shared-facade.d.ts +11 -1
  78. package/dist/integrations/pi/components/shell-shared-facade.js +1 -1
  79. package/dist/integrations/pi/engine/adapter.d.ts +22 -4
  80. package/dist/integrations/pi/engine/adapter.js +220 -70
  81. package/dist/integrations/pi/engine/conformance.d.ts +2 -0
  82. package/dist/integrations/pi/engine/conformance.js +2 -0
  83. package/dist/integrations/pi/engine/http-dispatcher.d.ts +4 -0
  84. package/dist/integrations/pi/engine/http-dispatcher.js +25 -0
  85. package/dist/integrations/pi/engine/index.d.ts +3 -0
  86. package/dist/integrations/pi/engine/index.js +3 -0
  87. package/dist/integrations/pi/engine/model-auth-integration.d.ts +1 -0
  88. package/dist/integrations/pi/engine/model-auth-integration.js +1 -0
  89. package/dist/integrations/pi/engine/package-integration.js +1 -1
  90. package/dist/integrations/pi/engine/project-trust-preflight.d.ts +22 -0
  91. package/dist/integrations/pi/engine/project-trust-preflight.js +50 -0
  92. package/dist/integrations/pi/engine/resource-extension-integration.d.ts +1 -0
  93. package/dist/integrations/pi/engine/resource-extension-integration.js +1 -0
  94. package/dist/integrations/pi/engine/runtime-integration.d.ts +16 -1
  95. package/dist/integrations/pi/engine/runtime-integration.js +34 -4
  96. package/dist/integrations/pi/engine/session-integration.d.ts +1 -0
  97. package/dist/integrations/pi/engine/session-integration.js +2 -1
  98. package/dist/integrations/pi/engine/settings-effects.d.ts +66 -0
  99. package/dist/integrations/pi/engine/settings-effects.js +249 -0
  100. package/dist/integrations/pi/engine/settings-integration.d.ts +14 -26
  101. package/dist/integrations/pi/engine/settings-integration.js +102 -108
  102. package/dist/integrations/pi/engine/workflow-controllers.d.ts +2 -1
  103. package/dist/integrations/pi/engine/workflow-controllers.js +1 -0
  104. package/dist/integrations/pi/session-ui/clipboard-image.d.ts +11 -0
  105. package/dist/integrations/pi/session-ui/clipboard-image.js +29 -0
  106. package/dist/integrations/pi/session-ui/index.d.ts +1 -0
  107. package/dist/integrations/pi/session-ui/index.js +1 -0
  108. package/dist/integrations/pi/session-ui/prompt-chips.js +6 -2
  109. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +39 -4
  110. package/dist/integrations/pi/session-ui/session-shell-root.js +215 -80
  111. package/dist/integrations/pi/session-ui/session-shell.d.ts +10 -1
  112. package/dist/integrations/pi/session-ui/session-shell.js +256 -45
  113. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  114. package/dist/integrations/pi/session-ui/session-viewport-controller.js +76 -11
  115. package/dist/integrations/pi/session-ui/stream-presentation-coalescer.d.ts +29 -0
  116. package/dist/integrations/pi/session-ui/stream-presentation-coalescer.js +79 -0
  117. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +9 -0
  118. package/dist/integrations/pi/session-ui/system-clipboard.js +38 -9
  119. package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
  120. package/dist/integrations/pi/tui-runtime/adapter.js +166 -8
  121. package/dist/integrations/pi/tui-runtime/contracts.d.ts +25 -0
  122. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +63 -0
  123. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +240 -0
  124. package/dist/integrations/pi/tui-runtime/index.d.ts +2 -0
  125. package/dist/integrations/pi/tui-runtime/index.js +2 -0
  126. package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.d.ts +39 -0
  127. package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.js +139 -0
  128. package/dist/native/darwin-arm64/manifest.json +1 -1
  129. package/dist/native/linux-x64/manifest.json +1 -1
  130. package/dist/native/win32-x64/manifest.json +2 -2
  131. package/dist/native/win32-x64/process-guardian.exe +0 -0
  132. package/dist/ui/apps/host.js +5 -9
  133. package/dist/ui/apps/registry.d.ts +1 -0
  134. package/dist/ui/apps/registry.js +1 -0
  135. package/dist/ui/components/dialog-panel.js +5 -3
  136. package/dist/ui/components/frame.d.ts +1 -0
  137. package/dist/ui/components/frame.js +1 -0
  138. package/dist/ui/components/index.d.ts +1 -0
  139. package/dist/ui/components/index.js +1 -0
  140. package/dist/ui/components/line-input.d.ts +1 -6
  141. package/dist/ui/components/line-input.js +4 -9
  142. package/dist/ui/components/list-view.d.ts +0 -6
  143. package/dist/ui/components/list-view.js +14 -7
  144. package/dist/ui/components/progress-status.d.ts +2 -0
  145. package/dist/ui/components/progress-status.js +4 -0
  146. package/dist/ui/components/scrollbar.js +1 -1
  147. package/dist/ui/components/shortcuts.d.ts +1 -0
  148. package/dist/ui/components/shortcuts.js +1 -0
  149. package/dist/ui/components/spans.d.ts +2 -5
  150. package/dist/ui/components/spans.js +28 -8
  151. package/dist/ui/components/surface.d.ts +0 -5
  152. package/dist/ui/components/surface.js +1 -5
  153. package/dist/ui/components/text-selection.d.ts +10 -1
  154. package/dist/ui/components/text-selection.js +96 -29
  155. package/dist/ui/components/text.js +3 -3
  156. package/dist/ui/components/transcript-viewport.d.ts +42 -0
  157. package/dist/ui/components/transcript-viewport.js +287 -39
  158. package/dist/ui/settings/sections.d.ts +6 -19
  159. package/dist/ui/settings/sections.js +17 -15
  160. package/dist/ui/settings/session.d.ts +8 -18
  161. package/dist/ui/settings/session.js +60 -52
  162. package/dist/ui/settings/store.d.ts +1 -0
  163. package/dist/ui/settings/store.js +2 -1
  164. package/docs/architecture/code-documentation.md +77 -0
  165. package/docs/architecture/project-structure.md +15 -1
  166. package/docs/architecture/ui-reference-provenance.md +23 -3
  167. package/docs/ci-release-runbook.md +98 -7
  168. package/docs/manual-owned-ui-checkpoint.md +28 -1
  169. package/docs/manual-terminal-colour-check.md +4 -3
  170. package/docs/repository-governance-live-acceptance.md +77 -0
  171. package/package.json +11 -3
@@ -1,10 +1,14 @@
1
+ import { PRODUCT_TEXT } from "../../../product-identity.js";
1
2
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../../ui/components/index.js";
2
3
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
3
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";
4
- import { PiTuiRuntimeAdapter, } from "../tui-runtime/index.js";
5
+ import { DamageAwareTerminalAdapter, PiTuiRuntimeAdapter, classifyPiTuiInput, } from "../tui-runtime/index.js";
6
+ import { canonicalizeClipboardImage } from "./clipboard-image.js";
7
+ import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
5
8
  import { preloadSystemClipboard, readSystemClipboardContent, writeSystemClipboardText, } from "./system-clipboard.js";
6
9
  import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
7
10
  export { OwnedUiSessionShellRoot } from "./session-shell-root.js";
11
+ /** Coordinates backend, owned presentation, and Pi TUI lifecycles for one interactive session. */
8
12
  export class OwnedUiSessionShell {
9
13
  backend;
10
14
  root;
@@ -24,8 +28,17 @@ export class OwnedUiSessionShell {
24
28
  #disposed = false;
25
29
  #pointerReporting = false;
26
30
  #customViewport;
31
+ #damageTerminal;
32
+ #streamPresentation;
27
33
  #removeViewportPreInput;
28
34
  #unsubscribeSettings;
35
+ #unbindPiSettings;
36
+ #unbindTerminalSettings;
37
+ #unbindShutdownSettings;
38
+ #terminalProgressEnabled = false;
39
+ #showImages = true;
40
+ #imageWidthCells = 80;
41
+ #fullscreenExitOutput = "transcript";
29
42
  #compactionQueue = [];
30
43
  #lastClearTime = 0;
31
44
  #lastEscapeTime = 0;
@@ -43,11 +56,19 @@ export class OwnedUiSessionShell {
43
56
  this.#resolveStopped = resolve;
44
57
  });
45
58
  let runtime;
59
+ let damageTerminal;
60
+ let streamPresentation;
46
61
  let pendingClipboardWrite = Promise.resolve();
47
62
  this.root = new OwnedUiSessionShellRoot(this.backend.view(), options.cwd, {
48
63
  getColumns: () => runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
49
64
  getRows: () => runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
50
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,
51
72
  onSubmit: text => { void this.submit(text); },
52
73
  onInterrupt: () => { void this.interrupt(); },
53
74
  onClear: () => { void this.clearOrExit(); },
@@ -73,9 +94,17 @@ export class OwnedUiSessionShell {
73
94
  await pendingClipboardWrite;
74
95
  if (options.clipboard === undefined)
75
96
  return readSystemClipboardContent();
76
- const image = await options.clipboard.readImage?.();
77
- if (image !== null && image !== undefined)
78
- return { kind: "image", ...image };
97
+ try {
98
+ const image = await options.clipboard.readImage?.();
99
+ if (image !== null && image !== undefined) {
100
+ const canonical = canonicalizeClipboardImage(image);
101
+ if (canonical !== null)
102
+ return { kind: "image", ...canonical };
103
+ }
104
+ }
105
+ catch {
106
+ // Compatibility: treat an unavailable or malformed image as text-capable clipboard input.
107
+ }
79
108
  const text = await options.clipboard.readText();
80
109
  return text === null ? null : { kind: "text", text };
81
110
  },
@@ -85,8 +114,10 @@ export class OwnedUiSessionShell {
85
114
  }, this.backend.agentDir, {
86
115
  getMessageRenderer: customType => this.backend.pinnedMessageRenderer(customType),
87
116
  getToolDefinition: toolName => this.backend.pinnedToolDefinition(toolName),
88
- }, options.sessionLayout);
89
- // Bare A1 owns a bounded viewport and therefore always runs on the alternate
117
+ }, options.sessionLayout, {
118
+ resolve: assetId => this.backend.resolveTranscriptImage(assetId),
119
+ });
120
+ // Invariant: bare A1 owns a bounded viewport and therefore always runs on the alternate
90
121
  // fullscreen surface. The pinned comparison profiles still honor Pi's mode.
91
122
  const tuiMode = this.#customViewport
92
123
  ? "fullscreen"
@@ -94,15 +125,72 @@ export class OwnedUiSessionShell {
94
125
  const runtimeOptions = {
95
126
  root: this.root,
96
127
  mode: tuiMode,
97
- ...(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
+ }),
98
152
  ...(options.terminal === undefined ? {} : { terminal: options.terminal }),
99
153
  hardwareCursor: this.backend.view().terminal.hardwareCursor,
100
154
  };
101
155
  runtime = new PiTuiRuntimeAdapter(runtimeOptions);
102
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;
163
+ const initialPiSettings = this.backend.pinnedSettingsSnapshot();
164
+ this.runtime.setHardwareCursor(initialPiSettings.showHardwareCursor);
165
+ this.runtime.setClearOnShrink(initialPiSettings.clearOnShrink);
166
+ this.#terminalProgressEnabled = initialPiSettings.showTerminalProgress;
167
+ this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
168
+ this.#unbindShutdownSettings = this.backend.bindSettingsOwner("shutdown", {
169
+ fullscreenExitOutput: { apply() { } },
170
+ });
171
+ this.#unbindTerminalSettings = this.backend.bindSettingsOwner("terminal", {
172
+ showHardwareCursor: { apply: value => {
173
+ if (typeof value !== "boolean")
174
+ throw new TypeError("Hardware cursor setting is invalid");
175
+ this.runtime.setHardwareCursor(value);
176
+ } },
177
+ clearOnShrink: { apply: value => {
178
+ if (typeof value !== "boolean")
179
+ throw new TypeError("Clear-on-shrink setting is invalid");
180
+ this.runtime.setClearOnShrink(value);
181
+ } },
182
+ showTerminalProgress: { apply: value => {
183
+ if (typeof value !== "boolean")
184
+ throw new TypeError("Terminal progress setting is invalid");
185
+ this.#terminalProgressEnabled = value;
186
+ this.#syncTerminalProgress(this.view());
187
+ } },
188
+ });
103
189
  this.#removeViewportPreInput = this.#customViewport
104
190
  ? this.runtime.addPreInputListener(data => {
105
- // 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
106
194
  // surface. Letting the transcript pre-router inspect those reports
107
195
  // steals settings value menus and numeric +/- controls before the
108
196
  // settings app can receive them.
@@ -129,6 +217,53 @@ export class OwnedUiSessionShell {
129
217
  this.#unsubscribeSettings = this.#customViewport && options.viewportSettings
130
218
  ? options.viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
131
219
  : () => { };
220
+ this.root.setEditorPaddingX(initialPiSettings.editorPaddingX);
221
+ this.root.setAutocompleteMaxVisible(initialPiSettings.autocompleteMaxVisible);
222
+ this.root.setOutputPad(initialPiSettings.outputPad);
223
+ this.root.setHideThinkingBlock(initialPiSettings.hideThinkingBlock);
224
+ this.root.setMermaidRenderingMode(initialPiSettings.mermaidRenderingMode);
225
+ this.#showImages = initialPiSettings.showImages;
226
+ this.#imageWidthCells = initialPiSettings.imageWidthCells;
227
+ this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
228
+ this.#unbindPiSettings = this.backend.bindSettingsOwner("shell", {
229
+ editorPaddingX: { apply: value => {
230
+ if (typeof value !== "number")
231
+ throw new TypeError("Editor padding is invalid");
232
+ this.root.setEditorPaddingX(value);
233
+ } },
234
+ autocompleteMaxVisible: { apply: value => {
235
+ if (typeof value !== "number")
236
+ throw new TypeError("Autocomplete maximum is invalid");
237
+ this.root.setAutocompleteMaxVisible(value);
238
+ } },
239
+ outputPad: { apply: value => {
240
+ if (value !== 0 && value !== 1)
241
+ throw new TypeError("Output padding is invalid");
242
+ this.root.setOutputPad(value);
243
+ } },
244
+ hideThinkingBlock: { apply: value => {
245
+ if (typeof value !== "boolean")
246
+ throw new TypeError("Thinking-block visibility is invalid");
247
+ this.root.setHideThinkingBlock(value);
248
+ } },
249
+ mermaidRenderingMode: { apply: value => {
250
+ if (value !== "off" && value !== "final" && value !== "streaming")
251
+ throw new TypeError("Mermaid mode is invalid");
252
+ this.root.setMermaidRenderingMode(value);
253
+ } },
254
+ showImages: { apply: value => {
255
+ if (typeof value !== "boolean")
256
+ throw new TypeError("Image visibility is invalid");
257
+ this.#showImages = value;
258
+ this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
259
+ } },
260
+ imageWidthCells: { apply: value => {
261
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
262
+ throw new TypeError("Image width is invalid");
263
+ this.#imageWidthCells = value;
264
+ this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
265
+ } },
266
+ });
132
267
  this.#extensionBridge = createPiExtensionUiBridge({
133
268
  runtime: {
134
269
  getColumns: () => this.runtime.viewport().columns,
@@ -136,7 +271,7 @@ export class OwnedUiSessionShell {
136
271
  requestRender: () => this.runtime.requestRender(),
137
272
  },
138
273
  agentDir: this.backend.agentDir,
139
- setInputSurface: component => this.root.setInputSurface(component),
274
+ setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
140
275
  showOverlay: (component, overlayOptions) => this.runtime.showOverlay(component, overlayOptions),
141
276
  listenInput: handler => this.runtime.addInputListener(handler),
142
277
  replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
@@ -150,7 +285,7 @@ export class OwnedUiSessionShell {
150
285
  setEditorText: text => this.root.editor.setText(text),
151
286
  pasteToEditor: text => this.root.editor.insertText(text),
152
287
  addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
153
- setCustomEditor: component => this.root.setInputSurface(component),
288
+ setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
154
289
  getFooterData: () => this.root.extensionFooterData(),
155
290
  getToolsExpanded: () => this.root.toolsExpanded,
156
291
  setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
@@ -163,7 +298,7 @@ export class OwnedUiSessionShell {
163
298
  });
164
299
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
165
300
  this.#unsubscribe = this.backend.onEvent(event => {
166
- // 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
167
302
  // cost of a chunk the same in a long session as in a new one. Everything else
168
303
  // resynchronizes the view, which is cheap next to re-reading the transcript.
169
304
  if (event.type === "agent-run-started")
@@ -174,6 +309,7 @@ export class OwnedUiSessionShell {
174
309
  const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.sessionGeneration
175
310
  ? this.#syncBlock(event.block)
176
311
  : semanticOnly ? this.view() : this.#syncView();
312
+ this.#syncTerminalProgress(view);
177
313
  if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
178
314
  void this.#flushCompactionQueue();
179
315
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
@@ -185,11 +321,15 @@ export class OwnedUiSessionShell {
185
321
  view() {
186
322
  return this.backend.view();
187
323
  }
324
+ damagePresentationDecision() {
325
+ return this.#damageTerminal?.lastDecision ?? null;
326
+ }
188
327
  start() {
189
328
  if (this.#started)
190
329
  return;
191
330
  this.#started = true;
192
331
  this.runtime.start();
332
+ this.#syncTerminalProgress(this.view());
193
333
  if (this.#customViewport)
194
334
  this.#setPointerReporting(true);
195
335
  void this.backend.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
@@ -238,7 +378,11 @@ export class OwnedUiSessionShell {
238
378
  }
239
379
  if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
240
380
  this.root.editor.addToHistory(displayInput);
241
- this.#compactionQueue.push({ text: input, type: "steer" });
381
+ this.#compactionQueue.push({
382
+ text: input,
383
+ type: "steer",
384
+ ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
385
+ });
242
386
  this.root.appendWorkflowResult({ command: "compact", outcome: "completed", message: `Queued during compaction: ${input}` });
243
387
  this.runtime.requestRender();
244
388
  return { outcome: "completed", diagnostic: null };
@@ -269,7 +413,7 @@ export class OwnedUiSessionShell {
269
413
  this.root.editor.setText("");
270
414
  return { outcome: "completed", diagnostic: null };
271
415
  }
272
- // 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
273
417
  // configured session navigator. The first escape intentionally does not
274
418
  // interrupt or mutate the prompt.
275
419
  const action = this.backend.pinnedSettingsSnapshot().doubleEscapeAction;
@@ -336,14 +480,20 @@ export class OwnedUiSessionShell {
336
480
  return workflowAdapterResult(result);
337
481
  }
338
482
  async queueFollowUp() {
339
- const text = this.root.editor.getText().trim();
340
- if (!text)
483
+ const displayInput = this.root.editor.getText().trim();
484
+ if (!displayInput)
341
485
  return rejected("nothing to queue");
342
- this.root.editor.addToHistory(text);
486
+ const prepared = this.root.preparePromptSubmission(displayInput);
487
+ const text = prepared.text.trim();
488
+ this.root.editor.addToHistory(displayInput);
343
489
  this.root.editor.setText("");
344
490
  this.root.resumeViewportFollowing();
345
491
  if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
346
- this.#compactionQueue.push({ text, type: "follow-up" });
492
+ this.#compactionQueue.push({
493
+ text,
494
+ type: "follow-up",
495
+ ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
496
+ });
347
497
  return { outcome: "completed", diagnostic: null };
348
498
  }
349
499
  return this.#execute({
@@ -351,6 +501,7 @@ export class OwnedUiSessionShell {
351
501
  correlationId: this.#correlation("follow-up"),
352
502
  sessionId: this.backend.sessionId,
353
503
  text,
504
+ ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
354
505
  });
355
506
  }
356
507
  restoreQueuedInput() {
@@ -381,7 +532,12 @@ export class OwnedUiSessionShell {
381
532
  onCancel?.();
382
533
  },
383
534
  });
384
- 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
+ });
385
541
  this.#dialogHandle = handle;
386
542
  return handle;
387
543
  }
@@ -581,7 +737,7 @@ export class OwnedUiSessionShell {
581
737
  return;
582
738
  }
583
739
  if (callback === "onTuiModeChange") {
584
- // The custom bare-A1 surface is permanently fullscreen; this callback
740
+ // Invariant: the custom bare-A1 surface is permanently fullscreen; this callback
585
741
  // remains available only to pinned comparison profiles.
586
742
  if (this.#customViewport)
587
743
  return;
@@ -597,12 +753,13 @@ export class OwnedUiSessionShell {
597
753
  this.#setPointerReporting(true);
598
754
  }
599
755
  }
600
- const result = this.backend.applyPinnedSettingValue(callback, value);
601
- if (result.outcome === "failed")
602
- this.root.appendWorkflowResult(result);
603
- else if (callback === "onTuiModeChange")
604
- this.root.appendWorkflowStatus(`TUI mode: ${value}`);
605
- this.runtime.requestRender();
756
+ void this.backend.applyPinnedSettingValue(callback, value).then(result => {
757
+ if (result.outcome === "failed")
758
+ this.root.appendWorkflowResult(result);
759
+ else if (callback === "onTuiModeChange")
760
+ this.root.appendWorkflowStatus(`TUI mode: ${value}`);
761
+ this.runtime.requestRender();
762
+ });
606
763
  },
607
764
  onCancel: close,
608
765
  });
@@ -801,12 +958,7 @@ export class OwnedUiSessionShell {
801
958
  this.root.appendDaxnuts();
802
959
  }
803
960
  }
804
- /**
805
- * Turns terminal pointer reporting on for a screen that reads the pointer, and off for
806
- * every path that ends it. While it is on the terminal hands A1 the wheel and the
807
- * button instead of scrolling and selecting itself, so leaving it on outlives the
808
- * screen that wanted it and takes the terminal's own scrolling and selection with it.
809
- */
961
+ // Invariant: pointer reporting is disabled on every path that ends the owning screen.
810
962
  #setPointerReporting(enabled, forceOff = false) {
811
963
  const effective = forceOff ? false : this.#customViewport || enabled;
812
964
  if (this.#pointerReporting === effective)
@@ -823,32 +975,62 @@ export class OwnedUiSessionShell {
823
975
  this.root.clearViewportPointerState();
824
976
  this.#setPointerReporting(false, true);
825
977
  this.#removeViewportPreInput();
978
+ this.#streamPresentation.dispose();
826
979
  this.#unsubscribeSettings();
980
+ const exitMode = this.backend.disposed
981
+ ? this.#fullscreenExitOutput
982
+ : this.backend.pinnedSettingsSnapshot().fullscreenExitOutput;
983
+ const exitTranscript = this.root.exitTranscript(this.runtime.viewport().columns);
984
+ const resume = this.backend.currentSessionResumeMetadata();
985
+ const resumeHint = resume === null
986
+ ? ""
987
+ : `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
988
+ const fullscreenExitText = this.runtime.mode !== "fullscreen"
989
+ ? ""
990
+ : exitMode === "resume-hint"
991
+ ? resumeHint
992
+ : [exitTranscript, resumeHint].filter(Boolean).join("\n\n");
993
+ this.#unbindPiSettings();
994
+ this.#unbindTerminalSettings();
995
+ this.#unbindShutdownSettings();
827
996
  this.#unsubscribe();
828
997
  this.#dialogHandle?.hide();
829
998
  await this.backend.unbindExtensionUi();
830
999
  this.#extensionBridge.dispose();
831
1000
  await this.runtime.dispose();
1001
+ if (fullscreenExitText.length > 0)
1002
+ this.runtime.writeAfterStop(`${fullscreenExitText}\n`);
832
1003
  }
833
- /**
834
- * Applies one transcript block without re-reading the session. The listeners still hear
835
- * the view they would have heard, so nothing downstream can tell the difference.
836
- */
1004
+ // Invariant: incremental block updates notify listeners with the same complete view contract.
837
1005
  #syncBlock(block) {
838
1006
  this.root.applyTranscriptBlock(block);
839
- 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
+ }
840
1016
  const view = this.view();
841
1017
  for (const listener of this.#listeners)
842
1018
  listener(view);
843
1019
  return view;
844
1020
  }
1021
+ #syncTerminalProgress(view) {
1022
+ if (!this.runtime.active)
1023
+ return;
1024
+ this.runtime.setTerminalProgress(this.#terminalProgressEnabled && view.lifecycle === "busy");
1025
+ }
845
1026
  #syncView() {
1027
+ this.#streamPresentation.noteImmediatePresentation();
846
1028
  const view = this.view();
847
1029
  if (this.backend.sessionGeneration !== this.#sessionGeneration) {
848
1030
  this.#sessionGeneration = this.backend.sessionGeneration;
849
1031
  this.#activeLoginDialog = undefined;
850
1032
  this.#extensionBridge.reset();
851
- // 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.
852
1034
  this.root.resetViewport();
853
1035
  this.#dialogHandle?.hide();
854
1036
  this.#dialogHandle = undefined;
@@ -874,10 +1056,10 @@ export class OwnedUiSessionShell {
874
1056
  return { outcome: "failed", diagnostic: "runtime is not active" };
875
1057
  this.#dialogHandle?.hide();
876
1058
  this.#dialogSource = "route";
877
- // 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
878
1060
  // it also stops the terminal treating a drag as a text selection.
879
1061
  this.#setPointerReporting(true);
880
- // 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
881
1063
  // through the overlay: the pinned shell handles that key before an overlay
882
1064
  // ever sees it, which is why an owned screen must not rely on being asked.
883
1065
  let armedAt = 0;
@@ -893,7 +1075,7 @@ export class OwnedUiSessionShell {
893
1075
  }
894
1076
  armedAt = now;
895
1077
  this.runtime.requestRender();
896
- // 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
897
1079
  // stray interrupt while it is up.
898
1080
  return { consume: true };
899
1081
  });
@@ -907,7 +1089,7 @@ export class OwnedUiSessionShell {
907
1089
  this.#dialogId = undefined;
908
1090
  this.#dialogSource = undefined;
909
1091
  };
910
- // Fullscreen Pi owns a fallback text-selection layer before focused overlay
1092
+ // Compatibility: fullscreen Pi owns a fallback text-selection layer before focused overlay
911
1093
  // components see pointer input. Route every mouse report to the owned screen
912
1094
  // at the pre-input boundary so dropdowns, value hover, and numeric +/- work,
913
1095
  // and consume even unhandled reports so settings content is never selected.
@@ -945,7 +1127,12 @@ export class OwnedUiSessionShell {
945
1127
  closeSurface();
946
1128
  void this.shutdown();
947
1129
  });
948
- 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
+ });
949
1136
  this.#dialogId = surface.id;
950
1137
  return { outcome: "completed", diagnostic: null };
951
1138
  }
@@ -953,7 +1140,7 @@ export class OwnedUiSessionShell {
953
1140
  if (!this.runtime.active)
954
1141
  return;
955
1142
  if (dialog === null) {
956
- // Locally owned routes (notably /settings) are independent of backend
1143
+ // Invariant: locally owned routes (notably /settings) are independent of backend
957
1144
  // lifecycle/status events and remain open while an agent is working.
958
1145
  if (this.#dialogSource !== "backend")
959
1146
  return;
@@ -981,7 +1168,12 @@ export class OwnedUiSessionShell {
981
1168
  this.#dialogSource = undefined;
982
1169
  },
983
1170
  });
984
- 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
+ });
985
1177
  this.#dialogId = dialog.id;
986
1178
  }
987
1179
  async #slashCommand(text) {
@@ -993,7 +1185,7 @@ export class OwnedUiSessionShell {
993
1185
  return this.#openOwnedRoute(name);
994
1186
  if (isWorkflowRoute(name))
995
1187
  return this.runWorkflow({ command: name, argument });
996
- // 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.
997
1189
  this.root.editor.addToHistory(text);
998
1190
  this.root.resumeViewportFollowing();
999
1191
  return this.#execute({
@@ -1117,6 +1309,7 @@ export class OwnedUiSessionShell {
1117
1309
  correlationId: this.#correlation(`compaction-${item.type}`),
1118
1310
  sessionId: this.backend.sessionId,
1119
1311
  text: item.text,
1312
+ ...(item.images === undefined ? {} : { images: item.images }),
1120
1313
  });
1121
1314
  }
1122
1315
  }
@@ -1131,6 +1324,24 @@ export class OwnedUiSessionShell {
1131
1324
  return `pi-shell-${prefix}-${this.#sequence}`;
1132
1325
  }
1133
1326
  }
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(" ");
1336
+ }
1337
+ export function quoteCommandArgument(value) {
1338
+ if (value.length > 0 && !/[^a-zA-Z0-9_\-./~:@]/.test(value))
1339
+ return value;
1340
+ return `'${value.replaceAll("'", `'\\''`)}'`;
1341
+ }
1342
+ function dim(value) {
1343
+ return `\u001b[2m${value}\u001b[22m`;
1344
+ }
1134
1345
  function modelReference(model) {
1135
1346
  if (typeof model !== "object" || model === null)
1136
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;