@timurproko/a1 0.1.8-dev.137 → 0.1.8-dev.151

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 (105) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/capabilities.d.ts +2 -11
  3. package/dist/cli/capabilities.js +1 -1
  4. package/dist/cli/dispatch.js +3 -6
  5. package/dist/composition/owned-ui.js +26 -5
  6. package/dist/composition/settings-route-host.d.ts +1 -1
  7. package/dist/composition/settings-route-host.js +1 -1
  8. package/dist/contracts/owned-ui/model.d.ts +28 -0
  9. package/dist/contracts/owned-ui/validation.js +14 -0
  10. package/dist/features/launch/development-launch.js +11 -3
  11. package/dist/features/launch/index.d.ts +0 -1
  12. package/dist/features/launch/index.js +0 -1
  13. package/dist/features/launch/intent.d.ts +3 -2
  14. package/dist/features/launch/intent.js +1 -2
  15. package/dist/features/launch/prepare-launch.d.ts +0 -2
  16. package/dist/features/launch/prepare-launch.js +2 -9
  17. package/dist/features/launch/profile-paths.d.ts +1 -2
  18. package/dist/features/launch/profile-paths.js +2 -8
  19. package/dist/features/launch/runtime-selection.d.ts +2 -5
  20. package/dist/features/owned-ui/settings-app.js +38 -14
  21. package/dist/foundation/lifecycle/model.d.ts +1 -1
  22. package/dist/foundation/lifecycle/model.js +1 -1
  23. package/dist/foundation/release/bootstrap.d.ts +1 -3
  24. package/dist/foundation/release/bootstrap.js +1 -1
  25. package/dist/foundation/storage/control-store.js +37 -3
  26. package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
  27. package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
  28. package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
  29. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  30. package/dist/integrations/pi/components/shell-footer-status.js +14 -5
  31. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -2
  32. package/dist/integrations/pi/components/shell-presenters-transcript.js +9 -8
  33. package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
  34. package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
  35. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +13 -0
  36. package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
  37. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
  38. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
  39. package/dist/integrations/pi/engine/adapter.js +21 -3
  40. package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
  41. package/dist/integrations/pi/engine/session-integration.js +17 -3
  42. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
  43. package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
  44. package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
  45. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +34 -5
  46. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +294 -45
  47. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
  48. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +167 -24
  49. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
  50. package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
  51. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
  52. package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
  53. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
  54. package/dist/integrations/pi/tui-runtime/adapter.js +51 -2
  55. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -6
  56. package/dist/native/darwin-arm64/manifest.json +1 -1
  57. package/dist/native/linux-x64/manifest.json +1 -1
  58. package/dist/native/win32-x64/manifest.json +3 -3
  59. package/dist/native/win32-x64/process-guardian.exe +0 -0
  60. package/dist/product-identity.d.ts +2 -2
  61. package/dist/product-identity.js +3 -4
  62. package/dist/product-identity.json +3 -5
  63. package/dist/ui/apps/contracts.d.ts +23 -1
  64. package/dist/ui/apps/host.js +9 -1
  65. package/dist/ui/components/index.d.ts +3 -0
  66. package/dist/ui/components/index.js +3 -0
  67. package/dist/ui/components/mouse.d.ts +10 -0
  68. package/dist/ui/components/mouse.js +23 -0
  69. package/dist/ui/components/prompt-row.d.ts +14 -0
  70. package/dist/ui/components/prompt-row.js +20 -0
  71. package/dist/ui/components/scrollbar.d.ts +26 -0
  72. package/dist/ui/components/scrollbar.js +35 -0
  73. package/dist/ui/components/spans.d.ts +21 -0
  74. package/dist/ui/components/spans.js +178 -0
  75. package/dist/ui/components/submitted-prompt.d.ts +23 -0
  76. package/dist/ui/components/submitted-prompt.js +50 -0
  77. package/dist/ui/components/text-selection.d.ts +53 -0
  78. package/dist/ui/components/text-selection.js +168 -0
  79. package/dist/ui/components/text.d.ts +14 -0
  80. package/dist/ui/components/text.js +40 -0
  81. package/dist/ui/components/transcript-viewport.d.ts +104 -0
  82. package/dist/ui/components/transcript-viewport.js +381 -0
  83. package/dist/ui/settings/declarations.d.ts +8 -1
  84. package/dist/ui/settings/declarations.js +33 -5
  85. package/dist/ui/settings/migrations.d.ts +1 -1
  86. package/dist/ui/settings/migrations.js +21 -2
  87. package/dist/ui/settings/sections.js +15 -8
  88. package/dist/ui/settings/session.d.ts +2 -0
  89. package/dist/ui/settings/session.js +7 -5
  90. package/docs/architecture/boundaries.md +3 -3
  91. package/docs/architecture/project-structure.md +2 -2
  92. package/docs/architecture/resource-and-data-policy.md +1 -1
  93. package/docs/architecture/toolchain.md +5 -5
  94. package/docs/architecture/ui-reference-provenance.md +11 -2
  95. package/docs/features/launch-profiles.md +7 -14
  96. package/docs/manual-launch-instance-acceptance.md +12 -35
  97. package/docs/manual-owned-ui-checkpoint.md +2 -3
  98. package/docs/manual-pi-boundary-candidate.md +4 -5
  99. package/package.json +4 -2
  100. package/dist/features/launch/profiles.d.ts +0 -12
  101. package/dist/features/launch/profiles.js +0 -26
  102. package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
  103. /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
  104. /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
  105. /package/dist/integrations/pi/{owned-ui → session-ui}/route-host.js +0 -0
@@ -2,6 +2,7 @@ import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../..
2
2
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
3
3
  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
4
  import { PiTuiRuntimeAdapter, } from "../tui-runtime/index.js";
5
+ import { preloadSystemClipboard, readSystemClipboardContent, writeSystemClipboardText, } from "./system-clipboard.js";
5
6
  import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
6
7
  export { OwnedUiSessionShellRoot } from "./session-shell-root.js";
7
8
  export class OwnedUiSessionShell {
@@ -15,14 +16,19 @@ export class OwnedUiSessionShell {
15
16
  #stopped;
16
17
  #resolveStopped;
17
18
  #dialogId;
19
+ #dialogSource;
18
20
  #routeHost;
19
21
  #dialogHandle;
20
22
  #sequence = 0;
21
23
  #started = false;
22
24
  #disposed = false;
23
25
  #pointerReporting = false;
26
+ #customViewport;
27
+ #removeViewportPreInput;
28
+ #unsubscribeSettings;
24
29
  #compactionQueue = [];
25
30
  #lastClearTime = 0;
31
+ #lastEscapeTime = 0;
26
32
  #activeLoginDialog;
27
33
  #sessionGeneration;
28
34
  constructor(options) {
@@ -30,14 +36,18 @@ export class OwnedUiSessionShell {
30
36
  this.#sessionGeneration = this.backend.sessionGeneration;
31
37
  this.#cwd = options.cwd;
32
38
  this.#routeHost = options.routeHost ?? null;
39
+ this.#customViewport = options.sessionLayout === "custom-viewport";
40
+ if (this.#customViewport && options.clipboard === undefined)
41
+ preloadSystemClipboard();
33
42
  this.#stopped = new Promise(resolve => {
34
43
  this.#resolveStopped = resolve;
35
44
  });
36
45
  let runtime;
46
+ let pendingClipboardWrite = Promise.resolve();
37
47
  this.root = new OwnedUiSessionShellRoot(this.backend.view(), options.cwd, {
38
48
  getColumns: () => runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
39
49
  getRows: () => runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
40
- requestRender: () => runtime?.requestRender(),
50
+ requestRender: force => runtime?.requestRender(force),
41
51
  onSubmit: text => { void this.submit(text); },
42
52
  onInterrupt: () => { void this.interrupt(); },
43
53
  onClear: () => { void this.clearOrExit(); },
@@ -52,19 +62,73 @@ export class OwnedUiSessionShell {
52
62
  onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
53
63
  onFollowUp: () => { void this.queueFollowUp(); },
54
64
  onDequeue: () => this.restoreQueuedInput(),
65
+ onCopyText: text => {
66
+ runtime?.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
67
+ const write = options.clipboard === undefined
68
+ ? writeSystemClipboardText(text)
69
+ : options.clipboard.writeText?.(text) ?? Promise.resolve();
70
+ pendingClipboardWrite = write.catch(() => { });
71
+ },
72
+ readClipboardContent: async () => {
73
+ await pendingClipboardWrite;
74
+ if (options.clipboard === undefined)
75
+ return readSystemClipboardContent();
76
+ const image = await options.clipboard.readImage?.();
77
+ if (image !== null && image !== undefined)
78
+ return { kind: "image", ...image };
79
+ const text = await options.clipboard.readText();
80
+ return text === null ? null : { kind: "text", text };
81
+ },
55
82
  }, {
56
83
  ...options.startup,
57
84
  resources: options.startup?.resources ?? shellResourceEntries(this.backend),
58
85
  }, this.backend.agentDir, {
59
86
  getMessageRenderer: customType => this.backend.pinnedMessageRenderer(customType),
60
87
  getToolDefinition: toolName => this.backend.pinnedToolDefinition(toolName),
61
- });
62
- const tuiMode = this.backend.disposed ? "regular" : this.backend.pinnedSettingsSnapshot().tuiMode;
63
- const runtimeOptions = options.terminal === undefined
64
- ? { root: this.root, mode: tuiMode, layoutRoot: this.root.layoutRoot(), hardwareCursor: this.backend.view().terminal.hardwareCursor }
65
- : { root: this.root, mode: tuiMode, layoutRoot: this.root.layoutRoot(), terminal: options.terminal, hardwareCursor: this.backend.view().terminal.hardwareCursor };
88
+ }, options.sessionLayout);
89
+ // Bare A1 owns a bounded viewport and therefore always runs on the alternate
90
+ // fullscreen surface. The pinned comparison profiles still honor Pi's mode.
91
+ const tuiMode = this.#customViewport
92
+ ? "fullscreen"
93
+ : this.backend.disposed ? "regular" : this.backend.pinnedSettingsSnapshot().tuiMode;
94
+ const runtimeOptions = {
95
+ root: this.root,
96
+ mode: tuiMode,
97
+ ...(this.#customViewport ? {} : { layoutRoot: this.root.layoutRoot() }),
98
+ ...(options.terminal === undefined ? {} : { terminal: options.terminal }),
99
+ hardwareCursor: this.backend.view().terminal.hardwareCursor,
100
+ };
66
101
  runtime = new PiTuiRuntimeAdapter(runtimeOptions);
67
102
  this.runtime = runtime;
103
+ this.#removeViewportPreInput = this.#customViewport
104
+ ? this.runtime.addPreInputListener(data => {
105
+ // An overlay or editor-replacement screen owns its entire pointer
106
+ // surface. Letting the transcript pre-router inspect those reports
107
+ // steals settings value menus and numeric +/- controls before the
108
+ // settings app can receive them.
109
+ if (this.runtime.hasOverlay() || !this.root.usesDefaultInputSurface())
110
+ return undefined;
111
+ const routed = this.root.handleViewportPreInput(data, true);
112
+ if (routed.copyText !== undefined) {
113
+ this.runtime.writeControl(`\u001b]52;c;${Buffer.from(routed.copyText, "utf8").toString("base64")}\u0007`);
114
+ }
115
+ if (!routed.consumed)
116
+ return routed.data === data ? undefined : { data: routed.data };
117
+ return routed.data.length === 0 ? { consume: true } : { data: routed.data };
118
+ })
119
+ : () => { };
120
+ const applyViewportSettings = () => {
121
+ const snapshot = options.viewportSettings?.snapshot();
122
+ this.root.setViewportConfig(snapshot ?? {
123
+ scrollbarAppearance: "auto",
124
+ scrollbarStyle: "thin",
125
+ scrollbarSpeed: "normal",
126
+ });
127
+ };
128
+ applyViewportSettings();
129
+ this.#unsubscribeSettings = this.#customViewport && options.viewportSettings
130
+ ? options.viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
131
+ : () => { };
68
132
  this.#extensionBridge = createPiExtensionUiBridge({
69
133
  runtime: {
70
134
  getColumns: () => this.runtime.viewport().columns,
@@ -102,9 +166,14 @@ export class OwnedUiSessionShell {
102
166
  // A streamed chunk names one block, and touching only that block is what keeps the
103
167
  // cost of a chunk the same in a long session as in a new one. Everything else
104
168
  // resynchronizes the view, which is cheap next to re-reading the transcript.
169
+ if (event.type === "agent-run-started")
170
+ this.root.resumeViewportFollowing();
171
+ if (event.type === "assistant-message-completed")
172
+ this.root.noteCompletedAssistantMessage();
173
+ const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
105
174
  const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.sessionGeneration
106
175
  ? this.#syncBlock(event.block)
107
- : this.#syncView();
176
+ : semanticOnly ? this.view() : this.#syncView();
108
177
  if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
109
178
  void this.#flushCompactionQueue();
110
179
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
@@ -121,6 +190,8 @@ export class OwnedUiSessionShell {
121
190
  return;
122
191
  this.#started = true;
123
192
  this.runtime.start();
193
+ if (this.#customViewport)
194
+ this.#setPointerReporting(true);
124
195
  void this.backend.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
125
196
  this.#syncView();
126
197
  }
@@ -133,16 +204,18 @@ export class OwnedUiSessionShell {
133
204
  return this.#stopped;
134
205
  }
135
206
  async submit(text) {
136
- const input = text.trim();
137
- if (!input)
207
+ const displayInput = text.trim();
208
+ if (!displayInput)
138
209
  return { outcome: "completed", diagnostic: null };
139
- if (input.startsWith("/"))
140
- return this.#slashCommand(input);
210
+ if (displayInput.startsWith("/"))
211
+ return this.#slashCommand(displayInput);
212
+ const prepared = this.root.preparePromptSubmission(displayInput);
213
+ const input = prepared.text.trim();
141
214
  if (input.startsWith("!")) {
142
215
  const excludeFromContext = input.startsWith("!!");
143
216
  const command = input.slice(excludeFromContext ? 2 : 1).trim();
144
217
  if (command) {
145
- this.root.editor.addToHistory(input);
218
+ this.root.editor.addToHistory(displayInput);
146
219
  try {
147
220
  const result = await this.backend.executeBashWorkflow(command, excludeFromContext);
148
221
  const workflow = {
@@ -164,19 +237,21 @@ export class OwnedUiSessionShell {
164
237
  }
165
238
  }
166
239
  if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
167
- this.root.editor.addToHistory(input);
240
+ this.root.editor.addToHistory(displayInput);
168
241
  this.#compactionQueue.push({ text: input, type: "steer" });
169
242
  this.root.appendWorkflowResult({ command: "compact", outcome: "completed", message: `Queued during compaction: ${input}` });
170
243
  this.runtime.requestRender();
171
244
  return { outcome: "completed", diagnostic: null };
172
245
  }
173
246
  const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
174
- this.root.editor.addToHistory(input);
247
+ this.root.editor.addToHistory(displayInput);
248
+ this.root.resumeViewportFollowing();
175
249
  return this.#execute({
176
250
  type,
177
251
  correlationId: this.#correlation(type),
178
252
  sessionId: this.backend.sessionId,
179
253
  text: input,
254
+ ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
180
255
  });
181
256
  }
182
257
  async clearOrExit(now = Date.now()) {
@@ -187,14 +262,29 @@ export class OwnedUiSessionShell {
187
262
  this.runtime.requestRender();
188
263
  return { outcome: "completed", diagnostic: null };
189
264
  }
190
- async interrupt() {
265
+ async interrupt(now = Date.now()) {
191
266
  if (this.view().lifecycle === "busy")
192
267
  return this.abort();
193
- if (this.root.editor.getText().length > 0) {
268
+ if (this.root.editor.getText().trim().length > 0) {
194
269
  this.root.editor.setText("");
195
270
  return { outcome: "completed", diagnostic: null };
196
271
  }
197
- return { outcome: "rejected", diagnostic: "nothing to interrupt" };
272
+ // Match Pi: with an empty editor, two escapes inside 500 ms open the
273
+ // configured session navigator. The first escape intentionally does not
274
+ // interrupt or mutate the prompt.
275
+ const action = this.backend.pinnedSettingsSnapshot().doubleEscapeAction;
276
+ if (action === "none")
277
+ return { outcome: "rejected", diagnostic: "nothing to interrupt" };
278
+ if (now - this.#lastEscapeTime < 500) {
279
+ this.#lastEscapeTime = 0;
280
+ if (action === "tree")
281
+ this.showTreeSelector();
282
+ else
283
+ this.showForkSelector();
284
+ return { outcome: "completed", diagnostic: null };
285
+ }
286
+ this.#lastEscapeTime = now;
287
+ return { outcome: "completed", diagnostic: null };
198
288
  }
199
289
  async abort() {
200
290
  return this.#execute(this.#simple("abort"));
@@ -251,6 +341,7 @@ export class OwnedUiSessionShell {
251
341
  return rejected("nothing to queue");
252
342
  this.root.editor.addToHistory(text);
253
343
  this.root.editor.setText("");
344
+ this.root.resumeViewportFollowing();
254
345
  if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
255
346
  this.#compactionQueue.push({ text, type: "follow-up" });
256
347
  return { outcome: "completed", diagnostic: null };
@@ -272,6 +363,8 @@ export class OwnedUiSessionShell {
272
363
  }
273
364
  showSelector(title, options, onSelect, onCancel) {
274
365
  this.#dialogHandle?.hide();
366
+ this.#dialogSource = "local";
367
+ this.#dialogId = undefined;
275
368
  const component = createPiShellSelector({
276
369
  title,
277
370
  options,
@@ -279,10 +372,12 @@ export class OwnedUiSessionShell {
279
372
  onSelect(id);
280
373
  this.#dialogHandle?.hide();
281
374
  this.#dialogHandle = undefined;
375
+ this.#dialogSource = undefined;
282
376
  },
283
377
  onCancel: () => {
284
378
  this.#dialogHandle?.hide();
285
379
  this.#dialogHandle = undefined;
380
+ this.#dialogSource = undefined;
286
381
  onCancel?.();
287
382
  },
288
383
  });
@@ -486,6 +581,10 @@ export class OwnedUiSessionShell {
486
581
  return;
487
582
  }
488
583
  if (callback === "onTuiModeChange") {
584
+ // The custom bare-A1 surface is permanently fullscreen; this callback
585
+ // remains available only to pinned comparison profiles.
586
+ if (this.#customViewport)
587
+ return;
489
588
  if (value !== "regular" && value !== "fullscreen")
490
589
  return;
491
590
  if (!this.runtime.switchMode(value)) {
@@ -493,6 +592,10 @@ export class OwnedUiSessionShell {
493
592
  this.runtime.requestRender();
494
593
  return;
495
594
  }
595
+ if (this.#customViewport) {
596
+ this.#pointerReporting = false;
597
+ this.#setPointerReporting(true);
598
+ }
496
599
  }
497
600
  const result = this.backend.applyPinnedSettingValue(callback, value);
498
601
  if (result.outcome === "failed")
@@ -704,19 +807,23 @@ export class OwnedUiSessionShell {
704
807
  * button instead of scrolling and selecting itself, so leaving it on outlives the
705
808
  * screen that wanted it and takes the terminal's own scrolling and selection with it.
706
809
  */
707
- #setPointerReporting(enabled) {
708
- if (this.#pointerReporting === enabled)
810
+ #setPointerReporting(enabled, forceOff = false) {
811
+ const effective = forceOff ? false : this.#customViewport || enabled;
812
+ if (this.#pointerReporting === effective)
709
813
  return;
710
- this.#pointerReporting = enabled;
814
+ this.#pointerReporting = effective;
711
815
  if (!this.runtime.active)
712
816
  return;
713
- this.runtime.writeControl(enabled ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
817
+ this.runtime.writeControl(effective ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
714
818
  }
715
819
  async dispose() {
716
820
  if (this.#disposed)
717
821
  return;
718
822
  this.#disposed = true;
719
- this.#setPointerReporting(false);
823
+ this.root.clearViewportPointerState();
824
+ this.#setPointerReporting(false, true);
825
+ this.#removeViewportPreInput();
826
+ this.#unsubscribeSettings();
720
827
  this.#unsubscribe();
721
828
  this.#dialogHandle?.hide();
722
829
  await this.backend.unbindExtensionUi();
@@ -741,7 +848,12 @@ export class OwnedUiSessionShell {
741
848
  this.#sessionGeneration = this.backend.sessionGeneration;
742
849
  this.#activeLoginDialog = undefined;
743
850
  this.#extensionBridge.reset();
744
- // A replaced session takes its screens with it, pointer reporting included.
851
+ // A replaced session takes its transient viewport and owned-route state with it.
852
+ this.root.resetViewport();
853
+ this.#dialogHandle?.hide();
854
+ this.#dialogHandle = undefined;
855
+ this.#dialogId = undefined;
856
+ this.#dialogSource = undefined;
745
857
  this.#setPointerReporting(false);
746
858
  this.root.setInputSurface(null);
747
859
  this.root.resetExtensionUi();
@@ -761,6 +873,7 @@ export class OwnedUiSessionShell {
761
873
  if (!this.runtime.active)
762
874
  return { outcome: "failed", diagnostic: "runtime is not active" };
763
875
  this.#dialogHandle?.hide();
876
+ this.#dialogSource = "route";
764
877
  // Any-event reporting: hover and drag are what the screen is driven by, and
765
878
  // it also stops the terminal treating a drag as a text selection.
766
879
  this.#setPointerReporting(true);
@@ -784,13 +897,32 @@ export class OwnedUiSessionShell {
784
897
  // stray interrupt while it is up.
785
898
  return { consume: true };
786
899
  });
900
+ let removeSurfacePreInput = () => { };
787
901
  const closeSurface = () => {
902
+ removeSurfacePreInput();
788
903
  removeInterruptWatch();
789
904
  this.#setPointerReporting(false);
790
905
  this.#dialogHandle?.hide();
791
906
  this.#dialogHandle = undefined;
792
907
  this.#dialogId = undefined;
908
+ this.#dialogSource = undefined;
793
909
  };
910
+ // Fullscreen Pi owns a fallback text-selection layer before focused overlay
911
+ // components see pointer input. Route every mouse report to the owned screen
912
+ // at the pre-input boundary so dropdowns, value hover, and numeric +/- work,
913
+ // and consume even unhandled reports so settings content is never selected.
914
+ removeSurfacePreInput = this.runtime.addPreInputListener(data => {
915
+ const { events, rest } = parseMouseInput(data);
916
+ if (events.length === 0)
917
+ return undefined;
918
+ for (const event of events)
919
+ surface.handleMouse(event);
920
+ if (surface.isClosed())
921
+ closeSurface();
922
+ else
923
+ this.runtime.requestRender();
924
+ return rest.length === 0 ? { consume: true } : { data: rest };
925
+ });
794
926
  const rows = () => Math.max(1, this.runtime.viewport().rows);
795
927
  const component = {
796
928
  render: (width) => [...surface.render(Math.max(1, width), rows())],
@@ -821,22 +953,32 @@ export class OwnedUiSessionShell {
821
953
  if (!this.runtime.active)
822
954
  return;
823
955
  if (dialog === null) {
956
+ // Locally owned routes (notably /settings) are independent of backend
957
+ // lifecycle/status events and remain open while an agent is working.
958
+ if (this.#dialogSource !== "backend")
959
+ return;
824
960
  this.#dialogHandle?.hide();
825
961
  this.#dialogHandle = undefined;
826
962
  this.#dialogId = undefined;
963
+ this.#dialogSource = undefined;
827
964
  return;
828
965
  }
829
- if (this.#dialogId === dialog.id)
966
+ if (this.#dialogSource === "backend" && this.#dialogId === dialog.id)
830
967
  return;
831
968
  this.#dialogHandle?.hide();
969
+ this.#dialogSource = "backend";
832
970
  const component = createPiShellDialog(dialog, {
833
971
  onSelect: () => {
834
972
  this.#dialogHandle?.hide();
835
973
  this.#dialogHandle = undefined;
974
+ this.#dialogId = undefined;
975
+ this.#dialogSource = undefined;
836
976
  },
837
977
  onCancel: () => {
838
978
  this.#dialogHandle?.hide();
839
979
  this.#dialogHandle = undefined;
980
+ this.#dialogId = undefined;
981
+ this.#dialogSource = undefined;
840
982
  },
841
983
  });
842
984
  this.#dialogHandle = this.runtime.showOverlay(component, { width: "70%", maxHeight: "80%", anchor: "center" });
@@ -853,6 +995,7 @@ export class OwnedUiSessionShell {
853
995
  return this.runWorkflow({ command: name, argument });
854
996
  // Unknown slash input, prompt templates, skills, and extension commands remain Pi prompt input.
855
997
  this.root.editor.addToHistory(text);
998
+ this.root.resumeViewportFollowing();
856
999
  return this.#execute({
857
1000
  type: this.view().lifecycle === "busy" ? "steer" : "prompt",
858
1001
  correlationId: this.#correlation("prompt-command"),
@@ -0,0 +1,39 @@
1
+ import type { OwnedUiViewportSettings } from "../../../contracts/owned-ui/index.js";
2
+ import { type TranscriptViewportFrame, type TranscriptViewportFrameInput } from "../../../ui/components/index.js";
3
+ import type { PiShellEditorPort } from "../components/index.js";
4
+ export interface SessionViewportControllerOptions {
5
+ readonly enabled: boolean;
6
+ readonly editor: PiShellEditorPort;
7
+ readonly requestRender: (force?: boolean) => void;
8
+ /** Whether mutable editor URL chips need an immediate forced repaint on deletion. */
9
+ readonly hasEditorLinks?: () => boolean;
10
+ }
11
+ export interface SessionViewportInputResult {
12
+ readonly data: string;
13
+ readonly consumed: boolean;
14
+ readonly copyText?: string;
15
+ }
16
+ /**
17
+ * Owns the stateful interaction policy of the bare-A1 session viewport. The
18
+ * shell root supplies semantic rows and dock geometry; this controller owns
19
+ * follow state, selection, pointer latches, timers, and input routing.
20
+ */
21
+ export declare class SessionViewportController {
22
+ #private;
23
+ constructor(options: SessionViewportControllerOptions);
24
+ get config(): OwnedUiViewportSettings;
25
+ get editorPointerSelecting(): boolean;
26
+ get transcriptPointerSelecting(): boolean;
27
+ setEditorPointerFrame(frame: {
28
+ readonly rowStart: number;
29
+ readonly rowEnd: number;
30
+ } | undefined): void;
31
+ compose(input: TranscriptViewportFrameInput): TranscriptViewportFrame;
32
+ /** Returns true when document wrapping may have changed. */
33
+ setConfig(config: OwnedUiViewportSettings): boolean;
34
+ resumeFollowing(): void;
35
+ noteCompletedAssistantMessage(): void;
36
+ reset(): void;
37
+ clearPointerState(): void;
38
+ handlePreInput(data: string, allowWheel?: boolean, now?: number): SessionViewportInputResult;
39
+ }