@timurproko/a1 0.2.7-dev.1252 → 0.2.7-dev.1255

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 (253) hide show
  1. package/bin/ui.js +3 -3
  2. package/dist/app/session-shell/clipboard-diagnostics.d.ts +2 -2
  3. package/dist/app/session-shell/clipboard-image.d.ts +0 -1
  4. package/dist/app/session-shell/clipboard-image.js +1 -1
  5. package/dist/app/session-shell/dialog-coordinator.d.ts +6 -5
  6. package/dist/app/session-shell/dialog-coordinator.js +3 -4
  7. package/dist/app/session-shell/dock-chrome.d.ts +11 -6
  8. package/dist/app/session-shell/dock-chrome.js +5 -8
  9. package/dist/app/session-shell/editor-hyperlink-budget.d.ts +0 -2
  10. package/dist/app/session-shell/editor-hyperlink-budget.js +1 -1
  11. package/dist/app/session-shell/exit-notice.d.ts +2 -2
  12. package/dist/app/session-shell/exit-notice.js +1 -1
  13. package/dist/app/session-shell/extension-surface.d.ts +8 -7
  14. package/dist/app/session-shell/helper-pool.d.ts +3 -4
  15. package/dist/app/session-shell/helper-pool.js +1 -1
  16. package/dist/app/session-shell/image-preparation-client.d.ts +4 -3
  17. package/dist/app/session-shell/image-preparation.d.ts +0 -2
  18. package/dist/app/session-shell/image-preparation.js +2 -2
  19. package/dist/app/session-shell/image-source.d.ts +2 -3
  20. package/dist/app/session-shell/image-source.js +2 -2
  21. package/dist/app/session-shell/input-surface-switch.d.ts +9 -7
  22. package/dist/app/session-shell/input-surface-switch.js +1 -2
  23. package/dist/app/session-shell/login-interaction.d.ts +5 -4
  24. package/dist/app/session-shell/login-interaction.js +2 -3
  25. package/dist/app/session-shell/paste-executor.d.ts +4 -8
  26. package/dist/app/session-shell/paste-executor.js +2 -2
  27. package/dist/app/session-shell/paste-protocol.d.ts +2 -2
  28. package/dist/app/session-shell/path-chip-presentation.d.ts +0 -5
  29. package/dist/app/session-shell/path-chip-presentation.js +3 -3
  30. package/dist/app/session-shell/prompt-chips.d.ts +8 -7
  31. package/dist/app/session-shell/prompt-history-controller.d.ts +3 -3
  32. package/dist/app/session-shell/prompt-submission.d.ts +5 -2
  33. package/dist/app/session-shell/prompt-submission.js +4 -5
  34. package/dist/app/session-shell/quit-outro-effects.d.ts +3 -3
  35. package/dist/app/session-shell/quit-outro-effects.js +1 -4
  36. package/dist/app/session-shell/quit-outro.d.ts +9 -12
  37. package/dist/app/session-shell/quit-outro.js +11 -11
  38. package/dist/app/session-shell/response-copy-protocol.d.ts +3 -2
  39. package/dist/app/session-shell/response-copy-transport.d.ts +2 -14
  40. package/dist/app/session-shell/response-copy-transport.js +1 -1
  41. package/dist/app/session-shell/session-lifecycle.d.ts +2 -1
  42. package/dist/app/session-shell/session-shell-root.d.ts +74 -89
  43. package/dist/app/session-shell/session-shell-root.js +81 -113
  44. package/dist/app/session-shell/session-shell.d.ts +13 -16
  45. package/dist/app/session-shell/session-shell.js +144 -268
  46. package/dist/app/session-shell/session-viewport-controller.d.ts +7 -6
  47. package/dist/app/session-shell/session-viewport-controller.js +2 -2
  48. package/dist/app/session-shell/settings-bindings.d.ts +24 -2
  49. package/dist/app/session-shell/settings-bindings.js +24 -0
  50. package/dist/app/session-shell/share-operation.d.ts +3 -3
  51. package/dist/app/session-shell/share-operation.js +4 -6
  52. package/dist/app/session-shell/shell-clipboard.d.ts +59 -0
  53. package/dist/app/session-shell/shell-clipboard.js +119 -0
  54. package/dist/app/session-shell/shell-editor-factory.d.ts +8 -4
  55. package/dist/app/session-shell/shell-editor-factory.js +13 -15
  56. package/dist/app/session-shell/shell-host.d.ts +16 -17
  57. package/dist/app/session-shell/shell-host.js +18 -19
  58. package/dist/app/session-shell/shell-layout.d.ts +2 -2
  59. package/dist/app/session-shell/shell-prompt-suggestions.d.ts +26 -0
  60. package/dist/app/session-shell/shell-prompt-suggestions.js +48 -0
  61. package/dist/app/session-shell/suggestion-surface.d.ts +1 -6
  62. package/dist/app/session-shell/suggestion-surface.js +1 -1
  63. package/dist/app/session-shell/system-clipboard.d.ts +2 -14
  64. package/dist/app/session-shell/system-clipboard.js +2 -7
  65. package/dist/app/session-shell/transcript-document.d.ts +11 -8
  66. package/dist/app/session-shell/transcript-document.js +13 -14
  67. package/dist/app/session-shell/viewport-layout.d.ts +2 -2
  68. package/dist/app/session-shell/workflow-output.d.ts +5 -3
  69. package/dist/app/session-shell/workflow-output.js +13 -15
  70. package/dist/cli/package-messages.d.ts +0 -1
  71. package/dist/cli/package-messages.js +1 -1
  72. package/dist/cli/session-context.d.ts +1 -1
  73. package/dist/cli/uninstall.d.ts +3 -2
  74. package/dist/composition/owned-settings.d.ts +2 -1
  75. package/dist/composition/owned-settings.js +0 -3
  76. package/dist/composition/owned-ui.d.ts +1 -1
  77. package/dist/composition/owned-ui.js +27 -27
  78. package/dist/composition/reference-selection-options.d.ts +1 -1
  79. package/dist/composition/reference-selection-options.js +1 -1
  80. package/dist/composition/session-runtime-registry.d.ts +2 -1
  81. package/dist/composition/settings-route-host.js +3 -10
  82. package/dist/composition/settings-search-input.d.ts +2 -1
  83. package/dist/composition/startup-route.d.ts +2 -2
  84. package/dist/composition/startup-route.js +2 -2
  85. package/dist/contracts/owned-ui/extension-ui.d.ts +2 -1
  86. package/dist/contracts/owned-ui/index.d.ts +1 -0
  87. package/dist/contracts/owned-ui/session-presenters.d.ts +3 -0
  88. package/dist/contracts/owned-ui/shell-components.d.ts +543 -0
  89. package/dist/contracts/owned-ui/shell-components.js +1 -0
  90. package/dist/contracts/presentation/component.d.ts +17 -0
  91. package/dist/contracts/presentation/component.js +8 -0
  92. package/dist/contracts/presentation/index.d.ts +6 -15
  93. package/dist/contracts/presentation/index.js +2 -0
  94. package/dist/contracts/presentation/terminal.d.ts +290 -0
  95. package/dist/contracts/presentation/terminal.js +1 -0
  96. package/dist/contracts/presentation/theme.d.ts +46 -0
  97. package/dist/contracts/presentation/theme.js +1 -0
  98. package/dist/features/launch/development-launch.js +1 -1
  99. package/dist/features/launch/runtime-selection.d.ts +2 -2
  100. package/dist/features/launch/runtime-selection.js +2 -2
  101. package/dist/features/owned-ui/project-trust-prompt.d.ts +2 -2
  102. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  103. package/dist/features/owned-ui/release-note-state.d.ts +2 -1
  104. package/dist/features/owned-ui/settings-app.js +38 -219
  105. package/dist/features/owned-ui/settings-edits.d.ts +31 -0
  106. package/dist/features/owned-ui/settings-edits.js +139 -0
  107. package/dist/features/owned-ui/settings-scroll-rail.d.ts +29 -0
  108. package/dist/features/owned-ui/settings-scroll-rail.js +79 -0
  109. package/dist/features/owned-ui/settings-search-input.d.ts +2 -1
  110. package/dist/features/prompt-history/service.d.ts +3 -2
  111. package/dist/foundation/lifecycle/claim-gate.d.ts +2 -1
  112. package/dist/foundation/release/automatic-update.d.ts +5 -14
  113. package/dist/foundation/release/automatic-update.js +14 -14
  114. package/dist/foundation/release/bootstrap.js +4 -4
  115. package/dist/foundation/release/dependency-certification.d.ts +0 -1
  116. package/dist/foundation/release/dependency-certification.js +1 -1
  117. package/dist/foundation/release/early-accent.d.ts +2 -2
  118. package/dist/foundation/release/early-accent.js +1 -1
  119. package/dist/foundation/release/index.d.ts +2 -2
  120. package/dist/foundation/release/index.js +1 -1
  121. package/dist/foundation/release/latest-release.d.ts +3 -18
  122. package/dist/foundation/release/latest-release.js +10 -10
  123. package/dist/foundation/release/ordinary-launch-selection.d.ts +1 -1
  124. package/dist/foundation/release/ordinary-launch-selection.js +1 -1
  125. package/dist/foundation/release/release-gc.d.ts +3 -3
  126. package/dist/foundation/release/release-gc.js +4 -4
  127. package/dist/foundation/release/{cohort-state.d.ts → release-state.d.ts} +23 -23
  128. package/dist/foundation/release/{cohort-state.js → release-state.js} +8 -8
  129. package/dist/foundation/release/update-activation.js +2 -2
  130. package/dist/foundation/release/update-execution-lease.d.ts +2 -3
  131. package/dist/foundation/release/update-execution-lease.js +1 -1
  132. package/dist/foundation/release/update-launch.d.ts +2 -2
  133. package/dist/foundation/release/update.d.ts +0 -2
  134. package/dist/foundation/release/update.js +14 -14
  135. package/dist/foundation/startup/startup-budget.d.ts +5 -5
  136. package/dist/foundation/startup/startup-budget.js +2 -2
  137. package/dist/integrations/pi/components/editor-interaction.d.ts +4 -12
  138. package/dist/integrations/pi/components/index.d.ts +15 -9
  139. package/dist/integrations/pi/components/index.js +9 -5
  140. package/dist/integrations/pi/components/lazy-selectors.d.ts +2 -10
  141. package/dist/integrations/pi/components/lazy-selectors.js +1 -1
  142. package/dist/integrations/pi/components/modal-frame.d.ts +5 -22
  143. package/dist/integrations/pi/components/modal-frame.js +3 -23
  144. package/dist/integrations/pi/components/owned-editor-ux.d.ts +4 -3
  145. package/dist/integrations/pi/components/prompt-input-port.d.ts +2 -19
  146. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -1
  147. package/dist/integrations/pi/components/shell-editor-autocomplete.d.ts +3 -2
  148. package/dist/integrations/pi/components/shell-footer-status.d.ts +2 -2
  149. package/dist/integrations/pi/components/shell-footer-status.js +3 -2
  150. package/dist/integrations/pi/components/shell-presenters-info.d.ts +6 -5
  151. package/dist/integrations/pi/components/shell-presenters-info.js +2 -1
  152. package/dist/integrations/pi/components/shell-session-info-reference.d.ts +2 -1
  153. package/dist/integrations/pi/components/shell-shared-facade.d.ts +33 -227
  154. package/dist/integrations/pi/components/shell-shared-facade.js +8 -1
  155. package/dist/integrations/pi/components/skills-command.d.ts +1 -9
  156. package/dist/integrations/pi/components/skills-command.js +5 -5
  157. package/dist/integrations/pi/components/theme.d.ts +9 -2
  158. package/dist/integrations/pi/components/theme.js +17 -2
  159. package/dist/integrations/pi/components/thinking-selector-dialog.d.ts +2 -1
  160. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +2 -2
  161. package/dist/integrations/pi/engine/adapter.d.ts +3 -5
  162. package/dist/integrations/pi/engine/adapter.js +3 -12
  163. package/dist/integrations/pi/engine/changelog.d.ts +2 -2
  164. package/dist/integrations/pi/engine/changelog.js +3 -3
  165. package/dist/integrations/pi/engine/compaction-progress.d.ts +2 -3
  166. package/dist/integrations/pi/engine/compaction-progress.js +1 -1
  167. package/dist/integrations/pi/engine/conformance.d.ts +2 -1
  168. package/dist/integrations/pi/engine/default-model-setting.d.ts +0 -2
  169. package/dist/integrations/pi/engine/default-model-setting.js +1 -2
  170. package/dist/integrations/pi/engine/event-delivery.d.ts +2 -1
  171. package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -1
  172. package/dist/integrations/pi/engine/host.js +2 -2
  173. package/dist/integrations/pi/engine/message-values.d.ts +0 -1
  174. package/dist/integrations/pi/engine/message-values.js +1 -1
  175. package/dist/integrations/pi/engine/pending-delivery.d.ts +0 -4
  176. package/dist/integrations/pi/engine/pending-delivery.js +3 -3
  177. package/dist/integrations/pi/engine/ports.d.ts +7 -1
  178. package/dist/integrations/pi/engine/ports.js +4 -0
  179. package/dist/integrations/pi/engine/project-trust-preflight.d.ts +2 -1
  180. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +3 -2
  181. package/dist/integrations/pi/engine/provider-authentication.d.ts +1 -1
  182. package/dist/integrations/pi/engine/public-main-entry.d.ts +1 -5
  183. package/dist/integrations/pi/engine/public-main-entry.js +1 -1
  184. package/dist/integrations/pi/engine/repository-pr-rest.d.ts +1 -9
  185. package/dist/integrations/pi/engine/repository-pr-rest.js +2 -2
  186. package/dist/integrations/pi/engine/repository-pr.d.ts +1 -3
  187. package/dist/integrations/pi/engine/repository-pr.js +1 -1
  188. package/dist/integrations/pi/engine/resource-catalog.d.ts +2 -1
  189. package/dist/integrations/pi/engine/session-integration.d.ts +1 -1
  190. package/dist/integrations/pi/engine/session-runtime.d.ts +4 -3
  191. package/dist/integrations/pi/engine/settings-bridge.d.ts +0 -2
  192. package/dist/integrations/pi/engine/settings-bridge.js +1 -1
  193. package/dist/integrations/pi/engine/settings-port.d.ts +3 -2
  194. package/dist/integrations/pi/engine/transcript-projection.d.ts +2 -1
  195. package/dist/integrations/pi/engine/workflow-contexts.d.ts +1 -1
  196. package/dist/integrations/pi/engine/workflow-runner.d.ts +1 -1
  197. package/dist/integrations/pi/engine/workflow-support.d.ts +0 -6
  198. package/dist/integrations/pi/engine/workflow-support.js +4 -18
  199. package/dist/integrations/pi/engine/workflows.d.ts +5 -5
  200. package/dist/integrations/pi/session-presenters/components.d.ts +3 -0
  201. package/dist/integrations/pi/session-presenters/components.js +75 -0
  202. package/dist/integrations/pi/session-presenters/index.d.ts +1 -0
  203. package/dist/integrations/pi/session-presenters/index.js +1 -0
  204. package/dist/integrations/pi/session-presenters/presentation.d.ts +3 -0
  205. package/dist/integrations/pi/session-presenters/presentation.js +10 -0
  206. package/dist/integrations/pi/session-presenters/presenters.d.ts +2 -1
  207. package/dist/integrations/pi/session-presenters/presenters.js +3 -1
  208. package/dist/integrations/pi/tui-runtime/adapter.js +21 -30
  209. package/dist/integrations/pi/tui-runtime/contracts.d.ts +22 -166
  210. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +5 -40
  211. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
  212. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  213. package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.d.ts +4 -6
  214. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -8
  215. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +3 -9
  216. package/dist/integrations/pi/tui-runtime/runtime-factory.d.ts +3 -0
  217. package/dist/integrations/pi/tui-runtime/runtime-factory.js +11 -0
  218. package/dist/ui/components/index.d.ts +2 -2
  219. package/dist/ui/components/index.js +1 -1
  220. package/dist/ui/components/selection-copy.d.ts +0 -1
  221. package/dist/ui/components/selection-copy.js +0 -12
  222. package/dist/ui/components/theme.d.ts +4 -19
  223. package/dist/ui/components/theme.js +6 -0
  224. package/dist/ui/settings/declarations.d.ts +2 -3
  225. package/dist/ui/settings/declarations.js +2 -3
  226. package/dist/ui/settings/early-accent.js +1 -1
  227. package/dist/ui/settings/index.d.ts +3 -5
  228. package/dist/ui/settings/index.js +2 -3
  229. package/dist/ui/settings/manager.d.ts +0 -4
  230. package/dist/ui/settings/manager.js +6 -62
  231. package/dist/ui/settings/resolution.d.ts +6 -18
  232. package/dist/ui/settings/resolution.js +4 -103
  233. package/dist/ui/settings/sections.d.ts +0 -4
  234. package/dist/ui/settings/sections.js +1 -1
  235. package/docs/architecture/boundaries.md +2 -2
  236. package/docs/architecture/internal-naming.md +2 -0
  237. package/docs/architecture/project-structure.md +4 -2
  238. package/docs/validation.md +1 -1
  239. package/package.json +3 -4
  240. package/dist/integrations/pi/components/components.d.ts +0 -4
  241. package/dist/integrations/pi/components/components.js +0 -48
  242. package/dist/integrations/pi/components/conformance.d.ts +0 -18
  243. package/dist/integrations/pi/components/conformance.js +0 -70
  244. package/dist/integrations/pi/components/shell-components.d.ts +0 -7
  245. package/dist/integrations/pi/components/shell-components.js +0 -7
  246. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +0 -33
  247. package/dist/integrations/pi/components/upstream/theme/theme-controller.js +0 -114
  248. package/dist/ui/settings/migrations.d.ts +0 -19
  249. package/dist/ui/settings/migrations.js +0 -225
  250. package/dist/ui/settings/progress-palette.generated.d.ts +0 -139
  251. package/dist/ui/settings/progress-palette.generated.js +0 -139
  252. /package/dist/{foundation/release → contracts/presentation}/progress-palette.generated.d.ts +0 -0
  253. /package/dist/{foundation/release → contracts/presentation}/progress-palette.generated.js +0 -0
@@ -1,6 +1,4 @@
1
- import { ResponseCopyCoordinator } from "./response-copy-coordinator.js";
2
- import { createResponseCopyExecutor, responseCopyDestination } from "./response-copy-transport.js";
3
- import { MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
1
+ import { ShellClipboard } from "./shell-clipboard.js";
4
2
  import { PromptHistoryController } from "./prompt-history-controller.js";
5
3
  import { SessionDialogCoordinator } from "./dialog-coordinator.js";
6
4
  import { LoginInteraction } from "./login-interaction.js";
@@ -8,22 +6,14 @@ import { PromptSubmission } from "./prompt-submission.js";
8
6
  import { SessionConcerns } from "./session-concern.js";
9
7
  import { composeExitText, SessionStopLatch } from "./session-lifecycle.js";
10
8
  import { openWorkflowSelector } from "./workflow-router.js";
11
- import { bindSessionSettings } from "./settings-bindings.js";
12
- import { promptSuggestionSurface, routeResponseToSuggestions, suggestionModelKey, suggestionPresentationBlockReason, } from "./suggestion-surface.js";
9
+ import { bindSessionSettings, bindShellSettings } from "./settings-bindings.js";
10
+ import { promptSuggestionSurface, suggestionPresentationBlockReason, } from "./suggestion-surface.js";
13
11
  import { CLEAR_EXIT_INTERVAL_MS, boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
14
12
  import { OWNED_UI_HIDDEN_COMMAND_NAMES } from "../../contracts/owned-ui/index.js";
15
- import { ImageAttachmentError } from "../../contracts/owned-ui/index.js";
16
13
  import { copyAcknowledgementText } from "../../ui/components/selection-copy.js";
17
- import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
18
- import { workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
19
- import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
20
- import { ProgramStatusReporter } from "../../integrations/pi/components/upstream/program-status-reporter.js";
21
- import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
22
- import { createPiShellAuthProviderSelector, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellReloadBox, createPiShellSkillsSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
14
+ import { ShellPromptSuggestions } from "./shell-prompt-suggestions.js";
23
15
  import { OwnedUiTerminalHost, componentRuntime, sessionTerminalHostOptions, } from "./shell-host.js";
24
- import { runImageWorker } from "./image-preparation-client.js";
25
16
  import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
26
- import { writeSystemClipboardText, } from "./system-clipboard.js";
27
17
  import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
28
18
  // Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
29
19
  const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
@@ -46,12 +36,9 @@ export class OwnedUiSessionPresenter {
46
36
  #concerns;
47
37
  #listeners = new Set();
48
38
  #unsubscribe;
49
- #unsubscribePromptSuggestions;
50
39
  #promptSuggestions;
51
40
  #promptImages;
52
41
  #skills;
53
- #unsubscribePromptImages;
54
- #unsubscribeSkills;
55
42
  #installedCommandSignature = "";
56
43
  #promptHistory = null;
57
44
  #historyBinding;
@@ -65,21 +52,17 @@ export class OwnedUiSessionPresenter {
65
52
  #shutdownPromise;
66
53
  #disposePromise;
67
54
  #layoutParts;
68
- #pendingClipboardWrite = Promise.resolve();
69
55
  // Invariant: translated startup diagnostics create at most one dock notice per kind per shell.
70
56
  #startupTrustHandled = false;
71
57
  #customViewport;
72
- #lazySelectors;
73
- #lazySelectorsPromise;
74
- #responseCopy;
58
+ #clipboard;
75
59
  // Rationale: only an executor this shell created owns a spare copy helper worth warming and disposing.
76
- #copyExecutor;
77
60
  #copyIntentSequence = 0;
78
61
  #unbindClipboardWriter;
79
62
  #reloadPresentation;
80
63
  #streamPresentation;
81
64
  #inputCoordination;
82
- #unsubscribeSettings;
65
+ #unbindShellSettings;
83
66
  #unbindSessionSettings;
84
67
  #terminalProgressEnabled = false;
85
68
  #programStatus;
@@ -88,7 +71,6 @@ export class OwnedUiSessionPresenter {
88
71
  #lastEscapeTime = 0;
89
72
  #sessionGeneration;
90
73
  #sessionBindingGeneration;
91
- #suggestionModelKey;
92
74
  constructor(host, options) {
93
75
  const { backend, cwd, routeHost, sessionLayout } = options.engine;
94
76
  const { terminal, startup, viewportSettings, reload: reloadPresentation, stream: streamPresentationOptions, input: inputPresentation, } = options.presentation ?? {};
@@ -99,15 +81,12 @@ export class OwnedUiSessionPresenter {
99
81
  this.#presenters = options.presenters;
100
82
  this.#sessionGeneration = this.backend.identity.sessionGeneration;
101
83
  this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
102
- this.#suggestionModelKey = suggestionModelKey(this.backend.session.view());
103
84
  this.#customViewport = sessionLayout === "custom-viewport";
104
85
  this.#inputCoordination = inputPresentation?.coordination !== false;
105
86
  this.#host = host;
106
87
  this.#terminal = host.connect(this);
107
88
  // Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
108
89
  this.#dialogHost = this.#createDialogHost();
109
- this.#lazySelectors = options.diagnostics?.lazySelectors;
110
- let promptSuggestionController = null;
111
90
  // Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
112
91
  this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
113
92
  this.#skills = this.#customViewport ? options.skills ?? null : null;
@@ -115,48 +94,19 @@ export class OwnedUiSessionPresenter {
115
94
  // throw releases them in reverse, so a shell that never finished constructing holds nothing.
116
95
  const constructed = [() => host.disconnect(this)];
117
96
  try {
118
- this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
119
- execute: responseCopy?.execute ?? (this.#copyExecutor = createResponseCopyExecutor({
120
- ...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.copy }),
121
- ...(terminal === undefined && clipboard === undefined ? {} : { destination: "terminal" }),
122
- ...(clipboard?.writeText === undefined ? {} : { writeText: (text, signal) => clipboard.writeText(text, signal) }),
123
- // Invariant: the owned host is the route contract. Pseudoterminal children must not infer
124
- // clipboard reachability from Node's view of stdout, and inactive presenters are fenced here.
125
- terminal: { submit: async (control, signal) => {
126
- if (signal.aborted || this.#disposed || !this.#terminal.running || !this.#terminal.attached)
127
- throw new Error("Copy canceled");
128
- // Performance: never grow the real terminal's pending buffer with another clipboard payload.
129
- if (terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
130
- throw new Error("Clipboard terminal is busy");
131
- }
132
- this.#terminal.writeControl(control);
133
- } },
134
- })),
135
- ...(responseCopy?.onEvent === undefined ? {} : { onEvent: responseCopy.onEvent }),
136
- }) : null;
137
- constructed.push(() => { this.#responseCopy?.dispose(); this.#copyExecutor?.dispose(); });
138
- const readClipboard = async (signal) => {
139
- if (signal.aborted)
140
- throw new ImageAttachmentError("image-canceled");
141
- if (clipboard === undefined)
142
- return runImageWorker({ kind: "clipboard" }, signal);
143
- try {
144
- const image = await clipboard.readImage?.(signal);
145
- if (image !== null && image !== undefined) {
146
- const canonical = await runImageWorker({ kind: "canonicalize", source: image }, signal);
147
- if (canonical !== null)
148
- return { kind: "image", ...canonical };
149
- }
150
- }
151
- catch (error) {
152
- if (error instanceof ImageAttachmentError)
153
- throw error;
154
- // Compatibility: an unavailable image reader can still provide text.
155
- }
156
- const text = await clipboard.readText(signal);
157
- return text === null ? null : { kind: "text", text };
158
- };
159
- this.root = new OwnedUiSessionShellRoot(this.backend.session.view(), cwd, {
97
+ this.#clipboard = new ShellClipboard({
98
+ ownedCopy: this.#customViewport, substituteTerminal: terminal !== undefined,
99
+ ...(sharedClipboard === undefined ? {} : { shared: sharedClipboard }),
100
+ ...(clipboard === undefined ? {} : { clipboard }),
101
+ ...(responseCopy === undefined ? {} : { responseCopy }),
102
+ ...(pastePreparation === undefined ? {} : { pastePreparation }),
103
+ disposed: () => this.#disposed,
104
+ terminalAttached: () => this.#terminal.running && this.#terminal.attached,
105
+ writeControl: control => this.#terminal.writeControl(control),
106
+ });
107
+ constructed.push(() => this.#clipboard.dispose());
108
+ const pastePreparationOptions = this.#clipboard.pastePreparation();
109
+ this.root = new OwnedUiSessionShellRoot(this.backend.session.view(), cwd, { presentation: options.presenters, components: options.presenters.components,
160
110
  getColumns: () => this.#terminal.viewport().columns,
161
111
  getRows: () => this.#terminal.viewport().rows,
162
112
  requestRender: force => this.#terminal.requestRender(force),
@@ -186,55 +136,31 @@ export class OwnedUiSessionPresenter {
186
136
  onDequeue: () => this.restoreQueuedInput(),
187
137
  onEditorChange: text => {
188
138
  this.#submission.noteEditorChange();
189
- promptSuggestionController?.abortPending();
139
+ this.#promptSuggestions?.abortPending();
190
140
  if (text.length === 0) {
191
141
  // Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
192
142
  // Reassert retained ghost text in the microtask before Pi emits its scheduled frame.
193
- queueMicrotask(() => promptSuggestionController?.restoreAvailable());
143
+ queueMicrotask(() => this.#promptSuggestions?.restoreAvailable());
194
144
  }
195
145
  this.root.reconcilePromptImageLimitNotice(text);
196
146
  },
197
- onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
147
+ onPromptSuggestionAccepted: () => this.#promptSuggestions?.accept(),
198
148
  onPromptSuggestionEligibilityChanged: () => {
199
- queueMicrotask(() => promptSuggestionController?.restoreAvailable());
149
+ queueMicrotask(() => this.#promptSuggestions?.restoreAvailable());
200
150
  },
201
151
  onInputSurfaceChanged: () => {
202
- promptSuggestionController?.invalidate();
152
+ this.#promptSuggestions?.invalidate();
203
153
  this.#promptHistory?.synchronize();
204
154
  },
205
155
  onCopyText: (text, cut) => {
206
- if (this.#responseCopy !== null) {
207
- this.#trackCopy(this.#responseCopy.submitText(text), cut ? undefined : text.length);
208
- return;
209
- }
210
- const write = () => {
211
- if (this.#disposed)
212
- return Promise.resolve();
213
- this.#terminal.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
214
- return clipboard === undefined ? writeSystemClipboardText(text)
215
- : clipboard.writeText?.(text) ?? Promise.resolve();
216
- };
217
- // Compatibility: comparison profiles retain their existing clipboard path.
218
- this.#pendingClipboardWrite = write().catch(() => { });
219
- },
220
- readClipboardContent: async (signal = new AbortController().signal) => {
221
- await this.#pendingClipboardWrite;
222
- return readClipboard(signal);
223
- },
224
- captureClipboardPaste: () => {
225
- const before = this.#responseCopy?.capturePasteBarrier() ?? (async () => true);
226
- if (clipboard !== undefined)
227
- return { kind: "provided", read: readClipboard, before };
228
- if (responseCopyDestination(process.env) === "terminal")
229
- return {
230
- kind: "provided", before, read: async () => { throw new ImageAttachmentError("paste-unavailable"); },
231
- };
232
- return { kind: "native", before };
156
+ const delivery = this.#clipboard.copyText(text);
157
+ if (delivery !== undefined)
158
+ this.#trackCopy(delivery, cut ? undefined : text.length);
233
159
  },
160
+ readClipboardContent: signal => this.#clipboard.readContent(signal),
161
+ captureClipboardPaste: () => this.#clipboard.capturePaste(),
234
162
  ...(pasteDiagnostics === undefined ? {} : { pasteDiagnostics: pasteDiagnostics }),
235
- ...(pastePreparation === undefined && sharedClipboard === undefined ? {} : {
236
- pastePreparation: { ...pastePreparation, ...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.paste }) },
237
- }),
163
+ ...(pastePreparationOptions === undefined ? {} : { pastePreparation: pastePreparationOptions }),
238
164
  ...(sharedKeybindings === undefined ? {} : { keybindings: sharedKeybindings }),
239
165
  ...(this.#promptImages === null ? {} : { promptImageLimit: this.#promptImages.limit }),
240
166
  skillsPresentation: () => this.#skills?.presentation() ?? "expand",
@@ -251,68 +177,42 @@ export class OwnedUiSessionPresenter {
251
177
  this.#dialogs = new SessionDialogCoordinator({
252
178
  terminal: this.#terminal,
253
179
  root: this.root,
254
- routeHost: routeHost ?? null,
180
+ routeHost: routeHost ?? null, dialogs: options.presenters.components.dialogs,
255
181
  disposed: () => this.#disposed,
256
182
  exit: () => { void this.shutdown(); },
257
183
  ...(startupRoute === undefined ? {} : { startupRoute }),
258
184
  });
259
185
  constructed.push(() => this.#dialogs.dispose());
260
186
  const initialPiSettings = this.backend.settings.snapshot();
261
- this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
187
+ this.#programStatus = options.presenters.components.chrome.programStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
262
188
  this.#login = new LoginInteraction({
263
- terminal: this.#terminal, root: this.root, programStatus: this.#programStatus,
189
+ terminal: this.#terminal, root: this.root, programStatus: this.#programStatus, dialogs: options.presenters.components.dialogs,
264
190
  syncProgress: () => this.#syncTerminalProgress(this.view()),
265
191
  });
266
192
  this.#submission = this.#createSubmission(cwd);
267
193
  this.#reloadPresentation = reloadPresentation;
268
- const presentationInterval = streamPresentationOptions?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
269
- this.#streamPresentation = streamPresentationOptions?.scheduler === undefined
270
- ? new StreamPresentationCoalescer(() => this.#terminal.requestRender(), presentationInterval)
271
- : new StreamPresentationCoalescer(() => this.#terminal.requestRender(), presentationInterval, streamPresentationOptions.scheduler);
194
+ this.#streamPresentation = new StreamPresentationCoalescer(() => this.#terminal.requestRender(), streamPresentationOptions?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS, streamPresentationOptions?.scheduler);
272
195
  constructed.push(() => this.#streamPresentation.dispose());
273
- const promptSuggestionOptions = this.#customViewport ? options.suggestions : undefined;
274
- promptSuggestionController = promptSuggestionOptions === undefined ? null : new ContextualPromptSuggestionController({
275
- generator: promptSuggestionOptions.generator,
276
- enabled: promptSuggestionOptions.enabled(),
277
- ...(promptSuggestionOptions.diagnostics === undefined ? {} : { diagnostics: promptSuggestionOptions.diagnostics }),
278
- surface: promptSuggestionSurface(this.root, identity => this.#promptSuggestionPresentationBlockReason(identity), () => this.#terminal.requestRender()),
279
- });
280
- this.#promptSuggestions = promptSuggestionController;
196
+ this.#promptSuggestions = new ShellPromptSuggestions(this.#customViewport ? options.suggestions : undefined, promptSuggestionSurface(this.root, identity => suggestionPresentationBlockReason(identity, { disposed: this.#disposed, view: this.view(), sessionGeneration: this.backend.identity.sessionGeneration }, () => this.root.promptSuggestionPresentationBlockReason()), () => this.#terminal.requestRender()), this.backend.session.view());
281
197
  constructed.push(() => this.#promptSuggestions?.dispose());
282
- this.#unsubscribePromptSuggestions = promptSuggestionOptions === undefined
283
- ? () => { }
284
- : promptSuggestionOptions.onChange(enabled => this.#promptSuggestions?.setEnabled(enabled));
285
- constructed.push(this.#unsubscribePromptSuggestions);
286
- this.#unsubscribePromptImages = this.#promptImages === null ? () => { } : this.#promptImages.onChange(() => {
287
- if (!this.#disposed && this.root.reconcilePromptImageLimitNotice())
198
+ this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
199
+ this.#unbindShellSettings = bindShellSettings({
200
+ customViewport: this.#customViewport, promptImages: this.#promptImages, skills: this.#skills,
201
+ viewport: viewportSettings, historyScope: promptHistory?.onScopeChange,
202
+ }, {
203
+ root: this.root,
204
+ promptImageLimitChanged: () => { if (!this.#disposed && this.root.reconcilePromptImageLimitNotice())
205
+ this.#terminal.requestRender(); },
206
+ skillsChanged: () => {
207
+ // Invariant: reinstalling drops extension provider wrappers, so an unrelated setting change leaves the list alone.
208
+ if (this.#disposed || this.#commandListSignature() === this.#installedCommandSignature)
209
+ return;
210
+ this.#installAutocompleteCommands();
288
211
  this.#terminal.requestRender();
212
+ },
213
+ historyScopeChanged: scope => this.#rebindTranscript(scope),
289
214
  });
290
- constructed.push(this.#unsubscribePromptImages);
291
- // Rationale: the same listener refreshes the menu for the A1 presentation choice and for the engine's
292
- // skill-command registration, both of which the owned settings manager reports through one change.
293
- this.#unsubscribeSkills = this.#skills === null ? () => { } : this.#skills.onChange(() => {
294
- // Invariant: reinstalling drops extension provider wrappers, so an unrelated setting change leaves the list alone.
295
- if (this.#disposed || this.#commandListSignature() === this.#installedCommandSignature)
296
- return;
297
- this.#installAutocompleteCommands();
298
- this.#terminal.requestRender();
299
- });
300
- constructed.push(this.#unsubscribeSkills);
301
- this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
302
- const applyViewportSettings = () => {
303
- const snapshot = viewportSettings?.snapshot();
304
- this.root.setViewportConfig(snapshot ?? {
305
- scrollbarAppearance: "auto",
306
- scrollbarStyle: "thin",
307
- scrollbarSpeed: "normal",
308
- });
309
- };
310
- applyViewportSettings();
311
- const unsubscribeViewport = this.#customViewport && viewportSettings ? viewportSettings.onChange(settings => this.root.setViewportConfig(settings)) : () => { };
312
- const unsubscribeHistoryScope = this.#customViewport && promptHistory?.onScopeChange?.(scope => this.#rebindTranscript(scope));
313
- this.#unsubscribeSettings = () => { unsubscribeViewport(); if (unsubscribeHistoryScope)
314
- unsubscribeHistoryScope(); };
315
- constructed.push(this.#unsubscribeSettings);
215
+ constructed.push(this.#unbindShellSettings);
316
216
  this.#unbindSessionSettings = bindSessionSettings(this.backend.settings, initialPiSettings, {
317
217
  terminal: this.#terminal,
318
218
  root: this.root,
@@ -345,64 +245,16 @@ export class OwnedUiSessionPresenter {
345
245
  prompt: request => this.#login.requestInput(request),
346
246
  notify: event => this.#login.notify(event),
347
247
  publish: message => {
348
- this.root.appendWorkflowMessage(message);
248
+ this.root.workflow.appendMessage(message);
349
249
  this.#terminal.requestRender();
350
250
  },
351
251
  finishLogin: () => this.#login.finish(),
352
252
  });
353
253
  this.#installAutocompleteCommands();
354
- this.#unsubscribe = this.backend.session.onEvent(event => {
355
- // Invariant: once disposal begins no event touches the view; only the stop still settles the shell.
356
- if (this.#disposed) {
357
- if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
358
- this.#stop.settleAfter(() => this.dispose());
359
- return;
360
- }
361
- // Performance: a streamed chunk names one block, and touching only that block is what keeps the
362
- // cost of a chunk the same in a long session as in a new one. Everything else
363
- // resynchronizes the view, which is cheap next to re-reading the transcript.
364
- if (event.type === "agent-run-started") {
365
- this.#promptSuggestions?.invalidate();
366
- this.root.resumeViewportFollowing();
367
- }
368
- if (event.type === "assistant-message-completed" && !event.successful) {
369
- this.#programStatus.setErrorMessage(this.view().status.diagnostics.at(-1));
370
- }
371
- if (event.type === "agent-run-started" || event.type === "assistant-message-completed" || event.type === "agent-run-settled") {
372
- this.#programStatus.handleEvent(event);
373
- }
374
- if (event.type === "assistant-message-completed") {
375
- this.root.noteCompletedAssistantMessage();
376
- routeResponseToSuggestions(this.#promptSuggestions, event, () => this.root.promptSuggestionPrepareBlockReason());
377
- }
378
- const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
379
- const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.identity.sessionGeneration
380
- ? this.#syncBlock(event.block)
381
- : semanticOnly ? this.view() : this.#syncView();
382
- this.#syncTerminalProgress(view);
383
- const currentModelKey = suggestionModelKey(view);
384
- if (this.backend.identity.sessionGeneration !== this.#sessionGeneration || currentModelKey !== this.#suggestionModelKey) {
385
- this.#promptSuggestions?.invalidate();
386
- this.#suggestionModelKey = currentModelKey;
387
- }
388
- if (event.type === "status" && /^(Retrying|Compacting)/.test(event.status.workingMessage ?? "")) {
389
- this.#promptSuggestions?.invalidate();
390
- }
391
- if (event.type === "agent-run-settled") {
392
- routeResponseToSuggestions(this.#promptSuggestions, event, () => this.root.promptSuggestionPrepareBlockReason());
393
- }
394
- if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
395
- this.#stop.settleAfter(() => this.dispose());
396
- });
254
+ this.#unsubscribe = this.backend.session.onEvent(event => this.#onSessionEvent(event));
397
255
  constructed.push(this.#unsubscribe);
398
- this.#unbindClipboardWriter = this.#responseCopy === null ? () => { } : this.backend.extensions.bindClipboardWriter(async (text) => {
399
- const result = await this.#responseCopy.submitText(text);
400
- if (result.outcome === "delivered")
401
- return true;
402
- if (result.outcome === "submitted-unverified")
403
- return false;
404
- throw new Error(result.outcome === "timed-out" ? "Clipboard delivery timed out" : "Clipboard delivery could not be completed");
405
- });
256
+ this.#unbindClipboardWriter = this.#clipboard.responseCopy === null ? () => { }
257
+ : this.backend.extensions.bindClipboardWriter(text => this.#clipboard.writeForExtension(text));
406
258
  constructed.push(this.#unbindClipboardWriter);
407
259
  if (this.backend.session.view().lifecycle === "stopped")
408
260
  this.#stop.resolve();
@@ -482,7 +334,7 @@ export class OwnedUiSessionPresenter {
482
334
  const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.#terminal.hasFocusedOverlay());
483
335
  if (routed.copySelection !== undefined) {
484
336
  const snapshot = routed.copySelection;
485
- this.#trackCopy(this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite), copyCharacters(snapshot));
337
+ this.#trackCopy(this.#clipboard.copySelection(snapshot), copyCharacters(snapshot));
486
338
  }
487
339
  if (routed.copyLastMessage)
488
340
  void this.runWorkflow({ command: "copy", argument: "" });
@@ -490,18 +342,54 @@ export class OwnedUiSessionPresenter {
490
342
  return routed.data === data ? undefined : { data: routed.data };
491
343
  return routed.data.length === 0 ? { consume: true } : { data: routed.data };
492
344
  }
345
+ #onSessionEvent(event) {
346
+ // Invariant: once disposal begins no event touches the view; only the stop still settles the shell.
347
+ if (this.#disposed) {
348
+ if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
349
+ this.#stop.settleAfter(() => this.dispose());
350
+ return;
351
+ }
352
+ // Performance: a streamed chunk names one block, and touching only that block is what keeps the
353
+ // cost of a chunk the same in a long session as in a new one. Everything else
354
+ // resynchronizes the view, which is cheap next to re-reading the transcript.
355
+ if (event.type === "agent-run-started") {
356
+ this.#promptSuggestions?.invalidate();
357
+ this.root.resumeViewportFollowing();
358
+ }
359
+ if (event.type === "assistant-message-completed" && !event.successful) {
360
+ this.#programStatus.setErrorMessage(this.view().status.diagnostics.at(-1));
361
+ }
362
+ if (event.type === "agent-run-started" || event.type === "assistant-message-completed" || event.type === "agent-run-settled") {
363
+ this.#programStatus.handleEvent(event);
364
+ }
365
+ if (event.type === "assistant-message-completed") {
366
+ this.root.noteCompletedAssistantMessage();
367
+ this.#promptSuggestions?.route(event, () => this.root.promptSuggestionPrepareBlockReason());
368
+ }
369
+ const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
370
+ const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.identity.sessionGeneration
371
+ ? this.#syncBlock(event.block)
372
+ : semanticOnly ? this.view() : this.#syncView();
373
+ this.#syncTerminalProgress(view);
374
+ this.#promptSuggestions?.noteView(view, this.backend.identity.sessionGeneration !== this.#sessionGeneration);
375
+ if (event.type === "status" && /^(Retrying|Compacting)/.test(event.status.workingMessage ?? "")) {
376
+ this.#promptSuggestions?.invalidate();
377
+ }
378
+ if (event.type === "agent-run-settled") {
379
+ this.#promptSuggestions?.route(event, () => this.root.promptSuggestionPrepareBlockReason());
380
+ }
381
+ if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
382
+ this.#stop.settleAfter(() => this.dispose());
383
+ }
493
384
  #trackCopy(delivery, count) {
494
385
  const intent = ++this.#copyIntentSequence;
495
386
  void delivery?.then(({ outcome }) => {
496
387
  if (count === undefined || this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
497
388
  || outcome !== "delivered" && outcome !== "submitted-unverified")
498
389
  return;
499
- this.root.showCopyAcknowledgement(copyAcknowledgementText(count));
390
+ this.root.chrome.showCopyAcknowledgement(copyAcknowledgementText(count));
500
391
  });
501
392
  }
502
- #promptSuggestionPresentationBlockReason(identity) {
503
- return suggestionPresentationBlockReason(identity, { disposed: this.#disposed, view: this.view(), sessionGeneration: this.backend.identity.sessionGeneration }, () => this.root.promptSuggestionPresentationBlockReason());
504
- }
505
393
  start() {
506
394
  if (this.#started)
507
395
  return;
@@ -516,9 +404,9 @@ export class OwnedUiSessionPresenter {
516
404
  if (this.#disposed || !this.#customViewport)
517
405
  return;
518
406
  this.#dialogs.presentStartupRoute();
519
- void this.#loadLazySelectors().then(selectors => selectors.prepare()).catch(() => { });
407
+ void this.#presenters.components.dialogs.lazySelectors().then(selectors => selectors.prepare()).catch(() => { });
520
408
  this.root.warmPastePreparation();
521
- this.#copyExecutor?.warm();
409
+ this.#clipboard.warm();
522
410
  });
523
411
  }
524
412
  onView(listener) {
@@ -618,7 +506,7 @@ export class OwnedUiSessionPresenter {
618
506
  }
619
507
  async cycleModel(direction) {
620
508
  const result = await this.backend.workflows.cycleModelWorkflow(direction);
621
- this.root.appendWorkflowResult(result);
509
+ this.root.workflow.appendResult(result);
622
510
  this.#terminal.requestRender();
623
511
  return workflowAdapterResult(result);
624
512
  }
@@ -639,8 +527,8 @@ export class OwnedUiSessionPresenter {
639
527
  setInputSurface: surface => this.root.setInputSurface(surface),
640
528
  requestRender: () => this.#terminal.requestRender(),
641
529
  viewport: () => this.#terminal.viewport(),
642
- appendWorkflowStatus: text => this.root.appendWorkflowStatus(text),
643
- appendWorkflowResult: result => this.root.appendWorkflowResult(result),
530
+ appendWorkflowStatus: text => this.root.workflow.appendStatus(text),
531
+ appendWorkflowResult: result => this.root.workflow.appendResult(result),
644
532
  runWorkflow: request => this.runWorkflow(request),
645
533
  // Invariant: the custom bare-A1 surface holds frames while optional selectors load and is permanently
646
534
  // fullscreen; the pinned comparison profiles render immediately and keep Pi's TUI mode switch.
@@ -649,14 +537,6 @@ export class OwnedUiSessionPresenter {
649
537
  : { switchTuiMode: mode => this.#terminal.switchMode(mode) }),
650
538
  };
651
539
  }
652
- #loadLazySelectors() {
653
- if (this.#lazySelectors !== undefined)
654
- return Promise.resolve(this.#lazySelectors);
655
- return this.#lazySelectorsPromise ??= import("../../integrations/pi/components/lazy-selectors.js").then(module => {
656
- this.#lazySelectors = module.piShellLazySelectors;
657
- return module.piShellLazySelectors;
658
- });
659
- }
660
540
  async showThinkingSelector() {
661
541
  const snapshot = this.backend.settings.snapshot();
662
542
  const close = () => {
@@ -670,7 +550,7 @@ export class OwnedUiSessionPresenter {
670
550
  };
671
551
  const releasePresentation = this.#customViewport ? this.#terminal.beginPresentationHold() : undefined;
672
552
  try {
673
- const selectors = await this.#loadLazySelectors();
553
+ const selectors = await this.#presenters.components.dialogs.lazySelectors();
674
554
  const component = await selectors.createThinking({
675
555
  currentLevel: snapshot.thinkingLevel,
676
556
  availableLevels: snapshot.availableThinkingLevels,
@@ -678,7 +558,7 @@ export class OwnedUiSessionPresenter {
678
558
  onCancel: close,
679
559
  onSelectAsDefault: level => this.backend.settings.setDefaultThinkingLevel(level),
680
560
  defaultLevel: snapshot.defaultThinkingLevel,
681
- ...(this.#customViewport ? {
561
+ ...(this.backend.settings.productMode === "bare" ? {
682
562
  presentation: {
683
563
  profile: "bare",
684
564
  cycleBinding: this.root.editor.keybindingConfig()["app.thinking.cycle"] ?? [],
@@ -698,11 +578,11 @@ export class OwnedUiSessionPresenter {
698
578
  /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
699
579
  showSkillsSelector(skills = this.#skillSummaries()) {
700
580
  const close = this.#dialogs.closeInputSurface;
701
- const component = createPiShellSkillsSelector({
581
+ const component = this.#presenters.components.dialogs.skillsSelector({
702
582
  skills,
703
583
  onSelect: name => {
704
584
  close();
705
- void this.#submission.sendSlashPrompt(skillPrompt(name), skillPrompt(name)).catch(() => this.#submission.reportError());
585
+ void this.#submission.sendSlashPrompt(this.#presenters.components.skills.prompt(name), this.#presenters.components.skills.prompt(name)).catch(() => this.#submission.reportError());
706
586
  },
707
587
  onCancel: close,
708
588
  });
@@ -715,12 +595,12 @@ export class OwnedUiSessionPresenter {
715
595
  showForkSelector() {
716
596
  const options = this.backend.catalog.pinnedForkOptions();
717
597
  if (options.length === 0) {
718
- this.root.appendWorkflowResult({ command: "fork", outcome: "completed", message: "No messages to fork from", messageKind: "status" });
598
+ this.root.workflow.appendResult({ command: "fork", outcome: "completed", message: "No messages to fork from", messageKind: "status" });
719
599
  this.#terminal.requestRender();
720
600
  return;
721
601
  }
722
602
  const close = this.#dialogs.closeInputSurface;
723
- const component = createPiShellUserMessageSelector(options, selection => {
603
+ const component = this.#presenters.components.dialogs.userMessageSelector(options, selection => {
724
604
  close();
725
605
  void this.runWorkflow({ command: "fork", argument: "", selection });
726
606
  }, close);
@@ -733,7 +613,7 @@ export class OwnedUiSessionPresenter {
733
613
  options = await this.backend.catalog.pinnedLogoutOptions();
734
614
  }
735
615
  catch (error) {
736
- this.root.appendWorkflowResult({
616
+ this.root.workflow.appendResult({
737
617
  command: "logout",
738
618
  outcome: "failed",
739
619
  message: `Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`,
@@ -742,12 +622,12 @@ export class OwnedUiSessionPresenter {
742
622
  return;
743
623
  }
744
624
  if (options.length === 0) {
745
- this.root.appendWorkflowStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
625
+ this.root.workflow.appendStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
746
626
  this.#terminal.requestRender();
747
627
  return;
748
628
  }
749
629
  const close = this.#dialogs.closeInputSurface;
750
- const component = createPiShellAuthProviderSelector("logout", options, selection => {
630
+ const component = this.#presenters.components.dialogs.authProviderSelector("logout", options, selection => {
751
631
  close();
752
632
  void this.runWorkflow({ command: "logout", argument: "", selection });
753
633
  }, close);
@@ -768,7 +648,7 @@ export class OwnedUiSessionPresenter {
768
648
  }
769
649
  const close = this.#dialogs.closeInputSurface;
770
650
  const labels = method.options.map(option => option.label);
771
- const component = createPiShellExtensionSelector(method.title, labels, label => {
651
+ const component = this.#presenters.components.dialogs.extensionSelector(method.title, labels, label => {
772
652
  const selection = method.options.find(option => option.label === label)?.id;
773
653
  if (!selection)
774
654
  return;
@@ -786,7 +666,7 @@ export class OwnedUiSessionPresenter {
786
666
  showLoginAuthTypeSelector() {
787
667
  const close = this.#dialogs.closeInputSurface;
788
668
  const labels = ["Sign in with an account", "Sign in with an API key"];
789
- const component = createPiShellExtensionSelector("Select authentication method:", labels, label => {
669
+ const component = this.#presenters.components.dialogs.extensionSelector("Select authentication method:", labels, label => {
790
670
  close();
791
671
  this.showLoginProviderSelector(label === labels[0] ? "oauth" : "api_key");
792
672
  }, close);
@@ -796,12 +676,12 @@ export class OwnedUiSessionPresenter {
796
676
  showLoginProviderSelector(authType, initialSearchInput) {
797
677
  const options = this.backend.catalog.pinnedLoginOptions(authType);
798
678
  if (options.length === 0) {
799
- this.root.appendWorkflowStatus(authType === "oauth" ? "No account providers available." : authType === "api_key" ? "No API key providers available." : "No login providers available.");
679
+ this.root.workflow.appendStatus(authType === "oauth" ? "No account providers available." : authType === "api_key" ? "No API key providers available." : "No login providers available.");
800
680
  this.#terminal.requestRender();
801
681
  return;
802
682
  }
803
683
  const close = this.#dialogs.closeInputSurface;
804
- const component = createPiShellAuthProviderSelector("login", options, id => {
684
+ const component = this.#presenters.components.dialogs.authProviderSelector("login", options, id => {
805
685
  close();
806
686
  void this.runWorkflow({ command: "login", argument: "", selection: id });
807
687
  }, () => {
@@ -860,7 +740,7 @@ export class OwnedUiSessionPresenter {
860
740
  }).catch(error => {
861
741
  if (this.#disposed)
862
742
  return;
863
- this.root.appendWorkflowResult({ command: "name", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
743
+ this.root.workflow.appendResult({ command: "name", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
864
744
  this.#terminal.requestRender();
865
745
  });
866
746
  return { outcome: "completed", diagnostic: null };
@@ -870,7 +750,7 @@ export class OwnedUiSessionPresenter {
870
750
  const setup = this.backend.catalog.pinnedAmbientAuthentication(request.selection);
871
751
  if (setup) {
872
752
  const close = this.#dialogs.closeInputSurface;
873
- const dialog = createPiShellLoginDialog(componentRuntime(this.#terminal), setup.providerId, close, setup.providerName, setup.title);
753
+ const dialog = this.#presenters.components.dialogs.loginDialog(componentRuntime(this.#terminal), setup.providerId, close, setup.providerName, setup.title);
874
754
  dialog.showInfo(setup.message, [], true);
875
755
  this.root.setInputSurface(dialog);
876
756
  this.#terminal.requestRender();
@@ -883,7 +763,7 @@ export class OwnedUiSessionPresenter {
883
763
  if (request.command === "reload") {
884
764
  const blocked = this.backend.workflows.reloadBlockedResult();
885
765
  if (blocked) {
886
- this.root.appendWorkflowResult(blocked);
766
+ this.root.workflow.appendResult(blocked);
887
767
  this.#terminal.requestRender();
888
768
  return workflowAdapterResult(blocked);
889
769
  }
@@ -893,8 +773,8 @@ export class OwnedUiSessionPresenter {
893
773
  ? await import("./share-operation.js")
894
774
  : undefined;
895
775
  const shareSurface = shareDialog === undefined ? undefined
896
- : (this.#customViewport ? shareDialog.createPiShellShareOperationDialog : shareDialog.createPiShellOperationLoader)(componentRuntime(this.#terminal), "Creating gist…");
897
- const operationSurface = shareSurface ?? (request.command === "reload" ? createPiShellReloadBox() : undefined);
776
+ : await this.#presenters.components.dialogs.shareProgress(componentRuntime(this.#terminal), "Creating gist…", this.#customViewport ? "dialog" : "loader");
777
+ const operationSurface = shareSurface ?? (request.command === "reload" ? this.#presenters.components.info.reloadBox() : undefined);
898
778
  const now = this.#reloadPresentation?.now ?? Date.now;
899
779
  const shownAt = now();
900
780
  if (operationSurface) {
@@ -925,7 +805,7 @@ export class OwnedUiSessionPresenter {
925
805
  if (result.outcome === "requires-selection" && request.command === "model") {
926
806
  if (result.messages !== undefined) {
927
807
  for (const message of result.messages)
928
- this.root.appendWorkflowMessage(message);
808
+ this.root.workflow.appendMessage(message);
929
809
  }
930
810
  this.showModelSelector((result.detail ?? request.argument.trim()) || undefined);
931
811
  return { outcome: "completed", diagnostic: null };
@@ -944,7 +824,7 @@ export class OwnedUiSessionPresenter {
944
824
  });
945
825
  }
946
826
  if (result.outcome === "requires-selection" || result.outcome === "requires-confirmation") {
947
- this.root.appendWorkflowResult({ command: request.command, outcome: "failed", message: `Owned controller missing for ${request.command}` });
827
+ this.root.workflow.appendResult({ command: request.command, outcome: "failed", message: `Owned controller missing for ${request.command}` });
948
828
  this.#terminal.requestRender();
949
829
  return { outcome: "failed", diagnostic: `Owned controller missing for ${request.command}` };
950
830
  }
@@ -959,10 +839,10 @@ export class OwnedUiSessionPresenter {
959
839
  this.root.resumeViewportFollowing();
960
840
  }
961
841
  if (shareDialog !== undefined && this.#customViewport && result.outcome === "completed" && result.detail !== undefined) {
962
- this.root.appendWorkflowStatus(shareDialog.renderPiShellShareResult(result.message, result.detail));
842
+ this.root.workflow.appendStatus(shareDialog.renderShellShareResult(result.message, result.detail, this.#presenters.theme.current(), url => this.#presenters.components.text.hyperlink(url)));
963
843
  }
964
844
  else {
965
- this.root.appendWorkflowResult(result);
845
+ this.root.workflow.appendResult(result);
966
846
  }
967
847
  this.#terminal.requestRender();
968
848
  return workflowAdapterResult(result);
@@ -978,17 +858,17 @@ export class OwnedUiSessionPresenter {
978
858
  showTrustSelector() {
979
859
  const context = this.backend.catalog.pinnedProjectTrustContext();
980
860
  const close = this.#dialogs.closeInputSurface;
981
- const component = createPiShellTrustSelector({
861
+ const component = this.#presenters.components.dialogs.trustSelector({
982
862
  ...context,
983
863
  onSelect: selection => {
984
864
  try {
985
865
  this.backend.catalog.persistProjectTrust(selection.updates);
986
866
  close();
987
- this.root.appendWorkflowStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
867
+ this.root.workflow.appendStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
988
868
  }
989
869
  catch (error) {
990
870
  close();
991
- this.root.appendWorkflowResult({ command: "trust", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
871
+ this.root.workflow.appendResult({ command: "trust", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
992
872
  }
993
873
  this.#terminal.requestRender();
994
874
  },
@@ -1002,7 +882,7 @@ export class OwnedUiSessionPresenter {
1002
882
  }
1003
883
  // Invariant: bare A1 routes `models`; the comparison profile keeps the pinned `model`/`scoped-models` pair. Hidden routes are shared.
1004
884
  #isWorkflowRoute(value) {
1005
- return workflowCommandNames(this.#customViewport ? "bare" : "comparison").includes(value)
885
+ return this.#presenters.components.workflows.commandNames(this.backend.settings.productMode).includes(value)
1006
886
  || OWNED_UI_HIDDEN_COMMAND_NAMES.includes(value);
1007
887
  }
1008
888
  /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
@@ -1039,8 +919,7 @@ export class OwnedUiSessionPresenter {
1039
919
  // Invariant: events stop first, so nothing re-enters the view during history close, outro, or restoration.
1040
920
  attempt(() => this.#unsubscribe());
1041
921
  this.#copyIntentSequence += 1;
1042
- this.#responseCopy?.dispose();
1043
- this.#copyExecutor?.dispose();
922
+ this.#clipboard.dispose();
1044
923
  this.#submission.cancelWaiting();
1045
924
  let pasteCleanup = Promise.resolve();
1046
925
  let historyCleanup = Promise.resolve(true);
@@ -1049,10 +928,7 @@ export class OwnedUiSessionPresenter {
1049
928
  attempt(() => this.root.clearViewportPointerState());
1050
929
  attempt(() => this.#streamPresentation.dispose());
1051
930
  attempt(() => this.#promptSuggestions?.dispose());
1052
- attempt(() => this.#unsubscribePromptSuggestions());
1053
- attempt(() => this.#unsubscribePromptImages());
1054
- attempt(() => this.#unsubscribeSkills());
1055
- attempt(() => this.#unsubscribeSettings());
931
+ attempt(() => this.#unbindShellSettings());
1056
932
  let exitText = "";
1057
933
  attempt(() => {
1058
934
  exitText = composeExitText({
@@ -1060,7 +936,7 @@ export class OwnedUiSessionPresenter {
1060
936
  customViewport: this.#customViewport,
1061
937
  exitMode: this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput,
1062
938
  resume: this.backend.identity.currentSessionResumeMetadata(),
1063
- transcript: () => this.root.exitTranscript(terminal.columns),
939
+ transcript: () => this.root.transcript.exitTranscript(terminal.columns),
1064
940
  });
1065
941
  });
1066
942
  attempt(() => this.#unbindClipboardWriter());
@@ -1088,7 +964,7 @@ export class OwnedUiSessionPresenter {
1088
964
  }
1089
965
  // Invariant: incremental block updates notify listeners with the same complete view contract.
1090
966
  #syncBlock(block) {
1091
- this.root.applyTranscriptBlock(block);
967
+ this.root.transcript.apply(block);
1092
968
  if (this.#customViewport && block.status === "live" && isCoalescedStreamBlock(block.kind)) {
1093
969
  this.#streamPresentation.request();
1094
970
  }
@@ -1105,7 +981,7 @@ export class OwnedUiSessionPresenter {
1105
981
  }
1106
982
  // Rationale: extensions reach the shell only through this bridge; every surface they draw lands in the root.
1107
983
  #createExtensionBridge() {
1108
- return createPiExtensionUiBridge({
984
+ return this.#presenters.components.dialogs.extensionUiBridge({
1109
985
  runtime: componentRuntime(this.#terminal),
1110
986
  agentDir: this.backend.identity.agentDir,
1111
987
  setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
@@ -1134,7 +1010,7 @@ export class OwnedUiSessionPresenter {
1134
1010
  backend: this.backend,
1135
1011
  root: this.root,
1136
1012
  terminal: this.#terminal,
1137
- cwd,
1013
+ cwd, statusText: this.#presenters.components.info.statusText,
1138
1014
  promptHistory: () => this.#promptHistory,
1139
1015
  imageSidecar: () => this.#historyBinding?.imageSidecar,
1140
1016
  disposed: () => this.#disposed,
@@ -1148,7 +1024,7 @@ export class OwnedUiSessionPresenter {
1148
1024
  // Invariant: the order is the reset order on session replacement; copy intent before pastes before the input surface.
1149
1025
  #createConcerns() {
1150
1026
  return new SessionConcerns([
1151
- { onSessionReplaced: () => { this.#copyIntentSequence += 1; this.#responseCopy?.reset(); } },
1027
+ { onSessionReplaced: () => { this.#copyIntentSequence += 1; this.#clipboard.reset(); } },
1152
1028
  this.#submission,
1153
1029
  { onSessionReplaced: () => this.root.resetPendingPastes() },
1154
1030
  { onSessionReplaced: () => this.#promptSuggestions?.invalidate() },
@@ -1171,7 +1047,7 @@ export class OwnedUiSessionPresenter {
1171
1047
  return;
1172
1048
  if (replaced) {
1173
1049
  this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
1174
- this.root.resetTranscript();
1050
+ this.root.transcript.reset();
1175
1051
  }
1176
1052
  const binding = this.#historyBinding;
1177
1053
  if (binding?.options.forSession !== undefined || binding?.options.forScope !== undefined) {
@@ -1254,7 +1130,7 @@ export class OwnedUiSessionPresenter {
1254
1130
  if (diagnostic === undefined || !this.#terminal.running)
1255
1131
  return;
1256
1132
  this.#startupTrustHandled = true;
1257
- this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
1133
+ this.root.workflow.appendMessage({ kind: "warning", message: diagnostic.message });
1258
1134
  }
1259
1135
  openOwnedRoute(route) {
1260
1136
  if (this.#disposed)
@@ -1267,7 +1143,7 @@ export class OwnedUiSessionPresenter {
1267
1143
  }
1268
1144
  const result = this.#dialogs.openRoute(route);
1269
1145
  if (result.outcome !== "completed" && result.diagnostic !== null) {
1270
- this.root.appendWorkflowMessage({ kind: "warning", message: result.diagnostic });
1146
+ this.root.workflow.appendMessage({ kind: "warning", message: result.diagnostic });
1271
1147
  }
1272
1148
  }
1273
1149
  hotkeysPresentation() {
@@ -1283,9 +1159,9 @@ export class OwnedUiSessionPresenter {
1283
1159
  if (this.#isWorkflowRoute(name))
1284
1160
  return this.runWorkflow({ command: name, argument });
1285
1161
  if (this.#skillsCollapsed()) {
1286
- if (name === SKILLS_COMMAND_NAME)
1162
+ if (name === this.#presenters.components.skills.commandName)
1287
1163
  return this.#runSkillsCommand(text, argument);
1288
- const rewritten = rewriteSkillsTunnelSubmission(text);
1164
+ const rewritten = this.#presenters.components.skills.rewriteTunnelSubmission(text);
1289
1165
  if (rewritten !== text)
1290
1166
  return this.#submission.sendSlashPrompt(rewritten, text);
1291
1167
  }
@@ -1305,7 +1181,7 @@ export class OwnedUiSessionPresenter {
1305
1181
  && !this.backend.identity.disposed && this.backend.settings.snapshot().enableSkillCommands;
1306
1182
  }
1307
1183
  #skillSummaries() {
1308
- return skillsFromCommands(this.backend.workflows.workflowAutocompleteCommands());
1184
+ return this.#presenters.components.skills.fromCommands(this.backend.workflows.workflowAutocompleteCommands());
1309
1185
  }
1310
1186
  // Protocol: bare "/skills" opens the dialog, "/skills <name> [args]" applies directly, and an unknown name is a command outcome.
1311
1187
  async #runSkillsCommand(text, argument) {
@@ -1317,18 +1193,18 @@ export class OwnedUiSessionPresenter {
1317
1193
  }
1318
1194
  const separator = trimmed.search(/\s/u);
1319
1195
  const token = separator < 0 ? trimmed : trimmed.slice(0, separator);
1320
- const skill = findSkillByArgument(skills, token);
1196
+ const skill = this.#presenters.components.skills.findByArgument(skills, token);
1321
1197
  if (skill === undefined) {
1322
1198
  const message = "Unknown skill: " + token;
1323
- this.root.appendWorkflowMessage({ kind: "error", message });
1199
+ this.root.workflow.appendMessage({ kind: "error", message });
1324
1200
  this.#terminal.requestRender();
1325
1201
  return { outcome: "failed", diagnostic: message };
1326
1202
  }
1327
- return this.#submission.sendSlashPrompt(skillPrompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1203
+ return this.#submission.sendSlashPrompt(this.#presenters.components.skills.prompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1328
1204
  }
1329
1205
  #treeSummary() {
1330
1206
  return new Promise(resolve => {
1331
- this.root.setInputSurface(createPiShellExtensionSelector("Summarize Branch?", ["No summary", "Summarize", "Summarize with custom prompt"], resolve, () => resolve(undefined)), true, "opaque");
1207
+ this.root.setInputSurface(this.#presenters.components.dialogs.extensionSelector("Summarize Branch?", ["No summary", "Summarize", "Summarize with custom prompt"], resolve, () => resolve(undefined)), true, "opaque");
1332
1208
  this.#terminal.requestRender();
1333
1209
  });
1334
1210
  }