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

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 (296) 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 +13 -14
  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/resource-labels.d.ts +2 -0
  39. package/dist/app/session-shell/resource-labels.js +158 -0
  40. package/dist/app/session-shell/response-copy-protocol.d.ts +3 -2
  41. package/dist/app/session-shell/response-copy-transport.d.ts +2 -14
  42. package/dist/app/session-shell/response-copy-transport.js +1 -1
  43. package/dist/app/session-shell/session-lifecycle.d.ts +2 -1
  44. package/dist/app/session-shell/session-prompt-history.d.ts +29 -0
  45. package/dist/app/session-shell/session-prompt-history.js +80 -0
  46. package/dist/app/session-shell/session-selectors.d.ts +46 -0
  47. package/dist/app/session-shell/session-selectors.js +269 -0
  48. package/dist/app/session-shell/session-shell-root.d.ts +76 -113
  49. package/dist/app/session-shell/session-shell-root.js +103 -329
  50. package/dist/app/session-shell/session-shell.d.ts +14 -37
  51. package/dist/app/session-shell/session-shell.js +177 -612
  52. package/dist/app/session-shell/session-viewport-controller.d.ts +9 -16
  53. package/dist/app/session-shell/session-viewport-controller.js +42 -320
  54. package/dist/app/session-shell/settings-bindings.d.ts +25 -3
  55. package/dist/app/session-shell/settings-bindings.js +26 -2
  56. package/dist/app/session-shell/share-operation.d.ts +3 -3
  57. package/dist/app/session-shell/share-operation.js +4 -6
  58. package/dist/app/session-shell/shell-clipboard.d.ts +59 -0
  59. package/dist/app/session-shell/shell-clipboard.js +119 -0
  60. package/dist/app/session-shell/shell-editor-factory.d.ts +8 -4
  61. package/dist/app/session-shell/shell-editor-factory.js +13 -15
  62. package/dist/app/session-shell/shell-host.d.ts +16 -17
  63. package/dist/app/session-shell/shell-host.js +18 -19
  64. package/dist/app/session-shell/shell-layout.d.ts +2 -2
  65. package/dist/app/session-shell/shell-prompt-suggestions.d.ts +26 -0
  66. package/dist/app/session-shell/shell-prompt-suggestions.js +48 -0
  67. package/dist/app/session-shell/suggestion-surface.d.ts +1 -6
  68. package/dist/app/session-shell/suggestion-surface.js +1 -1
  69. package/dist/app/session-shell/system-clipboard.d.ts +2 -14
  70. package/dist/app/session-shell/system-clipboard.js +2 -7
  71. package/dist/app/session-shell/transcript-document.d.ts +11 -8
  72. package/dist/app/session-shell/transcript-document.js +13 -14
  73. package/dist/app/session-shell/viewport-layout.d.ts +2 -2
  74. package/dist/app/session-shell/viewport-pointer.d.ts +83 -0
  75. package/dist/app/session-shell/viewport-pointer.js +361 -0
  76. package/dist/app/session-shell/workflow-output.d.ts +5 -3
  77. package/dist/app/session-shell/workflow-output.js +13 -15
  78. package/dist/cli/package-messages.d.ts +0 -1
  79. package/dist/cli/package-messages.js +1 -1
  80. package/dist/cli/session-context.d.ts +1 -1
  81. package/dist/cli/uninstall.d.ts +3 -2
  82. package/dist/composition/owned-settings.d.ts +2 -1
  83. package/dist/composition/owned-settings.js +0 -3
  84. package/dist/composition/owned-ui.d.ts +1 -1
  85. package/dist/composition/owned-ui.js +77 -73
  86. package/dist/composition/reference-selection-options.d.ts +1 -1
  87. package/dist/composition/reference-selection-options.js +1 -1
  88. package/dist/composition/session-runtime-registry.d.ts +2 -1
  89. package/dist/composition/settings-route-host.js +3 -10
  90. package/dist/composition/settings-search-input.d.ts +2 -1
  91. package/dist/composition/startup-route.d.ts +2 -2
  92. package/dist/composition/startup-route.js +2 -2
  93. package/dist/contracts/owned-ui/extension-ui.d.ts +2 -1
  94. package/dist/contracts/owned-ui/index.d.ts +2 -1
  95. package/dist/contracts/owned-ui/index.js +1 -1
  96. package/dist/contracts/owned-ui/session-presenters.d.ts +3 -0
  97. package/dist/contracts/owned-ui/shell-components.d.ts +543 -0
  98. package/dist/contracts/owned-ui/validation.d.ts +1 -11
  99. package/dist/contracts/owned-ui/validation.js +8 -104
  100. package/dist/contracts/presentation/component.d.ts +17 -0
  101. package/dist/contracts/presentation/component.js +8 -0
  102. package/dist/contracts/presentation/index.d.ts +6 -15
  103. package/dist/contracts/presentation/index.js +2 -0
  104. package/dist/contracts/presentation/terminal.d.ts +284 -0
  105. package/dist/contracts/presentation/terminal.js +1 -0
  106. package/dist/contracts/presentation/theme.d.ts +46 -0
  107. package/dist/contracts/presentation/theme.js +1 -0
  108. package/dist/features/launch/development-launch.js +1 -1
  109. package/dist/features/launch/runtime-selection.d.ts +2 -2
  110. package/dist/features/launch/runtime-selection.js +2 -2
  111. package/dist/features/owned-ui/project-trust-prompt.d.ts +2 -2
  112. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  113. package/dist/features/owned-ui/release-note-state.d.ts +2 -1
  114. package/dist/features/owned-ui/settings-app.js +49 -425
  115. package/dist/features/owned-ui/settings-edits.d.ts +31 -0
  116. package/dist/features/owned-ui/settings-edits.js +139 -0
  117. package/dist/features/owned-ui/settings-model-dialog.d.ts +41 -0
  118. package/dist/features/owned-ui/settings-model-dialog.js +194 -0
  119. package/dist/features/owned-ui/settings-scroll-rail.d.ts +29 -0
  120. package/dist/features/owned-ui/settings-scroll-rail.js +79 -0
  121. package/dist/features/owned-ui/settings-search-input.d.ts +2 -1
  122. package/dist/features/prompt-history/service.d.ts +3 -2
  123. package/dist/foundation/lifecycle/claim-gate.d.ts +2 -1
  124. package/dist/foundation/release/automatic-update.d.ts +5 -14
  125. package/dist/foundation/release/automatic-update.js +15 -22
  126. package/dist/foundation/release/bootstrap.js +4 -4
  127. package/dist/foundation/release/dependency-certification.d.ts +0 -1
  128. package/dist/foundation/release/dependency-certification.js +1 -1
  129. package/dist/foundation/release/dependency-layer.js +3 -3
  130. package/dist/foundation/release/early-accent.d.ts +2 -2
  131. package/dist/foundation/release/early-accent.js +1 -1
  132. package/dist/foundation/release/index.d.ts +2 -2
  133. package/dist/foundation/release/index.js +1 -1
  134. package/dist/foundation/release/latest-release.d.ts +3 -18
  135. package/dist/foundation/release/latest-release.js +11 -11
  136. package/dist/foundation/release/managed-path.d.ts +10 -0
  137. package/dist/foundation/release/managed-path.js +26 -0
  138. package/dist/foundation/release/ordinary-launch-selection.d.ts +1 -1
  139. package/dist/foundation/release/ordinary-launch-selection.js +1 -1
  140. package/dist/foundation/release/release-gc.d.ts +3 -3
  141. package/dist/foundation/release/release-gc.js +10 -12
  142. package/dist/foundation/release/{cohort-state.d.ts → release-state.d.ts} +23 -23
  143. package/dist/foundation/release/{cohort-state.js → release-state.js} +8 -8
  144. package/dist/foundation/release/restart-certification.js +3 -4
  145. package/dist/foundation/release/update-activation.js +3 -3
  146. package/dist/foundation/release/update-execution-lease.d.ts +2 -3
  147. package/dist/foundation/release/update-execution-lease.js +2 -8
  148. package/dist/foundation/release/update-installer.d.ts +45 -0
  149. package/dist/foundation/release/update-installer.js +57 -0
  150. package/dist/foundation/release/update-launch.d.ts +2 -2
  151. package/dist/foundation/release/update-recovery.js +2 -9
  152. package/dist/foundation/release/update.d.ts +4 -25
  153. package/dist/foundation/release/update.js +164 -183
  154. package/dist/foundation/startup/startup-budget.d.ts +5 -5
  155. package/dist/foundation/startup/startup-budget.js +2 -2
  156. package/dist/integrations/pi/components/bracketed-paste.d.ts +33 -0
  157. package/dist/integrations/pi/components/bracketed-paste.js +123 -0
  158. package/dist/integrations/pi/components/editor-interaction.d.ts +4 -12
  159. package/dist/integrations/pi/components/index.d.ts +15 -9
  160. package/dist/integrations/pi/components/index.js +9 -5
  161. package/dist/integrations/pi/components/lazy-selectors.d.ts +2 -10
  162. package/dist/integrations/pi/components/lazy-selectors.js +1 -1
  163. package/dist/integrations/pi/components/modal-frame.d.ts +5 -22
  164. package/dist/integrations/pi/components/modal-frame.js +3 -23
  165. package/dist/integrations/pi/components/owned-editor-ux.d.ts +4 -3
  166. package/dist/integrations/pi/components/owned-editor-ux.js +46 -140
  167. package/dist/integrations/pi/components/prompt-input-port.d.ts +2 -19
  168. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -1
  169. package/dist/integrations/pi/components/shell-editor-autocomplete.d.ts +3 -2
  170. package/dist/integrations/pi/components/shell-footer-status.d.ts +2 -2
  171. package/dist/integrations/pi/components/shell-footer-status.js +3 -2
  172. package/dist/integrations/pi/components/shell-presenters-info.d.ts +6 -5
  173. package/dist/integrations/pi/components/shell-presenters-info.js +2 -1
  174. package/dist/integrations/pi/components/shell-session-info-reference.d.ts +2 -1
  175. package/dist/integrations/pi/components/shell-shared-facade.d.ts +33 -227
  176. package/dist/integrations/pi/components/shell-shared-facade.js +8 -1
  177. package/dist/integrations/pi/components/skills-command.d.ts +1 -9
  178. package/dist/integrations/pi/components/skills-command.js +5 -5
  179. package/dist/integrations/pi/components/theme.d.ts +9 -2
  180. package/dist/integrations/pi/components/theme.js +17 -2
  181. package/dist/integrations/pi/components/thinking-selector-dialog.d.ts +2 -1
  182. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +2 -2
  183. package/dist/integrations/pi/engine/adapter.d.ts +4 -13
  184. package/dist/integrations/pi/engine/adapter.js +9 -61
  185. package/dist/integrations/pi/engine/changelog.d.ts +2 -2
  186. package/dist/integrations/pi/engine/changelog.js +3 -3
  187. package/dist/integrations/pi/engine/compaction-progress.d.ts +2 -3
  188. package/dist/integrations/pi/engine/compaction-progress.js +1 -1
  189. package/dist/integrations/pi/engine/conformance.d.ts +2 -1
  190. package/dist/integrations/pi/engine/default-model-setting.d.ts +0 -2
  191. package/dist/integrations/pi/engine/default-model-setting.js +1 -2
  192. package/dist/integrations/pi/engine/event-delivery.d.ts +2 -1
  193. package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -1
  194. package/dist/integrations/pi/engine/host.js +3 -3
  195. package/dist/integrations/pi/engine/message-values.d.ts +0 -1
  196. package/dist/integrations/pi/engine/message-values.js +1 -1
  197. package/dist/integrations/pi/engine/pending-delivery.d.ts +0 -4
  198. package/dist/integrations/pi/engine/pending-delivery.js +3 -3
  199. package/dist/integrations/pi/engine/ports.d.ts +7 -1
  200. package/dist/integrations/pi/engine/ports.js +4 -0
  201. package/dist/integrations/pi/engine/project-trust-preflight.d.ts +2 -1
  202. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +3 -2
  203. package/dist/integrations/pi/engine/provider-authentication.d.ts +1 -1
  204. package/dist/integrations/pi/engine/public-main-entry.d.ts +1 -5
  205. package/dist/integrations/pi/engine/public-main-entry.js +1 -1
  206. package/dist/integrations/pi/engine/repository-metadata.d.ts +49 -0
  207. package/dist/integrations/pi/engine/repository-metadata.js +189 -0
  208. package/dist/integrations/pi/engine/repository-pr-rest.d.ts +1 -9
  209. package/dist/integrations/pi/engine/repository-pr-rest.js +2 -2
  210. package/dist/integrations/pi/engine/repository-pr.d.ts +1 -3
  211. package/dist/integrations/pi/engine/repository-pr.js +1 -1
  212. package/dist/integrations/pi/engine/resource-catalog.d.ts +2 -1
  213. package/dist/integrations/pi/engine/session-integration.d.ts +1 -1
  214. package/dist/integrations/pi/engine/session-runtime.d.ts +7 -12
  215. package/dist/integrations/pi/engine/session-runtime.js +21 -178
  216. package/dist/integrations/pi/engine/settings-bridge.d.ts +0 -2
  217. package/dist/integrations/pi/engine/settings-bridge.js +1 -1
  218. package/dist/integrations/pi/engine/settings-port.d.ts +3 -2
  219. package/dist/integrations/pi/engine/transcript-projection.d.ts +2 -1
  220. package/dist/integrations/pi/engine/view-state.d.ts +21 -2
  221. package/dist/integrations/pi/engine/view-state.js +129 -1
  222. package/dist/integrations/pi/engine/workflow-contexts.d.ts +1 -1
  223. package/dist/integrations/pi/engine/workflow-runner.d.ts +1 -2
  224. package/dist/integrations/pi/engine/workflow-runner.js +254 -365
  225. package/dist/integrations/pi/engine/workflow-share.d.ts +9 -0
  226. package/dist/integrations/pi/engine/workflow-share.js +168 -0
  227. package/dist/integrations/pi/engine/workflow-support.d.ts +0 -20
  228. package/dist/integrations/pi/engine/workflow-support.js +4 -65
  229. package/dist/integrations/pi/engine/workflows.d.ts +5 -5
  230. package/dist/integrations/pi/session-presenters/components.d.ts +3 -0
  231. package/dist/integrations/pi/session-presenters/components.js +75 -0
  232. package/dist/integrations/pi/session-presenters/index.d.ts +1 -0
  233. package/dist/integrations/pi/session-presenters/index.js +1 -0
  234. package/dist/integrations/pi/session-presenters/presentation.d.ts +3 -0
  235. package/dist/integrations/pi/session-presenters/presentation.js +10 -0
  236. package/dist/integrations/pi/session-presenters/presenters.d.ts +2 -1
  237. package/dist/integrations/pi/session-presenters/presenters.js +3 -1
  238. package/dist/integrations/pi/tui-runtime/adapter.d.ts +9 -19
  239. package/dist/integrations/pi/tui-runtime/adapter.js +56 -174
  240. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +9 -44
  241. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -1
  242. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  243. package/dist/integrations/pi/tui-runtime/input-presentation-coordinator.d.ts +4 -6
  244. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +4 -10
  245. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +3 -9
  246. package/dist/integrations/pi/tui-runtime/presentation-adapter.d.ts +2 -2
  247. package/dist/integrations/pi/tui-runtime/runtime-factory.d.ts +3 -0
  248. package/dist/integrations/pi/tui-runtime/runtime-factory.js +11 -0
  249. package/dist/ui/components/index.d.ts +2 -2
  250. package/dist/ui/components/index.js +1 -1
  251. package/dist/ui/components/selection-copy.d.ts +0 -1
  252. package/dist/ui/components/selection-copy.js +0 -12
  253. package/dist/ui/components/theme.d.ts +4 -19
  254. package/dist/ui/components/theme.js +6 -0
  255. package/dist/ui/components/transcript-viewport-frame.d.ts +120 -0
  256. package/dist/ui/components/transcript-viewport-frame.js +314 -0
  257. package/dist/ui/components/transcript-viewport-types.d.ts +147 -0
  258. package/dist/ui/components/transcript-viewport-types.js +1 -0
  259. package/dist/ui/components/transcript-viewport.d.ts +2 -147
  260. package/dist/ui/components/transcript-viewport.js +87 -315
  261. package/dist/ui/settings/declarations.d.ts +2 -3
  262. package/dist/ui/settings/declarations.js +2 -3
  263. package/dist/ui/settings/early-accent.js +1 -1
  264. package/dist/ui/settings/index.d.ts +3 -5
  265. package/dist/ui/settings/index.js +2 -3
  266. package/dist/ui/settings/manager.d.ts +0 -4
  267. package/dist/ui/settings/manager.js +6 -62
  268. package/dist/ui/settings/resolution.d.ts +6 -18
  269. package/dist/ui/settings/resolution.js +4 -103
  270. package/dist/ui/settings/sections.d.ts +0 -4
  271. package/dist/ui/settings/sections.js +1 -1
  272. package/docs/architecture/boundaries.md +2 -2
  273. package/docs/architecture/internal-naming.md +2 -0
  274. package/docs/architecture/project-structure.md +4 -2
  275. package/docs/validation.md +1 -1
  276. package/package.json +3 -4
  277. package/dist/integrations/pi/components/components.d.ts +0 -4
  278. package/dist/integrations/pi/components/components.js +0 -48
  279. package/dist/integrations/pi/components/conformance.d.ts +0 -18
  280. package/dist/integrations/pi/components/conformance.js +0 -70
  281. package/dist/integrations/pi/components/shell-components.d.ts +0 -7
  282. package/dist/integrations/pi/components/shell-components.js +0 -7
  283. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +0 -33
  284. package/dist/integrations/pi/components/upstream/theme/theme-controller.js +0 -114
  285. package/dist/integrations/pi/engine/diagnostics-state.d.ts +0 -9
  286. package/dist/integrations/pi/engine/diagnostics-state.js +0 -34
  287. package/dist/integrations/pi/engine/usage-view.d.ts +0 -4
  288. package/dist/integrations/pi/engine/usage-view.js +0 -56
  289. package/dist/integrations/pi/tui-runtime/contracts.d.ts +0 -166
  290. package/dist/ui/settings/migrations.d.ts +0 -19
  291. package/dist/ui/settings/migrations.js +0 -225
  292. package/dist/ui/settings/progress-palette.generated.d.ts +0 -139
  293. package/dist/ui/settings/progress-palette.generated.js +0 -139
  294. /package/dist/{integrations/pi/tui-runtime/contracts.js → contracts/owned-ui/shell-components.js} +0 -0
  295. /package/dist/{foundation/release → contracts/presentation}/progress-palette.generated.d.ts +0 -0
  296. /package/dist/{foundation/release → contracts/presentation}/progress-palette.generated.js +0 -0
@@ -1,30 +1,22 @@
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";
4
- import { PromptHistoryController } from "./prompt-history-controller.js";
1
+ import { ShellClipboard } from "./shell-clipboard.js";
2
+ import { SessionPromptHistory } from "./session-prompt-history.js";
3
+ import { shellResourceEntries } from "./resource-labels.js";
5
4
  import { SessionDialogCoordinator } from "./dialog-coordinator.js";
6
5
  import { LoginInteraction } from "./login-interaction.js";
7
6
  import { PromptSubmission } from "./prompt-submission.js";
8
7
  import { SessionConcerns } from "./session-concern.js";
9
8
  import { composeExitText, SessionStopLatch } from "./session-lifecycle.js";
10
9
  import { openWorkflowSelector } from "./workflow-router.js";
11
- import { bindSessionSettings } from "./settings-bindings.js";
12
- import { promptSuggestionSurface, routeResponseToSuggestions, suggestionModelKey, suggestionPresentationBlockReason, } from "./suggestion-surface.js";
10
+ import { SessionSelectors } from "./session-selectors.js";
11
+ import { bindSessionSettings, bindShellSettings } from "./settings-bindings.js";
12
+ import { promptSuggestionSurface, suggestionPresentationBlockReason, } from "./suggestion-surface.js";
13
13
  import { CLEAR_EXIT_INTERVAL_MS, boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
14
14
  import { OWNED_UI_HIDDEN_COMMAND_NAMES } from "../../contracts/owned-ui/index.js";
15
- import { ImageAttachmentError } from "../../contracts/owned-ui/index.js";
16
15
  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";
16
+ import { ShellPromptSuggestions } from "./shell-prompt-suggestions.js";
23
17
  import { OwnedUiTerminalHost, componentRuntime, sessionTerminalHostOptions, } from "./shell-host.js";
24
- import { runImageWorker } from "./image-preparation-client.js";
25
18
  import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
26
- import { writeSystemClipboardText, } from "./system-clipboard.js";
27
- import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
19
+ import { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
28
20
  // Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
29
21
  const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
30
22
  export { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
@@ -37,24 +29,19 @@ export { formatSessionResumeCommand, quoteCommandArgument } from "./session-life
37
29
  export class OwnedUiSessionPresenter {
38
30
  backend;
39
31
  #presenters;
40
- #dialogHost;
32
+ selectors;
41
33
  root;
42
34
  #host;
43
35
  #terminal;
44
36
  #submission;
45
37
  #login;
46
38
  #concerns;
47
- #listeners = new Set();
48
39
  #unsubscribe;
49
- #unsubscribePromptSuggestions;
50
40
  #promptSuggestions;
51
41
  #promptImages;
52
42
  #skills;
53
- #unsubscribePromptImages;
54
- #unsubscribeSkills;
55
43
  #installedCommandSignature = "";
56
- #promptHistory = null;
57
- #historyBinding;
44
+ #history = null;
58
45
  #extensionBridge;
59
46
  #stop = new SessionStopLatch();
60
47
  #dialogs;
@@ -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,10 @@ 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
- // Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
108
- this.#dialogHost = this.#createDialogHost();
109
- this.#lazySelectors = options.diagnostics?.lazySelectors;
110
- let promptSuggestionController = null;
111
88
  // Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
112
89
  this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
113
90
  this.#skills = this.#customViewport ? options.skills ?? null : null;
@@ -115,48 +92,19 @@ export class OwnedUiSessionPresenter {
115
92
  // throw releases them in reverse, so a shell that never finished constructing holds nothing.
116
93
  const constructed = [() => host.disconnect(this)];
117
94
  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, {
95
+ this.#clipboard = new ShellClipboard({
96
+ ownedCopy: this.#customViewport, substituteTerminal: terminal !== undefined,
97
+ ...(sharedClipboard === undefined ? {} : { shared: sharedClipboard }),
98
+ ...(clipboard === undefined ? {} : { clipboard }),
99
+ ...(responseCopy === undefined ? {} : { responseCopy }),
100
+ ...(pastePreparation === undefined ? {} : { pastePreparation }),
101
+ disposed: () => this.#disposed,
102
+ terminalAttached: () => this.#terminal.running && this.#terminal.attached,
103
+ writeControl: control => this.#terminal.writeControl(control),
104
+ });
105
+ constructed.push(() => this.#clipboard.dispose());
106
+ const pastePreparationOptions = this.#clipboard.pastePreparation();
107
+ this.root = new OwnedUiSessionShellRoot(this.backend.session.view(), cwd, { presentation: options.presenters, components: options.presenters.components,
160
108
  getColumns: () => this.#terminal.viewport().columns,
161
109
  getRows: () => this.#terminal.viewport().rows,
162
110
  requestRender: force => this.#terminal.requestRender(force),
@@ -174,7 +122,7 @@ export class OwnedUiSessionPresenter {
174
122
  onInterrupt: () => { void this.interrupt(); },
175
123
  onClear: () => { void this.clearOrExit(); },
176
124
  onExit: () => { void this.shutdown(); },
177
- onModelSelect: () => this.#customViewport ? this.showModelsDialog() : this.showModelSelector(),
125
+ onModelSelect: () => this.#customViewport ? this.selectors.showModelsDialog() : this.selectors.showModelSelector(),
178
126
  onModelCycle: direction => { void this.cycleModel(direction); },
179
127
  onThinkingCycle: () => { void this.cycleThinkingLevel(); },
180
128
  onThinkingToggle: () => {
@@ -186,55 +134,31 @@ export class OwnedUiSessionPresenter {
186
134
  onDequeue: () => this.restoreQueuedInput(),
187
135
  onEditorChange: text => {
188
136
  this.#submission.noteEditorChange();
189
- promptSuggestionController?.abortPending();
137
+ this.#promptSuggestions?.abortPending();
190
138
  if (text.length === 0) {
191
139
  // Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
192
140
  // Reassert retained ghost text in the microtask before Pi emits its scheduled frame.
193
- queueMicrotask(() => promptSuggestionController?.restoreAvailable());
141
+ queueMicrotask(() => this.#promptSuggestions?.restoreAvailable());
194
142
  }
195
143
  this.root.reconcilePromptImageLimitNotice(text);
196
144
  },
197
- onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
145
+ onPromptSuggestionAccepted: () => this.#promptSuggestions?.accept(),
198
146
  onPromptSuggestionEligibilityChanged: () => {
199
- queueMicrotask(() => promptSuggestionController?.restoreAvailable());
147
+ queueMicrotask(() => this.#promptSuggestions?.restoreAvailable());
200
148
  },
201
149
  onInputSurfaceChanged: () => {
202
- promptSuggestionController?.invalidate();
203
- this.#promptHistory?.synchronize();
150
+ this.#promptSuggestions?.invalidate();
151
+ this.#history?.synchronize();
204
152
  },
205
153
  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 };
154
+ const delivery = this.#clipboard.copyText(text);
155
+ if (delivery !== undefined)
156
+ this.#trackCopy(delivery, cut ? undefined : text.length);
233
157
  },
158
+ readClipboardContent: signal => this.#clipboard.readContent(signal),
159
+ captureClipboardPaste: () => this.#clipboard.capturePaste(),
234
160
  ...(pasteDiagnostics === undefined ? {} : { pasteDiagnostics: pasteDiagnostics }),
235
- ...(pastePreparation === undefined && sharedClipboard === undefined ? {} : {
236
- pastePreparation: { ...pastePreparation, ...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.paste }) },
237
- }),
161
+ ...(pastePreparationOptions === undefined ? {} : { pastePreparation: pastePreparationOptions }),
238
162
  ...(sharedKeybindings === undefined ? {} : { keybindings: sharedKeybindings }),
239
163
  ...(this.#promptImages === null ? {} : { promptImageLimit: this.#promptImages.limit }),
240
164
  skillsPresentation: () => this.#skills?.presentation() ?? "expand",
@@ -251,68 +175,43 @@ export class OwnedUiSessionPresenter {
251
175
  this.#dialogs = new SessionDialogCoordinator({
252
176
  terminal: this.#terminal,
253
177
  root: this.root,
254
- routeHost: routeHost ?? null,
178
+ routeHost: routeHost ?? null, dialogs: options.presenters.components.dialogs,
255
179
  disposed: () => this.#disposed,
256
180
  exit: () => { void this.shutdown(); },
257
181
  ...(startupRoute === undefined ? {} : { startupRoute }),
258
182
  });
259
183
  constructed.push(() => this.#dialogs.dispose());
184
+ this.selectors = this.#createSelectors(options.presenters);
260
185
  const initialPiSettings = this.backend.settings.snapshot();
261
- this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
186
+ this.#programStatus = options.presenters.components.chrome.programStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
262
187
  this.#login = new LoginInteraction({
263
- terminal: this.#terminal, root: this.root, programStatus: this.#programStatus,
188
+ terminal: this.#terminal, root: this.root, programStatus: this.#programStatus, dialogs: options.presenters.components.dialogs,
264
189
  syncProgress: () => this.#syncTerminalProgress(this.view()),
265
190
  });
266
191
  this.#submission = this.#createSubmission(cwd);
267
192
  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);
193
+ this.#streamPresentation = new StreamPresentationCoalescer(() => this.#terminal.requestRender(), streamPresentationOptions?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS, streamPresentationOptions?.scheduler);
272
194
  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;
195
+ 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
196
  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())
197
+ this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
198
+ this.#unbindShellSettings = bindShellSettings({
199
+ customViewport: this.#customViewport, promptImages: this.#promptImages, skills: this.#skills,
200
+ viewport: viewportSettings, historyScope: promptHistory?.onScopeChange,
201
+ }, {
202
+ root: this.root,
203
+ promptImageLimitChanged: () => { if (!this.#disposed && this.root.reconcilePromptImageLimitNotice())
204
+ this.#terminal.requestRender(); },
205
+ skillsChanged: () => {
206
+ // Invariant: reinstalling drops extension provider wrappers, so an unrelated setting change leaves the list alone.
207
+ if (this.#disposed || this.#commandListSignature() === this.#installedCommandSignature)
208
+ return;
209
+ this.#installAutocompleteCommands();
288
210
  this.#terminal.requestRender();
211
+ },
212
+ historyScopeChanged: scope => this.#rebindTranscript(scope),
289
213
  });
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);
214
+ constructed.push(this.#unbindShellSettings);
316
215
  this.#unbindSessionSettings = bindSessionSettings(this.backend.settings, initialPiSettings, {
317
216
  terminal: this.#terminal,
318
217
  root: this.root,
@@ -324,17 +223,8 @@ export class OwnedUiSessionPresenter {
324
223
  }, { customViewport: this.#customViewport });
325
224
  constructed.push(this.#unbindSessionSettings);
326
225
  if (this.#customViewport && promptHistory !== undefined) {
327
- this.#historyBinding = { options: promptHistory, scope: promptHistory.scope ?? "unique", sessionId: promptHistory.sessionId, imageSidecar: promptHistory.imageSidecar };
328
- this.#promptHistory = new PromptHistoryController({
329
- editor: this.root.editor,
330
- store: promptHistory.store,
331
- limit: promptHistory.limit,
332
- fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
333
- active: () => this.root.usesDefaultInputSurface(),
334
- render: () => this.#terminal.requestRender(),
335
- rehydrate: value => this.root.rehydrateHistoryText(value, id => this.#historyBinding?.imageSidecar?.readAttachment(id) ?? null),
336
- });
337
- constructed.push(() => { void this.#promptHistory?.close().catch(() => false); });
226
+ this.#history = this.#createHistory(promptHistory);
227
+ constructed.push(() => { void this.#history?.close().catch(() => false); });
338
228
  }
339
229
  this.#extensionBridge = this.#createExtensionBridge();
340
230
  constructed.push(() => this.#extensionBridge.dispose());
@@ -345,64 +235,16 @@ export class OwnedUiSessionPresenter {
345
235
  prompt: request => this.#login.requestInput(request),
346
236
  notify: event => this.#login.notify(event),
347
237
  publish: message => {
348
- this.root.appendWorkflowMessage(message);
238
+ this.root.workflow.appendMessage(message);
349
239
  this.#terminal.requestRender();
350
240
  },
351
241
  finishLogin: () => this.#login.finish(),
352
242
  });
353
243
  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
- });
244
+ this.#unsubscribe = this.backend.session.onEvent(event => this.#onSessionEvent(event));
397
245
  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
- });
246
+ this.#unbindClipboardWriter = this.#clipboard.responseCopy === null ? () => { }
247
+ : this.backend.extensions.bindClipboardWriter(text => this.#clipboard.writeForExtension(text));
406
248
  constructed.push(this.#unbindClipboardWriter);
407
249
  if (this.backend.session.view().lifecycle === "stopped")
408
250
  this.#stop.resolve();
@@ -439,7 +281,7 @@ export class OwnedUiSessionPresenter {
439
281
  return this.root.inputCoordinationSurface();
440
282
  }
441
283
  setOverlaySurfaces(surfaces) {
442
- this.root.setViewportOverlaySurfaces(surfaces);
284
+ this.root.viewport.setOverlaySurfaces(surfaces);
443
285
  }
444
286
  noteImmediatePresentation() {
445
287
  this.#streamPresentation.noteImmediatePresentation();
@@ -482,7 +324,7 @@ export class OwnedUiSessionPresenter {
482
324
  const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.#terminal.hasFocusedOverlay());
483
325
  if (routed.copySelection !== undefined) {
484
326
  const snapshot = routed.copySelection;
485
- this.#trackCopy(this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite), copyCharacters(snapshot));
327
+ this.#trackCopy(this.#clipboard.copySelection(snapshot), copyCharacters(snapshot));
486
328
  }
487
329
  if (routed.copyLastMessage)
488
330
  void this.runWorkflow({ command: "copy", argument: "" });
@@ -490,24 +332,60 @@ export class OwnedUiSessionPresenter {
490
332
  return routed.data === data ? undefined : { data: routed.data };
491
333
  return routed.data.length === 0 ? { consume: true } : { data: routed.data };
492
334
  }
335
+ #onSessionEvent(event) {
336
+ // Invariant: once disposal begins no event touches the view; only the stop still settles the shell.
337
+ if (this.#disposed) {
338
+ if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
339
+ this.#stop.settleAfter(() => this.dispose());
340
+ return;
341
+ }
342
+ // Performance: a streamed chunk names one block, and touching only that block is what keeps the
343
+ // cost of a chunk the same in a long session as in a new one. Everything else
344
+ // resynchronizes the view, which is cheap next to re-reading the transcript.
345
+ if (event.type === "agent-run-started") {
346
+ this.#promptSuggestions?.invalidate();
347
+ this.root.viewport.resumeFollowing();
348
+ }
349
+ if (event.type === "assistant-message-completed" && !event.successful) {
350
+ this.#programStatus.setErrorMessage(this.view().status.diagnostics.at(-1));
351
+ }
352
+ if (event.type === "agent-run-started" || event.type === "assistant-message-completed" || event.type === "agent-run-settled") {
353
+ this.#programStatus.handleEvent(event);
354
+ }
355
+ if (event.type === "assistant-message-completed") {
356
+ this.root.viewport.noteCompletedAssistantMessage();
357
+ this.#promptSuggestions?.route(event, () => this.root.promptSuggestionPrepareBlockReason());
358
+ }
359
+ const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
360
+ const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.identity.sessionGeneration
361
+ ? this.#syncBlock(event.block)
362
+ : semanticOnly ? this.view() : this.#syncView();
363
+ this.#syncTerminalProgress(view);
364
+ this.#promptSuggestions?.noteView(view, this.backend.identity.sessionGeneration !== this.#sessionGeneration);
365
+ if (event.type === "status" && /^(Retrying|Compacting)/.test(event.status.workingMessage ?? "")) {
366
+ this.#promptSuggestions?.invalidate();
367
+ }
368
+ if (event.type === "agent-run-settled") {
369
+ this.#promptSuggestions?.route(event, () => this.root.promptSuggestionPrepareBlockReason());
370
+ }
371
+ if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
372
+ this.#stop.settleAfter(() => this.dispose());
373
+ }
493
374
  #trackCopy(delivery, count) {
494
375
  const intent = ++this.#copyIntentSequence;
495
376
  void delivery?.then(({ outcome }) => {
496
377
  if (count === undefined || this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
497
378
  || outcome !== "delivered" && outcome !== "submitted-unverified")
498
379
  return;
499
- this.root.showCopyAcknowledgement(copyAcknowledgementText(count));
380
+ this.root.chrome.showCopyAcknowledgement(copyAcknowledgementText(count));
500
381
  });
501
382
  }
502
- #promptSuggestionPresentationBlockReason(identity) {
503
- return suggestionPresentationBlockReason(identity, { disposed: this.#disposed, view: this.view(), sessionGeneration: this.backend.identity.sessionGeneration }, () => this.root.promptSuggestionPresentationBlockReason());
504
- }
505
383
  start() {
506
384
  if (this.#started)
507
385
  return;
508
386
  this.#started = true;
509
387
  this.#terminal.start();
510
- this.#promptHistory?.start();
388
+ this.#history?.start();
511
389
  this.#syncTerminalProgress(this.view());
512
390
  void this.backend.extensions.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
513
391
  this.#syncView();
@@ -516,16 +394,11 @@ export class OwnedUiSessionPresenter {
516
394
  if (this.#disposed || !this.#customViewport)
517
395
  return;
518
396
  this.#dialogs.presentStartupRoute();
519
- void this.#loadLazySelectors().then(selectors => selectors.prepare()).catch(() => { });
520
- this.root.warmPastePreparation();
521
- this.#copyExecutor?.warm();
397
+ void this.#presenters.components.dialogs.lazySelectors().then(selectors => selectors.prepare()).catch(() => { });
398
+ this.root.promptChips.warm();
399
+ this.#clipboard.warm();
522
400
  });
523
401
  }
524
- onView(listener) {
525
- this.#listeners.add(listener);
526
- listener(this.view());
527
- return () => this.#listeners.delete(listener);
528
- }
529
402
  waitUntilStopped() {
530
403
  return this.#stop.stopped;
531
404
  }
@@ -562,9 +435,9 @@ export class OwnedUiSessionPresenter {
562
435
  if (now - this.#lastEscapeTime < 500) {
563
436
  this.#lastEscapeTime = 0;
564
437
  if (action === "tree")
565
- await this.showTreeSelector();
438
+ await this.selectors.showTreeSelector();
566
439
  else
567
- this.showForkSelector();
440
+ this.selectors.showForkSelector();
568
441
  return { outcome: "completed", diagnostic: null };
569
442
  }
570
443
  this.#lastEscapeTime = now;
@@ -573,36 +446,6 @@ export class OwnedUiSessionPresenter {
573
446
  async abort() {
574
447
  return this.#submission.execute(this.#simple("abort"));
575
448
  }
576
- async retry() {
577
- this.#promptSuggestions?.invalidate();
578
- return this.#submission.execute(this.#simple("retry"));
579
- }
580
- async compact() {
581
- this.#promptSuggestions?.invalidate();
582
- return this.#submission.execute(this.#simple("compact"));
583
- }
584
- async newSession() {
585
- this.#promptSuggestions?.invalidate();
586
- return this.#submission.execute(this.#simple("new-session"));
587
- }
588
- async resumeSession(sessionPath) {
589
- this.#promptSuggestions?.invalidate();
590
- return this.#submission.execute({
591
- type: "resume-session",
592
- correlationId: this.#correlation("resume"),
593
- sessionId: this.backend.identity.sessionId,
594
- sessionPath,
595
- });
596
- }
597
- async setModel(providerId, modelId) {
598
- this.#promptSuggestions?.invalidate();
599
- return this.#submission.execute({
600
- type: "set-model",
601
- correlationId: this.#correlation("model"),
602
- sessionId: this.backend.identity.sessionId,
603
- model: { providerId, modelId, displayName: modelId },
604
- });
605
- }
606
449
  async setThinkingLevel(thinkingLevel) {
607
450
  return this.#submission.execute({
608
451
  type: "set-thinking-level",
@@ -618,7 +461,7 @@ export class OwnedUiSessionPresenter {
618
461
  }
619
462
  async cycleModel(direction) {
620
463
  const result = await this.backend.workflows.cycleModelWorkflow(direction);
621
- this.root.appendWorkflowResult(result);
464
+ this.root.workflow.appendResult(result);
622
465
  this.#terminal.requestRender();
623
466
  return workflowAdapterResult(result);
624
467
  }
@@ -631,193 +474,6 @@ export class OwnedUiSessionPresenter {
631
474
  showSelector(title, options, onSelect, onCancel) {
632
475
  return this.#dialogs.showSelector(title, options, onSelect, onCancel);
633
476
  }
634
- // Rationale: presenters see the shell only as a dialog host; selectors open in the input dock and run choices as workflows.
635
- #createDialogHost() {
636
- const isDisposed = () => this.#disposed;
637
- return {
638
- get disposed() { return isDisposed(); },
639
- setInputSurface: surface => this.root.setInputSurface(surface),
640
- requestRender: () => this.#terminal.requestRender(),
641
- viewport: () => this.#terminal.viewport(),
642
- appendWorkflowStatus: text => this.root.appendWorkflowStatus(text),
643
- appendWorkflowResult: result => this.root.appendWorkflowResult(result),
644
- runWorkflow: request => this.runWorkflow(request),
645
- // Invariant: the custom bare-A1 surface holds frames while optional selectors load and is permanently
646
- // fullscreen; the pinned comparison profiles render immediately and keep Pi's TUI mode switch.
647
- ...(this.#customViewport
648
- ? { beginPresentationHold: () => this.#terminal.beginPresentationHold() }
649
- : { switchTuiMode: mode => this.#terminal.switchMode(mode) }),
650
- };
651
- }
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
- async showThinkingSelector() {
661
- const snapshot = this.backend.settings.snapshot();
662
- const close = () => {
663
- this.root.setFooterLevel(true);
664
- this.root.setInputSurface(null);
665
- this.#terminal.requestRender();
666
- };
667
- const select = (level) => {
668
- close();
669
- void this.runWorkflow({ command: "thinking", argument: "", selection: level });
670
- };
671
- const releasePresentation = this.#customViewport ? this.#terminal.beginPresentationHold() : undefined;
672
- try {
673
- const selectors = await this.#loadLazySelectors();
674
- const component = await selectors.createThinking({
675
- currentLevel: snapshot.thinkingLevel,
676
- availableLevels: snapshot.availableThinkingLevels,
677
- onSelect: select,
678
- onCancel: close,
679
- onSelectAsDefault: level => this.backend.settings.setDefaultThinkingLevel(level),
680
- defaultLevel: snapshot.defaultThinkingLevel,
681
- ...(this.#customViewport ? {
682
- presentation: {
683
- profile: "bare",
684
- cycleBinding: this.root.editor.keybindingConfig()["app.thinking.cycle"] ?? [],
685
- },
686
- } : {}),
687
- });
688
- if (this.#disposed)
689
- return;
690
- this.root.setFooterLevel(false);
691
- this.root.setInputSurface(component);
692
- this.#terminal.requestRender();
693
- }
694
- finally {
695
- releasePresentation?.();
696
- }
697
- }
698
- /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
699
- showSkillsSelector(skills = this.#skillSummaries()) {
700
- const close = this.#dialogs.closeInputSurface;
701
- const component = createPiShellSkillsSelector({
702
- skills,
703
- onSelect: name => {
704
- close();
705
- void this.#submission.sendSlashPrompt(skillPrompt(name), skillPrompt(name)).catch(() => this.#submission.reportError());
706
- },
707
- onCancel: close,
708
- });
709
- this.root.setInputSurface(component);
710
- this.#terminal.requestRender();
711
- }
712
- showModelSelector(initialSearchInput) {
713
- this.#presenters.openModelSelector(this.#dialogHost, initialSearchInput);
714
- }
715
- showForkSelector() {
716
- const options = this.backend.catalog.pinnedForkOptions();
717
- if (options.length === 0) {
718
- this.root.appendWorkflowResult({ command: "fork", outcome: "completed", message: "No messages to fork from", messageKind: "status" });
719
- this.#terminal.requestRender();
720
- return;
721
- }
722
- const close = this.#dialogs.closeInputSurface;
723
- const component = createPiShellUserMessageSelector(options, selection => {
724
- close();
725
- void this.runWorkflow({ command: "fork", argument: "", selection });
726
- }, close);
727
- this.root.setInputSurface(component);
728
- this.#terminal.requestRender();
729
- }
730
- async showLogoutSelector() {
731
- let options;
732
- try {
733
- options = await this.backend.catalog.pinnedLogoutOptions();
734
- }
735
- catch (error) {
736
- this.root.appendWorkflowResult({
737
- command: "logout",
738
- outcome: "failed",
739
- message: `Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`,
740
- });
741
- this.#terminal.requestRender();
742
- return;
743
- }
744
- 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.");
746
- this.#terminal.requestRender();
747
- return;
748
- }
749
- const close = this.#dialogs.closeInputSurface;
750
- const component = createPiShellAuthProviderSelector("logout", options, selection => {
751
- close();
752
- void this.runWorkflow({ command: "logout", argument: "", selection });
753
- }, close);
754
- this.root.setInputSurface(component);
755
- this.#terminal.requestRender();
756
- }
757
- showLoginMethodSelector(providerReference) {
758
- const method = this.backend.catalog.pinnedLoginMethodOptions(providerReference);
759
- if (method.options.length === 0) {
760
- this.showLoginProviderSelector(undefined, providerReference);
761
- return;
762
- }
763
- if (method.options.length === 1) {
764
- const selection = method.options[0]?.id;
765
- if (selection)
766
- void this.runWorkflow({ command: "login", argument: "", selection });
767
- return;
768
- }
769
- const close = this.#dialogs.closeInputSurface;
770
- const labels = method.options.map(option => option.label);
771
- const component = createPiShellExtensionSelector(method.title, labels, label => {
772
- const selection = method.options.find(option => option.label === label)?.id;
773
- if (!selection)
774
- return;
775
- close();
776
- void this.runWorkflow({ command: "login", argument: "", selection });
777
- }, close);
778
- this.root.setInputSurface(component);
779
- this.#terminal.requestRender();
780
- }
781
- showTreeSelector(initialSelectedId) {
782
- return this.#presenters.openTreeSelector(this.#dialogHost, (entryId, skipSummaryPrompt) => {
783
- void this.#completeTreeSelection(entryId, skipSummaryPrompt);
784
- }, initialSelectedId);
785
- }
786
- showLoginAuthTypeSelector() {
787
- const close = this.#dialogs.closeInputSurface;
788
- const labels = ["Sign in with an account", "Sign in with an API key"];
789
- const component = createPiShellExtensionSelector("Select authentication method:", labels, label => {
790
- close();
791
- this.showLoginProviderSelector(label === labels[0] ? "oauth" : "api_key");
792
- }, close);
793
- this.root.setInputSurface(component);
794
- this.#terminal.requestRender();
795
- }
796
- showLoginProviderSelector(authType, initialSearchInput) {
797
- const options = this.backend.catalog.pinnedLoginOptions(authType);
798
- 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.");
800
- this.#terminal.requestRender();
801
- return;
802
- }
803
- const close = this.#dialogs.closeInputSurface;
804
- const component = createPiShellAuthProviderSelector("login", options, id => {
805
- close();
806
- void this.runWorkflow({ command: "login", argument: "", selection: id });
807
- }, () => {
808
- close();
809
- if (authType !== undefined)
810
- this.showLoginAuthTypeSelector();
811
- }, initialSearchInput);
812
- this.root.setInputSurface(component);
813
- this.#terminal.requestRender();
814
- }
815
- showSessionSelector() {
816
- return this.#presenters.openSessionSelector(this.#dialogHost, () => { void this.shutdown(); });
817
- }
818
- showSettingsSelector() {
819
- this.#presenters.openSettingsSelector(this.#dialogHost);
820
- }
821
477
  shutdown() {
822
478
  this.#shutdownPromise ??= this.#shutdown().finally(() => {
823
479
  // Invariant: a failed quit is retryable; ordinary shutdown has disposed the shell already.
@@ -860,7 +516,7 @@ export class OwnedUiSessionPresenter {
860
516
  }).catch(error => {
861
517
  if (this.#disposed)
862
518
  return;
863
- this.root.appendWorkflowResult({ command: "name", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
519
+ this.root.workflow.appendResult({ command: "name", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
864
520
  this.#terminal.requestRender();
865
521
  });
866
522
  return { outcome: "completed", diagnostic: null };
@@ -870,20 +526,20 @@ export class OwnedUiSessionPresenter {
870
526
  const setup = this.backend.catalog.pinnedAmbientAuthentication(request.selection);
871
527
  if (setup) {
872
528
  const close = this.#dialogs.closeInputSurface;
873
- const dialog = createPiShellLoginDialog(componentRuntime(this.#terminal), setup.providerId, close, setup.providerName, setup.title);
529
+ const dialog = this.#presenters.components.dialogs.loginDialog(componentRuntime(this.#terminal), setup.providerId, close, setup.providerName, setup.title);
874
530
  dialog.showInfo(setup.message, [], true);
875
531
  this.root.setInputSurface(dialog);
876
532
  this.#terminal.requestRender();
877
533
  return { outcome: "completed", diagnostic: null };
878
534
  }
879
535
  }
880
- const selected = openWorkflowSelector(request, this);
536
+ const selected = openWorkflowSelector(request, this.selectors);
881
537
  if (selected !== undefined)
882
538
  return selected;
883
539
  if (request.command === "reload") {
884
540
  const blocked = this.backend.workflows.reloadBlockedResult();
885
541
  if (blocked) {
886
- this.root.appendWorkflowResult(blocked);
542
+ this.root.workflow.appendResult(blocked);
887
543
  this.#terminal.requestRender();
888
544
  return workflowAdapterResult(blocked);
889
545
  }
@@ -893,8 +549,8 @@ export class OwnedUiSessionPresenter {
893
549
  ? await import("./share-operation.js")
894
550
  : undefined;
895
551
  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);
552
+ : await this.#presenters.components.dialogs.shareProgress(componentRuntime(this.#terminal), "Creating gist…", this.#customViewport ? "dialog" : "loader");
553
+ const operationSurface = shareSurface ?? (request.command === "reload" ? this.#presenters.components.info.reloadBox() : undefined);
898
554
  const now = this.#reloadPresentation?.now ?? Date.now;
899
555
  const shownAt = now();
900
556
  if (operationSurface) {
@@ -925,9 +581,9 @@ export class OwnedUiSessionPresenter {
925
581
  if (result.outcome === "requires-selection" && request.command === "model") {
926
582
  if (result.messages !== undefined) {
927
583
  for (const message of result.messages)
928
- this.root.appendWorkflowMessage(message);
584
+ this.root.workflow.appendMessage(message);
929
585
  }
930
- this.showModelSelector((result.detail ?? request.argument.trim()) || undefined);
586
+ this.selectors.showModelSelector((result.detail ?? request.argument.trim()) || undefined);
931
587
  return { outcome: "completed", diagnostic: null };
932
588
  }
933
589
  if (result.outcome === "requires-confirmation" && request.command === "resume") {
@@ -944,7 +600,7 @@ export class OwnedUiSessionPresenter {
944
600
  });
945
601
  }
946
602
  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}` });
603
+ this.root.workflow.appendResult({ command: request.command, outcome: "failed", message: `Owned controller missing for ${request.command}` });
948
604
  this.#terminal.requestRender();
949
605
  return { outcome: "failed", diagnostic: `Owned controller missing for ${request.command}` };
950
606
  }
@@ -956,13 +612,13 @@ export class OwnedUiSessionPresenter {
956
612
  if (request.command === "tree" && result.outcome === "completed") {
957
613
  if (result.detail && !this.root.editor.getText().trim())
958
614
  this.root.editor.setText(result.detail);
959
- this.root.resumeViewportFollowing();
615
+ this.root.viewport.resumeFollowing();
960
616
  }
961
617
  if (shareDialog !== undefined && this.#customViewport && result.outcome === "completed" && result.detail !== undefined) {
962
- this.root.appendWorkflowStatus(shareDialog.renderPiShellShareResult(result.message, result.detail));
618
+ this.root.workflow.appendStatus(shareDialog.renderShellShareResult(result.message, result.detail, this.#presenters.theme.current(), url => this.#presenters.components.text.hyperlink(url)));
963
619
  }
964
620
  else {
965
- this.root.appendWorkflowResult(result);
621
+ this.root.workflow.appendResult(result);
966
622
  }
967
623
  this.#terminal.requestRender();
968
624
  return workflowAdapterResult(result);
@@ -975,40 +631,11 @@ export class OwnedUiSessionPresenter {
975
631
  const sleep = this.#reloadPresentation?.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
976
632
  await sleep(remaining);
977
633
  }
978
- showTrustSelector() {
979
- const context = this.backend.catalog.pinnedProjectTrustContext();
980
- const close = this.#dialogs.closeInputSurface;
981
- const component = createPiShellTrustSelector({
982
- ...context,
983
- onSelect: selection => {
984
- try {
985
- this.backend.catalog.persistProjectTrust(selection.updates);
986
- close();
987
- this.root.appendWorkflowStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
988
- }
989
- catch (error) {
990
- close();
991
- this.root.appendWorkflowResult({ command: "trust", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
992
- }
993
- this.#terminal.requestRender();
994
- },
995
- onCancel: close,
996
- });
997
- this.root.setInputSurface(component);
998
- this.#terminal.requestRender();
999
- }
1000
- showScopedModelsSelector() {
1001
- this.#presenters.openScopedModelsSelector(this.#dialogHost);
1002
- }
1003
634
  // Invariant: bare A1 routes `models`; the comparison profile keeps the pinned `model`/`scoped-models` pair. Hidden routes are shared.
1004
635
  #isWorkflowRoute(value) {
1005
- return workflowCommandNames(this.#customViewport ? "bare" : "comparison").includes(value)
636
+ return this.#presenters.components.workflows.commandNames(this.backend.settings.productMode).includes(value)
1006
637
  || OWNED_UI_HIDDEN_COMMAND_NAMES.includes(value);
1007
638
  }
1008
- /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
1009
- showModelsDialog(initialQuery) {
1010
- this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
1011
- }
1012
639
  // Invariant: one teardown per presenter, starting on a microtask so never inside the event listener that
1013
640
  // requested it. Callers during the teardown share its outcome; a later call finds it done. Closing the
1014
641
  // terminal host's last presenter ends the terminal.
@@ -1039,20 +666,16 @@ export class OwnedUiSessionPresenter {
1039
666
  // Invariant: events stop first, so nothing re-enters the view during history close, outro, or restoration.
1040
667
  attempt(() => this.#unsubscribe());
1041
668
  this.#copyIntentSequence += 1;
1042
- this.#responseCopy?.dispose();
1043
- this.#copyExecutor?.dispose();
669
+ this.#clipboard.dispose();
1044
670
  this.#submission.cancelWaiting();
1045
671
  let pasteCleanup = Promise.resolve();
1046
672
  let historyCleanup = Promise.resolve(true);
1047
- attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
673
+ attempt(() => { historyCleanup = this.#history?.close() ?? Promise.resolve(true); });
1048
674
  attempt(() => { pasteCleanup = this.root.disposePendingPastes(); });
1049
- attempt(() => this.root.clearViewportPointerState());
675
+ attempt(() => this.root.viewport.clearPointerState());
1050
676
  attempt(() => this.#streamPresentation.dispose());
1051
677
  attempt(() => this.#promptSuggestions?.dispose());
1052
- attempt(() => this.#unsubscribePromptSuggestions());
1053
- attempt(() => this.#unsubscribePromptImages());
1054
- attempt(() => this.#unsubscribeSkills());
1055
- attempt(() => this.#unsubscribeSettings());
678
+ attempt(() => this.#unbindShellSettings());
1056
679
  let exitText = "";
1057
680
  attempt(() => {
1058
681
  exitText = composeExitText({
@@ -1060,7 +683,7 @@ export class OwnedUiSessionPresenter {
1060
683
  customViewport: this.#customViewport,
1061
684
  exitMode: this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput,
1062
685
  resume: this.backend.identity.currentSessionResumeMetadata(),
1063
- transcript: () => this.root.exitTranscript(terminal.columns),
686
+ transcript: () => this.root.transcript.exitTranscript(terminal.columns),
1064
687
  });
1065
688
  });
1066
689
  attempt(() => this.#unbindClipboardWriter());
@@ -1088,7 +711,7 @@ export class OwnedUiSessionPresenter {
1088
711
  }
1089
712
  // Invariant: incremental block updates notify listeners with the same complete view contract.
1090
713
  #syncBlock(block) {
1091
- this.root.applyTranscriptBlock(block);
714
+ this.root.transcript.apply(block);
1092
715
  if (this.#customViewport && block.status === "live" && isCoalescedStreamBlock(block.kind)) {
1093
716
  this.#streamPresentation.request();
1094
717
  }
@@ -1099,13 +722,11 @@ export class OwnedUiSessionPresenter {
1099
722
  this.#streamPresentation.presentImmediate();
1100
723
  }
1101
724
  const view = this.view();
1102
- for (const listener of this.#listeners)
1103
- listener(view);
1104
725
  return view;
1105
726
  }
1106
727
  // Rationale: extensions reach the shell only through this bridge; every surface they draw lands in the root.
1107
728
  #createExtensionBridge() {
1108
- return createPiExtensionUiBridge({
729
+ return this.#presenters.components.dialogs.extensionUiBridge({
1109
730
  runtime: componentRuntime(this.#terminal),
1110
731
  agentDir: this.backend.identity.agentDir,
1111
732
  setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
@@ -1129,14 +750,40 @@ export class OwnedUiSessionPresenter {
1129
750
  setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
1130
751
  });
1131
752
  }
753
+ #createSelectors(presenters) {
754
+ return new SessionSelectors({
755
+ root: this.root,
756
+ terminal: this.#terminal,
757
+ dialogs: this.#dialogs,
758
+ presenters,
759
+ backend: this.backend,
760
+ customViewport: this.#customViewport,
761
+ disposed: () => this.#disposed,
762
+ runWorkflow: request => this.runWorkflow(request),
763
+ sendSlashPrompt: (text, typed) => this.#submission.sendSlashPrompt(text, typed),
764
+ reportError: () => this.#submission.reportError(),
765
+ input: (title, placeholder, inputOptions) => this.#extensionBridge.input(title, placeholder, inputOptions),
766
+ shutdown: () => { void this.shutdown(); },
767
+ });
768
+ }
769
+ #createHistory(promptHistory) {
770
+ return new SessionPromptHistory(promptHistory, {
771
+ editor: this.root.editor,
772
+ usesDefaultInputSurface: () => this.root.usesDefaultInputSurface(),
773
+ rehydrate: (text, resolveImage) => this.root.promptChips.rehydrateHistoryText(text, resolveImage),
774
+ requestRender: () => this.#terminal.requestRender(),
775
+ transcript: () => this.view().transcript,
776
+ currentHistorySessionId: () => this.backend.identity.currentHistorySessionId(),
777
+ });
778
+ }
1132
779
  #createSubmission(cwd) {
1133
780
  return new PromptSubmission({
1134
781
  backend: this.backend,
1135
782
  root: this.root,
1136
783
  terminal: this.#terminal,
1137
- cwd,
1138
- promptHistory: () => this.#promptHistory,
1139
- imageSidecar: () => this.#historyBinding?.imageSidecar,
784
+ cwd, statusText: this.#presenters.components.info.statusText,
785
+ promptHistory: () => this.#history?.controller ?? null,
786
+ imageSidecar: () => this.#history?.imageSidecar,
1140
787
  disposed: () => this.#disposed,
1141
788
  correlation: prefix => this.#correlation(prefix),
1142
789
  lifecycle: () => this.view().lifecycle,
@@ -1148,7 +795,7 @@ export class OwnedUiSessionPresenter {
1148
795
  // Invariant: the order is the reset order on session replacement; copy intent before pastes before the input surface.
1149
796
  #createConcerns() {
1150
797
  return new SessionConcerns([
1151
- { onSessionReplaced: () => { this.#copyIntentSequence += 1; this.#responseCopy?.reset(); } },
798
+ { onSessionReplaced: () => { this.#copyIntentSequence += 1; this.#clipboard.reset(); } },
1152
799
  this.#submission,
1153
800
  { onSessionReplaced: () => this.root.resetPendingPastes() },
1154
801
  { onSessionReplaced: () => this.#promptSuggestions?.invalidate() },
@@ -1157,7 +804,7 @@ export class OwnedUiSessionPresenter {
1157
804
  this.#login,
1158
805
  { onSessionReplaced: () => this.#extensionBridge.reset() },
1159
806
  // Invariant: a replaced session takes its transient viewport and owned-route state with it.
1160
- { onSessionReplaced: () => this.root.resetViewport() },
807
+ { onSessionReplaced: () => this.root.viewport.reset() },
1161
808
  this.#dialogs,
1162
809
  { onSessionReplaced: () => this.root.setInputSurface(null) },
1163
810
  { onSessionReplaced: () => this.root.resetExtensionUi() },
@@ -1171,50 +818,9 @@ export class OwnedUiSessionPresenter {
1171
818
  return;
1172
819
  if (replaced) {
1173
820
  this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
1174
- this.root.resetTranscript();
1175
- }
1176
- const binding = this.#historyBinding;
1177
- if (binding?.options.forSession !== undefined || binding?.options.forScope !== undefined) {
1178
- const sessionId = this.backend.identity.currentHistorySessionId();
1179
- const selectedScope = scope ?? binding.scope;
1180
- if (selectedScope !== binding.scope || sessionId !== binding.sessionId) {
1181
- if (!replaced)
1182
- this.root.editor.recall?.restoreDraft?.();
1183
- const previous = this.#promptHistory;
1184
- this.#promptHistory = null;
1185
- binding.imageSidecar = undefined;
1186
- binding.scope = selectedScope;
1187
- binding.sessionId = sessionId ?? undefined;
1188
- void previous?.close().catch(() => false);
1189
- const destination = binding.options.forScope !== undefined
1190
- ? binding.options.forScope(selectedScope, sessionId)
1191
- : sessionId === null ? null : binding.options.forSession(sessionId);
1192
- if (destination !== null) {
1193
- binding.imageSidecar = destination.imageSidecar;
1194
- const controller = new PromptHistoryController({
1195
- editor: this.root.editor,
1196
- store: destination.store,
1197
- limit: binding.options.limit,
1198
- fallback: replaced ? this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []) : [],
1199
- active: () => this.root.usesDefaultInputSurface(),
1200
- render: () => this.#terminal.requestRender(),
1201
- rehydrate: value => this.root.rehydrateHistoryText(value, id => this.#historyBinding?.imageSidecar?.readAttachment(id) ?? null),
1202
- });
1203
- this.#promptHistory = controller;
1204
- if (this.#started)
1205
- controller.start();
1206
- }
1207
- else {
1208
- // Security: missing identity must not reopen Shared or retain another session's recall.
1209
- const fallback = replaced ? this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []).reverse() : [];
1210
- this.root.editor.recall?.reset();
1211
- this.root.editor.recall?.replace(fallback);
1212
- }
1213
- return;
1214
- }
821
+ this.root.transcript.reset();
1215
822
  }
1216
- if (replaced)
1217
- this.#promptHistory?.reset(this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
823
+ this.#history?.rebind(replaced, scope);
1218
824
  }
1219
825
  #syncTerminalProgress(view) {
1220
826
  if (!this.#terminal.running)
@@ -1241,8 +847,6 @@ export class OwnedUiSessionPresenter {
1241
847
  this.#dialogs.syncBackend(view.dialog);
1242
848
  this.#presentStartupTrustWarning(view);
1243
849
  this.#terminal.requestRender();
1244
- for (const listener of this.#listeners)
1245
- listener(view);
1246
850
  return view;
1247
851
  }
1248
852
  // Rationale: a cancelled or failed trust choice explains the restricted session where the
@@ -1254,7 +858,7 @@ export class OwnedUiSessionPresenter {
1254
858
  if (diagnostic === undefined || !this.#terminal.running)
1255
859
  return;
1256
860
  this.#startupTrustHandled = true;
1257
- this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
861
+ this.root.workflow.appendMessage({ kind: "warning", message: diagnostic.message });
1258
862
  }
1259
863
  openOwnedRoute(route) {
1260
864
  if (this.#disposed)
@@ -1267,7 +871,7 @@ export class OwnedUiSessionPresenter {
1267
871
  }
1268
872
  const result = this.#dialogs.openRoute(route);
1269
873
  if (result.outcome !== "completed" && result.diagnostic !== null) {
1270
- this.root.appendWorkflowMessage({ kind: "warning", message: result.diagnostic });
874
+ this.root.workflow.appendMessage({ kind: "warning", message: result.diagnostic });
1271
875
  }
1272
876
  }
1273
877
  hotkeysPresentation() {
@@ -1283,9 +887,9 @@ export class OwnedUiSessionPresenter {
1283
887
  if (this.#isWorkflowRoute(name))
1284
888
  return this.runWorkflow({ command: name, argument });
1285
889
  if (this.#skillsCollapsed()) {
1286
- if (name === SKILLS_COMMAND_NAME)
890
+ if (name === this.#presenters.components.skills.commandName)
1287
891
  return this.#runSkillsCommand(text, argument);
1288
- const rewritten = rewriteSkillsTunnelSubmission(text);
892
+ const rewritten = this.#presenters.components.skills.rewriteTunnelSubmission(text);
1289
893
  if (rewritten !== text)
1290
894
  return this.#submission.sendSlashPrompt(rewritten, text);
1291
895
  }
@@ -1305,65 +909,26 @@ export class OwnedUiSessionPresenter {
1305
909
  && !this.backend.identity.disposed && this.backend.settings.snapshot().enableSkillCommands;
1306
910
  }
1307
911
  #skillSummaries() {
1308
- return skillsFromCommands(this.backend.workflows.workflowAutocompleteCommands());
912
+ return this.#presenters.components.skills.fromCommands(this.backend.workflows.workflowAutocompleteCommands());
1309
913
  }
1310
914
  // Protocol: bare "/skills" opens the dialog, "/skills <name> [args]" applies directly, and an unknown name is a command outcome.
1311
915
  async #runSkillsCommand(text, argument) {
1312
916
  const skills = this.#skillSummaries();
1313
917
  const trimmed = argument.trim();
1314
918
  if (trimmed.length === 0) {
1315
- this.showSkillsSelector(skills);
919
+ this.selectors.showSkillsSelector(skills);
1316
920
  return { outcome: "completed", diagnostic: null };
1317
921
  }
1318
922
  const separator = trimmed.search(/\s/u);
1319
923
  const token = separator < 0 ? trimmed : trimmed.slice(0, separator);
1320
- const skill = findSkillByArgument(skills, token);
924
+ const skill = this.#presenters.components.skills.findByArgument(skills, token);
1321
925
  if (skill === undefined) {
1322
926
  const message = "Unknown skill: " + token;
1323
- this.root.appendWorkflowMessage({ kind: "error", message });
927
+ this.root.workflow.appendMessage({ kind: "error", message });
1324
928
  this.#terminal.requestRender();
1325
929
  return { outcome: "failed", diagnostic: message };
1326
930
  }
1327
- return this.#submission.sendSlashPrompt(skillPrompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1328
- }
1329
- #treeSummary() {
1330
- return new Promise(resolve => {
1331
- this.root.setInputSurface(createPiShellExtensionSelector("Summarize Branch?", ["No summary", "Summarize", "Summarize with custom prompt"], resolve, () => resolve(undefined)), true, "opaque");
1332
- this.#terminal.requestRender();
1333
- });
1334
- }
1335
- async #completeTreeSelection(entryId, skipSummaryPrompt) {
1336
- let summarize = false;
1337
- let customInstructions;
1338
- if (!skipSummaryPrompt) {
1339
- while (true) {
1340
- const choice = await this.#treeSummary();
1341
- if (choice === undefined) {
1342
- await this.showTreeSelector(entryId);
1343
- return;
1344
- }
1345
- summarize = choice !== "No summary";
1346
- if (choice === "Summarize with custom prompt") {
1347
- customInstructions = await this.#extensionBridge.input("Custom Summarization Instructions", "", { retainSurfaceOnSettle: true });
1348
- if (customInstructions === undefined)
1349
- continue;
1350
- }
1351
- break;
1352
- }
1353
- }
1354
- this.root.setInputSurface(null);
1355
- this.#terminal.requestRender();
1356
- const result = await this.runWorkflow({
1357
- command: "tree",
1358
- argument: "",
1359
- selection: entryId,
1360
- treeSummary: {
1361
- summarize,
1362
- ...(customInstructions === undefined ? {} : { customInstructions }),
1363
- },
1364
- });
1365
- if (result.diagnostic === "Branch summarization cancelled")
1366
- await this.showTreeSelector(entryId);
931
+ return this.#submission.sendSlashPrompt(this.#presenters.components.skills.prompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1367
932
  }
1368
933
  #simple(type) {
1369
934
  return { type, correlationId: this.#correlation(type), sessionId: this.backend.identity.sessionId };