@timurproko/a1 0.2.6 → 0.2.7-dev.1222

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 (419) hide show
  1. package/README.md +41 -3
  2. package/bin/automatic-update.js +7 -0
  3. package/bin/cli.js +4 -0
  4. package/bin/guardian.js +5 -0
  5. package/bin/tab-launch-context.d.ts +27 -0
  6. package/bin/tab-launch-context.js +94 -0
  7. package/bin/ui.js +107 -59
  8. package/dist/app/session-shell/clipboard-diagnostics.js +1 -1
  9. package/dist/app/session-shell/dialog-coordinator.d.ts +42 -0
  10. package/dist/app/session-shell/dialog-coordinator.js +133 -0
  11. package/dist/app/session-shell/dock-chrome.d.ts +34 -0
  12. package/dist/app/session-shell/dock-chrome.js +109 -0
  13. package/dist/app/session-shell/exit-notice.d.ts +1 -1
  14. package/dist/app/session-shell/exit-notice.js +1 -1
  15. package/dist/app/session-shell/extension-surface.d.ts +30 -0
  16. package/dist/app/session-shell/extension-surface.js +105 -0
  17. package/dist/app/session-shell/helper-pool.d.ts +3 -5
  18. package/dist/app/session-shell/helper-pool.js +3 -5
  19. package/dist/app/session-shell/input-surface-switch.d.ts +36 -0
  20. package/dist/app/session-shell/input-surface-switch.js +85 -0
  21. package/dist/app/session-shell/login-interaction.d.ts +27 -0
  22. package/dist/app/session-shell/login-interaction.js +91 -0
  23. package/dist/app/session-shell/prompt-submission.d.ts +48 -0
  24. package/dist/app/session-shell/prompt-submission.js +276 -0
  25. package/dist/app/session-shell/resident-tab-coordinator.d.ts +38 -0
  26. package/dist/app/session-shell/resident-tab-coordinator.js +77 -0
  27. package/dist/app/session-shell/response-copy-helper.js +21 -6
  28. package/dist/app/session-shell/response-copy-protocol.d.ts +1 -1
  29. package/dist/app/session-shell/response-copy-transport.d.ts +16 -6
  30. package/dist/app/session-shell/response-copy-transport.js +69 -17
  31. package/dist/app/session-shell/session-concern.d.ts +22 -0
  32. package/dist/app/session-shell/session-concern.js +29 -0
  33. package/dist/app/session-shell/session-lifecycle.d.ts +29 -0
  34. package/dist/app/session-shell/session-lifecycle.js +49 -0
  35. package/dist/app/session-shell/session-shell-root.d.ts +42 -6
  36. package/dist/app/session-shell/session-shell-root.js +197 -908
  37. package/dist/app/session-shell/session-shell.d.ts +3 -8
  38. package/dist/app/session-shell/session-shell.js +327 -835
  39. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -1
  40. package/dist/app/session-shell/session-viewport-controller.js +41 -38
  41. package/dist/app/session-shell/settings-bindings.d.ts +17 -0
  42. package/dist/app/session-shell/settings-bindings.js +91 -0
  43. package/dist/app/session-shell/shell-editor-factory.d.ts +27 -0
  44. package/dist/app/session-shell/shell-editor-factory.js +105 -0
  45. package/dist/app/session-shell/shell-host.d.ts +6 -0
  46. package/dist/app/session-shell/shell-host.js +34 -7
  47. package/dist/app/session-shell/shell-layout.d.ts +64 -0
  48. package/dist/app/session-shell/shell-layout.js +18 -0
  49. package/dist/app/session-shell/suggestion-surface.d.ts +25 -0
  50. package/dist/app/session-shell/suggestion-surface.js +63 -0
  51. package/dist/app/session-shell/transcript-document.d.ts +78 -0
  52. package/dist/app/session-shell/transcript-document.js +365 -0
  53. package/dist/app/session-shell/viewport-layout.d.ts +31 -0
  54. package/dist/app/session-shell/viewport-layout.js +160 -0
  55. package/dist/app/session-shell/workflow-output.d.ts +33 -0
  56. package/dist/app/session-shell/workflow-output.js +174 -0
  57. package/dist/app/session-shell/workflow-router.d.ts +25 -0
  58. package/dist/app/session-shell/workflow-router.js +39 -0
  59. package/dist/cli/dispatch.d.ts +11 -1
  60. package/dist/cli/dispatch.js +79 -5
  61. package/dist/cli/tabs-request.d.ts +7 -0
  62. package/dist/cli/tabs-request.js +1 -0
  63. package/dist/cli/tabs.d.ts +14 -0
  64. package/dist/cli/tabs.js +151 -0
  65. package/dist/cli/version-stats.js +38 -3
  66. package/dist/composition/owned-settings.d.ts +12 -0
  67. package/dist/composition/owned-settings.js +36 -0
  68. package/dist/composition/owned-ui.d.ts +27 -28
  69. package/dist/composition/owned-ui.js +207 -153
  70. package/dist/composition/reference-selection-options.d.ts +3 -0
  71. package/dist/composition/reference-selection-options.js +16 -0
  72. package/dist/composition/resident-tab-bridge.d.ts +25 -0
  73. package/dist/composition/resident-tab-bridge.js +180 -0
  74. package/dist/composition/session-backend.d.ts +19 -0
  75. package/dist/composition/session-backend.js +1 -0
  76. package/dist/composition/session-runtime-registry.d.ts +20 -0
  77. package/dist/composition/session-runtime-registry.js +85 -0
  78. package/dist/composition/settings-route-host.d.ts +16 -2
  79. package/dist/composition/settings-route-host.js +42 -17
  80. package/dist/composition/settings-search-input.d.ts +9 -0
  81. package/dist/composition/settings-search-input.js +61 -0
  82. package/dist/composition/startup-route.d.ts +32 -0
  83. package/dist/composition/startup-route.js +72 -0
  84. package/dist/composition/utility-tab.d.ts +28 -0
  85. package/dist/composition/utility-tab.js +244 -0
  86. package/dist/contracts/owned-ui/index.d.ts +1 -1
  87. package/dist/contracts/owned-ui/model.d.ts +3 -0
  88. package/dist/contracts/owned-ui/session-backend.d.ts +3 -0
  89. package/dist/contracts/owned-ui/session-workflows.d.ts +11 -4
  90. package/dist/contracts/presentation/index.d.ts +11 -0
  91. package/dist/contracts/presentation/index.js +12 -0
  92. package/dist/features/launch/development-launch.js +6 -0
  93. package/dist/features/launch/index.d.ts +1 -1
  94. package/dist/features/launch/intent.d.ts +3 -1
  95. package/dist/features/launch/intent.js +7 -4
  96. package/dist/features/launch/profile-paths.js +4 -0
  97. package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
  98. package/dist/features/owned-ui/plus-menu-dialog.js +84 -0
  99. package/dist/features/owned-ui/project-trust-dialog.d.ts +1 -1
  100. package/dist/features/owned-ui/project-trust-dialog.js +6 -8
  101. package/dist/features/owned-ui/project-trust-prompt.d.ts +1 -0
  102. package/dist/features/owned-ui/project-trust-prompt.js +7 -3
  103. package/dist/features/owned-ui/reference-document-selection.d.ts +30 -0
  104. package/dist/features/owned-ui/reference-document-selection.js +179 -0
  105. package/dist/features/owned-ui/reference-screen-app.d.ts +6 -1
  106. package/dist/features/owned-ui/reference-screen-app.js +51 -10
  107. package/dist/features/owned-ui/settings-app.d.ts +13 -2
  108. package/dist/features/owned-ui/settings-app.js +136 -97
  109. package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
  110. package/dist/features/owned-ui/settings-search-input.js +1 -0
  111. package/dist/features/prompt-history/index.d.ts +1 -1
  112. package/dist/features/prompt-history/paths.d.ts +2 -1
  113. package/dist/features/prompt-history/paths.js +10 -2
  114. package/dist/features/prompt-history/service.d.ts +5 -0
  115. package/dist/features/prompt-history/service.js +1 -1
  116. package/dist/features/resident-tab/auto-name.d.ts +11 -0
  117. package/dist/features/resident-tab/auto-name.js +44 -0
  118. package/dist/features/resident-tab/index.d.ts +5 -0
  119. package/dist/features/resident-tab/index.js +7 -0
  120. package/dist/features/resident-tab/shortcut-bindings-file.d.ts +8 -0
  121. package/dist/features/resident-tab/shortcut-bindings-file.js +40 -0
  122. package/dist/features/resident-tab/shortcut-declarations.d.ts +13 -0
  123. package/dist/features/resident-tab/shortcut-declarations.js +26 -0
  124. package/dist/features/resident-tab/shortcut-projection.d.ts +29 -0
  125. package/dist/features/resident-tab/shortcut-projection.js +64 -0
  126. package/dist/features/resident-tab/status-deriver.d.ts +10 -0
  127. package/dist/features/resident-tab/status-deriver.js +12 -0
  128. package/dist/foundation/launch-context/index.d.ts +12 -0
  129. package/dist/foundation/launch-context/index.js +26 -0
  130. package/dist/foundation/launch-guardian/exit-watcher.d.ts +14 -0
  131. package/dist/foundation/launch-guardian/exit-watcher.js +94 -0
  132. package/dist/foundation/launch-guardian/main.d.ts +7 -1
  133. package/dist/foundation/launch-guardian/main.js +60 -7
  134. package/dist/foundation/lifecycle/session-repository-context.js +16 -2
  135. package/dist/foundation/process-containment/{native-guardian-containment.d.ts → addon-containment.d.ts} +7 -5
  136. package/dist/foundation/process-containment/addon-containment.js +139 -0
  137. package/dist/foundation/process-containment/addon-process-inspector.d.ts +11 -0
  138. package/dist/foundation/process-containment/addon-process-inspector.js +36 -0
  139. package/dist/foundation/process-containment/addon.d.ts +42 -0
  140. package/dist/foundation/process-containment/addon.js +57 -0
  141. package/dist/foundation/process-containment/artifact.d.ts +1 -1
  142. package/dist/foundation/process-containment/artifact.js +6 -5
  143. package/dist/foundation/process-containment/contracts.d.ts +2 -0
  144. package/dist/foundation/process-containment/index.d.ts +4 -4
  145. package/dist/foundation/process-containment/index.js +3 -3
  146. package/dist/foundation/process-containment/native-process-inspector.d.ts +2 -1
  147. package/dist/foundation/process-containment/native-process-inspector.js +10 -10
  148. package/dist/foundation/release/automatic-update.d.ts +37 -0
  149. package/dist/foundation/release/automatic-update.js +265 -0
  150. package/dist/foundation/release/bootstrap.d.ts +7 -9
  151. package/dist/foundation/release/bootstrap.js +107 -24
  152. package/dist/foundation/release/cohort-state.d.ts +1 -1
  153. package/dist/foundation/release/cohort-state.js +1 -1
  154. package/dist/foundation/release/dependency-certification.js +54 -19
  155. package/dist/foundation/release/early-accent.d.ts +12 -0
  156. package/dist/foundation/release/early-accent.js +59 -0
  157. package/dist/foundation/release/index.d.ts +2 -2
  158. package/dist/foundation/release/index.js +2 -2
  159. package/dist/foundation/release/latest-release.d.ts +24 -2
  160. package/dist/foundation/release/latest-release.js +51 -1
  161. package/dist/foundation/release/progress-palette.generated.d.ts +137 -3
  162. package/dist/foundation/release/progress-palette.generated.js +137 -3
  163. package/dist/foundation/release/release-gc.d.ts +3 -0
  164. package/dist/foundation/release/release-gc.js +59 -8
  165. package/dist/foundation/release/release-store.d.ts +12 -0
  166. package/dist/foundation/release/release-store.js +124 -2
  167. package/dist/foundation/release/update-execution-lease.d.ts +48 -0
  168. package/dist/foundation/release/update-execution-lease.js +161 -0
  169. package/dist/foundation/release/update.d.ts +12 -7
  170. package/dist/foundation/release/update.js +281 -196
  171. package/dist/foundation/resident-tabs/bounded-host-command.d.ts +18 -0
  172. package/dist/foundation/resident-tabs/bounded-host-command.js +39 -0
  173. package/dist/foundation/resident-tabs/bridge-client.d.ts +70 -0
  174. package/dist/foundation/resident-tabs/bridge-client.js +321 -0
  175. package/dist/foundation/resident-tabs/doctor.d.ts +28 -0
  176. package/dist/foundation/resident-tabs/doctor.js +166 -0
  177. package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
  178. package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
  179. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +36 -0
  180. package/dist/foundation/resident-tabs/ensure-terminal-host.js +151 -0
  181. package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
  182. package/dist/foundation/resident-tabs/fault-injection.js +26 -0
  183. package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
  184. package/dist/foundation/resident-tabs/fixture-control.js +35 -0
  185. package/dist/foundation/resident-tabs/host-entry.d.ts +1 -0
  186. package/dist/foundation/resident-tabs/host-entry.js +17 -0
  187. package/dist/foundation/resident-tabs/index.d.ts +12 -0
  188. package/dist/foundation/resident-tabs/index.js +6 -0
  189. package/dist/foundation/resident-tabs/maintenance.d.ts +39 -0
  190. package/dist/foundation/resident-tabs/maintenance.js +75 -0
  191. package/dist/foundation/resident-tabs/progress-observation-schema.d.ts +34 -0
  192. package/dist/foundation/resident-tabs/progress-observation-schema.js +1 -0
  193. package/dist/foundation/resident-tabs/progress-observation.d.ts +4 -0
  194. package/dist/foundation/resident-tabs/progress-observation.js +150 -0
  195. package/dist/foundation/resident-tabs/prompt-journal.d.ts +63 -0
  196. package/dist/foundation/resident-tabs/prompt-journal.js +213 -0
  197. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  198. package/dist/foundation/resident-tabs/protocol.js +195 -0
  199. package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
  200. package/dist/foundation/resident-tabs/recovery.js +205 -0
  201. package/dist/foundation/resident-tabs/release-holds.d.ts +28 -0
  202. package/dist/foundation/resident-tabs/release-holds.js +75 -0
  203. package/dist/foundation/resident-tabs/routes.d.ts +5 -0
  204. package/dist/foundation/resident-tabs/routes.js +5 -0
  205. package/dist/foundation/resident-tabs/routing.d.ts +11 -0
  206. package/dist/foundation/resident-tabs/routing.js +10 -0
  207. package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
  208. package/dist/foundation/resident-tabs/session-sync.js +37 -0
  209. package/dist/foundation/resident-tabs/settings.d.ts +12 -0
  210. package/dist/foundation/resident-tabs/settings.js +58 -0
  211. package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +14 -0
  212. package/dist/foundation/resident-tabs/terminal-host-artifact.js +78 -0
  213. package/dist/foundation/resident-tabs/zip-archive.d.ts +9 -0
  214. package/dist/foundation/resident-tabs/zip-archive.js +56 -0
  215. package/dist/foundation/session-writer-lock/index.d.ts +30 -0
  216. package/dist/foundation/session-writer-lock/index.js +203 -0
  217. package/dist/foundation/startup/startup-descriptor.js +2 -2
  218. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  219. package/dist/foundation/terminal-cleanup/index.js +1 -1
  220. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +6 -0
  221. package/dist/foundation/terminal-cleanup/terminal-reset.js +6 -0
  222. package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
  223. package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
  224. package/dist/integrations/pi/components/modal-frame.js +48 -9
  225. package/dist/integrations/pi/components/models-dialog.js +4 -52
  226. package/dist/integrations/pi/components/owned-editor-ux.d.ts +6 -1
  227. package/dist/integrations/pi/components/owned-editor-ux.js +138 -11
  228. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
  229. package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
  230. package/dist/integrations/pi/components/share-operation-dialog.d.ts +2 -0
  231. package/dist/integrations/pi/components/share-operation-dialog.js +2 -1
  232. package/dist/integrations/pi/components/shell-editor-autocomplete.js +58 -36
  233. package/dist/integrations/pi/components/shell-extension-ui.js +79 -60
  234. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  235. package/dist/integrations/pi/components/shell-footer-status.js +21 -5
  236. package/dist/integrations/pi/components/shell-hotkey-sections.js +1 -1
  237. package/dist/integrations/pi/components/shell-presenters-info.d.ts +5 -2
  238. package/dist/integrations/pi/components/shell-presenters-info.js +18 -4
  239. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
  240. package/dist/integrations/pi/components/shell-selectors-dialogs.js +5 -1
  241. package/dist/integrations/pi/components/shell-shared-facade.d.ts +12 -1
  242. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  243. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  244. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
  245. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  246. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  247. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  248. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  249. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  250. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  251. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  252. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  253. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +8 -6
  254. package/dist/integrations/pi/components/upstream/components/owned-editor.js +16 -9
  255. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  256. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +4 -4
  257. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
  258. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  259. package/dist/integrations/pi/components/upstream/components/session-footer.js +31 -25
  260. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  261. package/dist/integrations/pi/components/upstream/components/session-selector.js +8 -7
  262. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  263. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  264. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
  265. package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
  266. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  267. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  268. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +8 -7
  269. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +10 -9
  270. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +2 -6
  271. package/dist/integrations/pi/components/upstream/components/tool-execution.js +103 -101
  272. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +9 -9
  273. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -21
  274. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  275. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  276. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
  277. package/dist/integrations/pi/components/upstream/history/editor-core.js +7 -5
  278. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  279. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  280. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  281. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  282. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  283. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  284. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  285. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  286. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  287. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  288. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +5 -0
  289. package/dist/integrations/pi/components/upstream/theme/theme.js +29 -2
  290. package/dist/integrations/pi/engine/adapter.d.ts +5 -77
  291. package/dist/integrations/pi/engine/adapter.js +95 -394
  292. package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
  293. package/dist/integrations/pi/engine/command-dispatch.js +57 -0
  294. package/dist/integrations/pi/engine/host.js +2 -2
  295. package/dist/integrations/pi/engine/offline-settings-port.d.ts +23 -0
  296. package/dist/integrations/pi/engine/offline-settings-port.js +53 -0
  297. package/dist/integrations/pi/engine/pi-settings-metadata.json +14 -2
  298. package/dist/integrations/pi/engine/ports.d.ts +56 -0
  299. package/dist/integrations/pi/engine/ports.js +93 -0
  300. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
  301. package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
  302. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  303. package/dist/integrations/pi/engine/runtime-integration.js +102 -29
  304. package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
  305. package/dist/integrations/pi/engine/session-runtime.js +13 -5
  306. package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
  307. package/dist/integrations/pi/engine/session-selection.js +45 -9
  308. package/dist/integrations/pi/engine/settings-bridge.d.ts +5 -0
  309. package/dist/integrations/pi/engine/settings-bridge.js +15 -0
  310. package/dist/integrations/pi/engine/settings-port.js +11 -15
  311. package/dist/integrations/pi/engine/view-state.d.ts +42 -0
  312. package/dist/integrations/pi/engine/view-state.js +126 -0
  313. package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
  314. package/dist/integrations/pi/engine/workflow-runner.js +74 -5
  315. package/dist/integrations/pi/engine/workflow-support.d.ts +12 -0
  316. package/dist/integrations/pi/engine/workflow-support.js +36 -0
  317. package/dist/integrations/pi/engine/workflows.d.ts +3 -1
  318. package/dist/integrations/pi/engine/workflows.js +1 -0
  319. package/dist/integrations/pi/startup-public.js +257 -310
  320. package/dist/integrations/pi/startup-public.manifest.json +564 -440
  321. package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
  322. package/dist/integrations/pi/tui-runtime/adapter.js +32 -1
  323. package/dist/integrations/pi/tui-runtime/contracts.d.ts +5 -0
  324. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
  325. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +31 -2
  326. package/dist/native/darwin-arm64/manifest.json +10 -7
  327. package/dist/native/darwin-arm64/process-guardian.node +0 -0
  328. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
  329. package/dist/native/darwin-arm64/terminal-host.manifest.json +46 -0
  330. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  331. package/dist/native/darwin-x64/manifest.json +23 -0
  332. package/dist/native/darwin-x64/process-guardian.node +0 -0
  333. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
  334. package/dist/native/darwin-x64/terminal-host.manifest.json +46 -0
  335. package/dist/native/darwin-x64/terminal-host.node +0 -0
  336. package/dist/native/linux-x64/manifest.json +10 -7
  337. package/dist/native/linux-x64/process-guardian.node +0 -0
  338. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
  339. package/dist/native/linux-x64/terminal-host.manifest.json +46 -0
  340. package/dist/native/linux-x64/terminal-host.node +0 -0
  341. package/dist/native/win32-x64/manifest.json +10 -7
  342. package/dist/native/win32-x64/process-guardian.node +0 -0
  343. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
  344. package/dist/native/win32-x64/terminal-host.manifest.json +46 -0
  345. package/dist/native/win32-x64/terminal-host.node +0 -0
  346. package/dist/product-identity.d.ts +3 -3
  347. package/dist/product-identity.js +4 -4
  348. package/dist/product-identity.json +16 -2
  349. package/dist/runtime-payload-inventory.json +6330 -6677
  350. package/dist/ui/apps/host.d.ts +2 -0
  351. package/dist/ui/apps/host.js +7 -1
  352. package/dist/ui/components/dialog-panel.d.ts +17 -8
  353. package/dist/ui/components/dialog-panel.js +117 -41
  354. package/dist/ui/components/index.d.ts +5 -5
  355. package/dist/ui/components/index.js +4 -4
  356. package/dist/ui/components/line-input.d.ts +4 -1
  357. package/dist/ui/components/line-input.js +3 -1
  358. package/dist/ui/components/prompt-input.d.ts +11 -8
  359. package/dist/ui/components/prompt-input.js +15 -13
  360. package/dist/ui/components/scrollbar.d.ts +1 -1
  361. package/dist/ui/components/scrollbar.js +1 -1
  362. package/dist/ui/components/selection-copy.d.ts +3 -3
  363. package/dist/ui/components/selection-copy.js +16 -1
  364. package/dist/ui/components/spans.js +2 -2
  365. package/dist/ui/components/submitted-prompt.d.ts +9 -6
  366. package/dist/ui/components/submitted-prompt.js +20 -12
  367. package/dist/ui/components/surface.js +4 -2
  368. package/dist/ui/components/text-selection.d.ts +1 -2
  369. package/dist/ui/components/text-selection.js +5 -2
  370. package/dist/ui/components/text.d.ts +3 -6
  371. package/dist/ui/components/text.js +6 -8
  372. package/dist/ui/components/transcript-viewport.d.ts +9 -2
  373. package/dist/ui/components/transcript-viewport.js +179 -58
  374. package/dist/ui/settings/declarations.d.ts +90 -16
  375. package/dist/ui/settings/declarations.js +67 -14
  376. package/dist/ui/settings/early-accent.d.ts +81 -0
  377. package/dist/ui/settings/early-accent.js +50 -0
  378. package/dist/ui/settings/index.d.ts +4 -2
  379. package/dist/ui/settings/index.js +2 -1
  380. package/dist/ui/settings/manager.d.ts +16 -3
  381. package/dist/ui/settings/manager.js +122 -33
  382. package/dist/ui/settings/migrations.d.ts +6 -0
  383. package/dist/ui/settings/migrations.js +73 -0
  384. package/dist/ui/settings/profile-document.d.ts +26 -0
  385. package/dist/ui/settings/profile-document.js +164 -0
  386. package/dist/ui/settings/progress-palette.generated.d.ts +139 -0
  387. package/dist/ui/settings/progress-palette.generated.js +139 -0
  388. package/dist/ui/settings/sections.d.ts +4 -0
  389. package/dist/ui/settings/sections.js +9 -3
  390. package/docs/architecture/boundaries.md +24 -9
  391. package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
  392. package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
  393. package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
  394. package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
  395. package/docs/architecture/internal-naming.md +3 -1
  396. package/docs/architecture/process-guardian-provenance.md +18 -10
  397. package/docs/architecture/project-structure.md +8 -0
  398. package/docs/architecture/resident-tabs-requirements.md +18 -0
  399. package/docs/architecture/resident-tabs.md +649 -0
  400. package/docs/architecture/response-copy-delivery.md +1 -1
  401. package/docs/architecture/terminal-host-proof-gate.md +2 -0
  402. package/docs/architecture/terminal-host-provenance.md +16 -4
  403. package/docs/architecture/tool-image-presentation.md +2 -1
  404. package/docs/architecture/toolchain.md +11 -9
  405. package/docs/ci-release-runbook.md +14 -7
  406. package/docs/features/launch-profiles.md +11 -8
  407. package/docs/features/prompt-history.md +41 -20
  408. package/docs/manual-resident-tabs-acceptance.md +51 -0
  409. package/docs/openspec-archive-automation.md +49 -22
  410. package/docs/validation.md +49 -9
  411. package/package.json +19 -6
  412. package/dist/foundation/process-containment/darwin-process-inspector.d.ts +0 -5
  413. package/dist/foundation/process-containment/darwin-process-inspector.js +0 -7
  414. package/dist/foundation/process-containment/native-guardian-containment.js +0 -152
  415. package/dist/foundation/process-containment/windows-process-inspector.d.ts +0 -19
  416. package/dist/foundation/process-containment/windows-process-inspector.js +0 -56
  417. package/dist/native/darwin-arm64/process-guardian +0 -0
  418. package/dist/native/linux-x64/process-guardian +0 -0
  419. package/dist/native/win32-x64/process-guardian.exe +0 -0
@@ -1,27 +1,42 @@
1
1
  import { ResponseCopyCoordinator } from "./response-copy-coordinator.js";
2
- import { createResponseCopyExecutor, hasAsyncClipboardOutput, responseCopyDestination } from "./response-copy-transport.js";
2
+ import { createResponseCopyExecutor, responseCopyDestination } from "./response-copy-transport.js";
3
3
  import { MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
4
4
  import { PromptHistoryController } from "./prompt-history-controller.js";
5
- import { PRODUCT_TEXT } from "../../product-identity.js";
6
- import { boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
7
- import { assertOwnedUiCommand, OWNED_UI_HIDDEN_COMMAND_NAMES } from "../../contracts/owned-ui/index.js";
8
- import { assertPromptImages, ImageAttachmentError } from "../../contracts/owned-ui/index.js";
5
+ import { SessionDialogCoordinator } from "./dialog-coordinator.js";
6
+ import { LoginInteraction } from "./login-interaction.js";
7
+ import { PromptSubmission } from "./prompt-submission.js";
8
+ import { ResidentTabCoordinator } from "./resident-tab-coordinator.js";
9
+ import { SessionConcerns } from "./session-concern.js";
10
+ import { composeExitText, SessionStopLatch } from "./session-lifecycle.js";
11
+ import { openWorkflowSelector } from "./workflow-router.js";
12
+ import { bindSessionSettings } from "./settings-bindings.js";
13
+ import { promptSuggestionSurface, routeResponseToSuggestions, suggestionModelKey, suggestionPresentationBlockReason, } from "./suggestion-surface.js";
14
+ import { CLEAR_EXIT_INTERVAL_MS, boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
15
+ import { OWNED_UI_HIDDEN_COMMAND_NAMES } from "../../contracts/owned-ui/index.js";
16
+ import { ImageAttachmentError } from "../../contracts/owned-ui/index.js";
17
+ import { copyAcknowledgementText } from "../../ui/components/selection-copy.js";
9
18
  import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
10
19
  import { workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
11
20
  import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
12
21
  import { ProgramStatusReporter } from "../../integrations/pi/components/upstream/program-status-reporter.js";
13
22
  import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
14
- import { createPiShellAuthProviderSelector, createPiShellDialog, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellReloadBox, createPiShellSelector, createPiShellSkillsSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
15
- import { renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
16
- import { OwnedUiTerminalHost, sessionTerminalHostOptions, } from "./shell-host.js";
23
+ import { createPiShellAuthProviderSelector, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellReloadBox, createPiShellSkillsSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
24
+ import { OwnedUiTerminalHost, componentRuntime, sessionTerminalHostOptions, } from "./shell-host.js";
17
25
  import { runImageWorker } from "./image-preparation-client.js";
18
26
  import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
19
27
  import { writeSystemClipboardText, } from "./system-clipboard.js";
20
28
  import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
21
29
  // Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
22
30
  const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
31
+ /** Resident Agents refuse the commands their tab bar serves instead, naming where they live. */
32
+ const RESIDENT_UTILITY_TAB_REFUSALS = Object.freeze({
33
+ settings: "Settings is available from the global Settings tab.",
34
+ changelog: "The changelog opens as its own tab from the command palette (Alt+/).",
35
+ name: "Rename the session by renaming its tab (F2).",
36
+ });
23
37
  export { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
24
38
  export { OwnedUiTerminalHost, sessionTerminalHostOptions } from "./shell-host.js";
39
+ export { formatSessionResumeCommand, quoteCommandArgument } from "./session-lifecycle.js";
25
40
  /**
26
41
  * Presents one engine session: its transcript root, editor, controllers, submission, workflows, and dialogs.
27
42
  * It reaches the terminal only through its terminal host's handle, and paints only while it is attached.
@@ -33,7 +48,10 @@ export class OwnedUiSessionPresenter {
33
48
  root;
34
49
  #host;
35
50
  #terminal;
36
- #cwd;
51
+ #resident;
52
+ #submission;
53
+ #login;
54
+ #concerns;
37
55
  #listeners = new Set();
38
56
  #unsubscribe;
39
57
  #unsubscribePromptSuggestions;
@@ -44,18 +62,11 @@ export class OwnedUiSessionPresenter {
44
62
  #unsubscribeSkills;
45
63
  #installedCommandSignature = "";
46
64
  #promptHistory = null;
47
- #promptHistoryImageSidecar;
65
+ #historyBinding;
48
66
  #extensionBridge;
49
- #stopped;
50
- #resolveStopped;
51
- #dialogId;
52
- #dialogSource;
53
- #routeHost;
54
- #startupRoute;
55
- #startupRouteTimer;
56
- #dialogHandle;
67
+ #stop = new SessionStopLatch();
68
+ #dialogs;
57
69
  #sequence = 0;
58
- #editorRevision = 0;
59
70
  #started = false;
60
71
  #disposed = false;
61
72
  #released;
@@ -77,18 +88,12 @@ export class OwnedUiSessionPresenter {
77
88
  #streamPresentation;
78
89
  #inputCoordination;
79
90
  #unsubscribeSettings;
80
- #unbindPiSettings;
81
- #unbindTerminalSettings;
82
- #unbindShutdownSettings;
91
+ #unbindSessionSettings;
83
92
  #terminalProgressEnabled = false;
84
93
  #programStatus;
85
- #showImages = true;
86
- #imageWidthCells = 80;
87
94
  #fullscreenExitOutput = "transcript";
88
- #waitingImages = new Map();
89
95
  #lastClearTime = 0;
90
96
  #lastEscapeTime = 0;
91
- #activeLoginDialog;
92
97
  #sessionGeneration;
93
98
  #sessionBindingGeneration;
94
99
  #suggestionModelKey;
@@ -102,10 +107,7 @@ export class OwnedUiSessionPresenter {
102
107
  this.#presenters = options.presenters;
103
108
  this.#sessionGeneration = this.backend.identity.sessionGeneration;
104
109
  this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
105
- this.#suggestionModelKey = modelKey(this.backend.session.view());
106
- this.#cwd = cwd;
107
- this.#routeHost = routeHost ?? null;
108
- this.#startupRoute = options.engine.startupRoute;
110
+ this.#suggestionModelKey = suggestionModelKey(this.backend.session.view());
109
111
  this.#customViewport = sessionLayout === "custom-viewport";
110
112
  this.#inputCoordination = inputPresentation?.coordination !== false;
111
113
  this.#host = host;
@@ -113,9 +115,6 @@ export class OwnedUiSessionPresenter {
113
115
  // Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
114
116
  this.#dialogHost = this.#createDialogHost();
115
117
  this.#lazySelectors = options.diagnostics?.lazySelectors;
116
- this.#stopped = new Promise(resolve => {
117
- this.#resolveStopped = resolve;
118
- });
119
118
  let promptSuggestionController = null;
120
119
  // Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
121
120
  this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
@@ -124,31 +123,24 @@ export class OwnedUiSessionPresenter {
124
123
  // throw releases them in reverse, so a shell that never finished constructing holds nothing.
125
124
  const constructed = [() => host.disconnect(this)];
126
125
  try {
127
- const terminalCopy = terminal !== undefined || hasAsyncClipboardOutput();
128
126
  this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
129
127
  execute: responseCopy?.execute ?? (this.#copyExecutor = createResponseCopyExecutor({
130
128
  ...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.copy }),
131
129
  ...(terminal === undefined && clipboard === undefined ? {} : { destination: "terminal" }),
132
130
  ...(clipboard?.writeText === undefined ? {} : { writeText: (text, signal) => clipboard.writeText(text, signal) }),
133
- ...(terminalCopy ? { terminal: { submit: async (control, signal) => {
134
- if (signal.aborted || this.#disposed || !this.#terminal.running)
135
- throw new Error("Copy canceled");
136
- // Performance: never grow the real terminal's pending buffer with another clipboard payload.
137
- if (terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
138
- throw new Error("Clipboard terminal is busy");
139
- }
140
- this.#terminal.writeControl(control);
141
- } } } : {}),
131
+ // Invariant: the owned host is the route contract. Resident and pseudoterminal children must not infer
132
+ // clipboard reachability from Node's view of stdout, and inactive presenters are fenced here.
133
+ terminal: { submit: async (control, signal) => {
134
+ if (signal.aborted || this.#disposed || !this.#terminal.running || !this.#terminal.attached)
135
+ throw new Error("Copy canceled");
136
+ // Performance: never grow the real terminal's pending buffer with another clipboard payload.
137
+ if (terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
138
+ throw new Error("Clipboard terminal is busy");
139
+ }
140
+ this.#terminal.writeControl(control);
141
+ } },
142
142
  })),
143
143
  ...(responseCopy?.onEvent === undefined ? {} : { onEvent: responseCopy.onEvent }),
144
- onFailure: result => {
145
- if (this.#disposed || !this.#terminal.running)
146
- return;
147
- this.root.appendWorkflowStatus(result.outcome === "timed-out" ? "Copy timed out; the clipboard did not respond."
148
- : result.failure === "size" ? "Selection exceeds this clipboard route's size limit."
149
- : "Could not copy the selection; the clipboard is unavailable.");
150
- this.#terminal.requestRender();
151
- },
152
144
  }) : null;
153
145
  constructed.push(() => { this.#responseCopy?.dispose(); this.#copyExecutor?.dispose(); });
154
146
  const readClipboard = async (signal) => {
@@ -183,10 +175,12 @@ export class OwnedUiSessionPresenter {
183
175
  replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
184
176
  }),
185
177
  enableDockInputReuse: inputPresentation?.viewportReuse !== false,
178
+ footerSessionName: options.engine.residentTab === undefined,
179
+ hideUtilityTabCommands: options.engine.residentTab !== undefined,
186
180
  persistentHistory: this.#customViewport && promptHistory !== undefined,
187
181
  ...(promptHistory === undefined ? {} : { historyEditor: promptHistory.editor }),
188
- onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
189
- onPasteRejected: error => this.#reportSubmissionError(error),
182
+ onSubmit: text => { void this.submit(text).catch(() => this.#submission.reportError()); },
183
+ onPasteRejected: error => this.#submission.reportError(error),
190
184
  onInterrupt: () => { void this.interrupt(); },
191
185
  onClear: () => { void this.clearOrExit(); },
192
186
  onExit: () => { void this.shutdown(); },
@@ -198,10 +192,11 @@ export class OwnedUiSessionPresenter {
198
192
  this.#terminal.requestRender();
199
193
  },
200
194
  onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
201
- onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
195
+ onFollowUp: () => { void this.queueFollowUp().catch(() => this.#submission.reportError()); },
202
196
  onDequeue: () => this.restoreQueuedInput(),
203
197
  onEditorChange: text => {
204
- this.#editorRevision++;
198
+ this.#submission.noteEditorChange();
199
+ this.#resident.checkpoint(text);
205
200
  promptSuggestionController?.abortPending();
206
201
  if (text.length === 0) {
207
202
  // Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
@@ -218,9 +213,9 @@ export class OwnedUiSessionPresenter {
218
213
  promptSuggestionController?.invalidate();
219
214
  this.#promptHistory?.synchronize();
220
215
  },
221
- onCopyText: text => {
216
+ onCopyText: (text, cut) => {
222
217
  if (this.#responseCopy !== null) {
223
- void this.#responseCopy.submitText(text);
218
+ this.#trackCopy(this.#responseCopy.submitText(text), cut ? undefined : text.length);
224
219
  return;
225
220
  }
226
221
  const write = () => {
@@ -263,8 +258,27 @@ export class OwnedUiSessionPresenter {
263
258
  // Invariant: the presenter owns its root (theme subscription and helper clients) and disposes it once the
264
259
  // terminal no longer renders it.
265
260
  constructed.push(() => this.root.dispose());
261
+ this.#resident = new ResidentTabCoordinator(options.engine.residentTab, { root: this.root });
262
+ constructed.push(() => this.#resident.dispose());
263
+ const startupRoute = options.engine.startupRoute;
264
+ this.#dialogs = new SessionDialogCoordinator({
265
+ terminal: this.#terminal,
266
+ root: this.root,
267
+ routeHost: routeHost ?? null,
268
+ disposed: () => this.#disposed,
269
+ exit: () => { void this.shutdown(); },
270
+ routeClosed: route => this.#resident.closeOwnedRoute(route),
271
+ ...(startupRoute === undefined ? {} : { startupRoute }),
272
+ });
273
+ constructed.push(() => this.#dialogs.dispose());
266
274
  const initialPiSettings = this.backend.settings.snapshot();
267
275
  this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
276
+ this.#login = new LoginInteraction({
277
+ terminal: this.#terminal, root: this.root, programStatus: this.#programStatus,
278
+ syncProgress: () => this.#syncTerminalProgress(this.view()),
279
+ });
280
+ this.#submission = this.#createSubmission(cwd);
281
+ this.#resident.restoreInput(options.history?.imageSidecar);
268
282
  this.#reloadPresentation = reloadPresentation;
269
283
  const presentationInterval = streamPresentationOptions?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
270
284
  this.#streamPresentation = streamPresentationOptions?.scheduler === undefined
@@ -276,18 +290,7 @@ export class OwnedUiSessionPresenter {
276
290
  generator: promptSuggestionOptions.generator,
277
291
  enabled: promptSuggestionOptions.enabled(),
278
292
  ...(promptSuggestionOptions.diagnostics === undefined ? {} : { diagnostics: promptSuggestionOptions.diagnostics }),
279
- surface: {
280
- canPresent: identity => this.#promptSuggestionPresentationBlockReason(identity) === null,
281
- presentationBlockReason: identity => this.#promptSuggestionPresentationBlockReason(identity),
282
- present: text => {
283
- if (!this.root.canPresentPromptSuggestion())
284
- return false;
285
- this.root.setPromptSuggestion(text);
286
- return true;
287
- },
288
- clear: () => this.root.setPromptSuggestion(null),
289
- requestRender: () => this.#terminal.requestRender(),
290
- },
293
+ surface: promptSuggestionSurface(this.root, identity => this.#promptSuggestionPresentationBlockReason(identity), () => this.#terminal.requestRender()),
291
294
  });
292
295
  this.#promptSuggestions = promptSuggestionController;
293
296
  constructed.push(() => this.#promptSuggestions?.dispose());
@@ -310,35 +313,7 @@ export class OwnedUiSessionPresenter {
310
313
  this.#terminal.requestRender();
311
314
  });
312
315
  constructed.push(this.#unsubscribeSkills);
313
- this.#terminal.setHardwareCursor(initialPiSettings.showHardwareCursor);
314
- this.#terminal.setClearOnShrink(initialPiSettings.clearOnShrink);
315
- this.#terminalProgressEnabled = initialPiSettings.showTerminalProgress;
316
316
  this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
317
- // Invariant: bare A1 prints only the resume hint at exit, so the pinned exit-output
318
- // choice is hidden there and cannot be bound; the comparison profile binds and honors it.
319
- this.#unbindShutdownSettings = this.backend.settings.productMode === "bare" ? () => { } : this.backend.settings.bindOwner("shutdown", {
320
- fullscreenExitOutput: { apply() { } },
321
- });
322
- constructed.push(this.#unbindShutdownSettings);
323
- this.#unbindTerminalSettings = this.backend.settings.bindOwner("terminal", {
324
- showHardwareCursor: { apply: value => {
325
- if (typeof value !== "boolean")
326
- throw new TypeError("Hardware cursor setting is invalid");
327
- this.#terminal.setHardwareCursor(value);
328
- } },
329
- clearOnShrink: { apply: value => {
330
- if (typeof value !== "boolean")
331
- throw new TypeError("Clear-on-shrink setting is invalid");
332
- this.#terminal.setClearOnShrink(value);
333
- } },
334
- showTerminalProgress: { apply: value => {
335
- if (typeof value !== "boolean")
336
- throw new TypeError("Terminal progress setting is invalid");
337
- this.#terminalProgressEnabled = value;
338
- this.#syncTerminalProgress(this.view());
339
- } },
340
- });
341
- constructed.push(this.#unbindTerminalSettings);
342
317
  const applyViewportSettings = () => {
343
318
  const snapshot = viewportSettings?.snapshot();
344
319
  this.root.setViewportConfig(snapshot ?? {
@@ -348,74 +323,23 @@ export class OwnedUiSessionPresenter {
348
323
  });
349
324
  };
350
325
  applyViewportSettings();
351
- this.#unsubscribeSettings = this.#customViewport && viewportSettings
352
- ? viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
353
- : () => { };
326
+ const unsubscribeViewport = this.#customViewport && viewportSettings ? viewportSettings.onChange(settings => this.root.setViewportConfig(settings)) : () => { };
327
+ const unsubscribeHistoryScope = this.#customViewport && promptHistory?.onScopeChange?.(scope => this.#rebindTranscript(scope));
328
+ this.#unsubscribeSettings = () => { unsubscribeViewport(); if (unsubscribeHistoryScope)
329
+ unsubscribeHistoryScope(); };
354
330
  constructed.push(this.#unsubscribeSettings);
355
- this.root.setEditorPaddingX(initialPiSettings.editorPaddingX);
356
- this.root.setAutocompleteMaxVisible(initialPiSettings.autocompleteMaxVisible);
357
- this.root.setOutputPad(initialPiSettings.outputPad);
358
- this.root.setFullscreenCopyOnSelect(initialPiSettings.fullscreenCopyOnSelect);
359
- this.root.setHideThinkingBlock(initialPiSettings.hideThinkingBlock);
360
- this.root.setMermaidRenderingMode(initialPiSettings.mermaidRenderingMode);
361
- this.#showImages = initialPiSettings.showImages;
362
- this.#imageWidthCells = initialPiSettings.imageWidthCells;
363
- this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
364
- this.#unbindPiSettings = this.backend.settings.bindOwner("shell", {
365
- editorPaddingX: { apply: value => {
366
- if (typeof value !== "number")
367
- throw new TypeError("Editor padding is invalid");
368
- this.root.setEditorPaddingX(value);
369
- } },
370
- autocompleteMaxVisible: { apply: value => {
371
- if (typeof value !== "number")
372
- throw new TypeError("Autocomplete maximum is invalid");
373
- this.root.setAutocompleteMaxVisible(value);
374
- } },
375
- outputPad: { apply: value => {
376
- if (value !== 0 && value !== 1)
377
- throw new TypeError("Output padding is invalid");
378
- this.root.setOutputPad(value);
379
- } },
380
- hideThinkingBlock: { apply: value => {
381
- if (typeof value !== "boolean")
382
- throw new TypeError("Thinking-block visibility is invalid");
383
- this.root.setHideThinkingBlock(value);
384
- } },
385
- mermaidRenderingMode: { apply: value => {
386
- if (value !== "off" && value !== "final" && value !== "streaming")
387
- throw new TypeError("Mermaid mode is invalid");
388
- this.root.setMermaidRenderingMode(value);
389
- } },
390
- showImages: { apply: value => {
391
- if (typeof value !== "boolean")
392
- throw new TypeError("Image visibility is invalid");
393
- this.#showImages = value;
394
- this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
395
- } },
396
- imageWidthCells: { apply: value => {
397
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
398
- throw new TypeError("Image width is invalid");
399
- this.#imageWidthCells = value;
400
- this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
401
- } },
402
- ...(this.#customViewport ? { fullscreenCopyOnSelect: { apply: (value) => {
403
- if (typeof value !== "boolean")
404
- throw new TypeError("Fullscreen copy-on-select setting is invalid");
405
- this.root.setFullscreenCopyOnSelect(value);
406
- } } } : {
407
- fullscreenWheelScrollLines: { apply: (value) => {
408
- if (value !== "auto" && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 100)) {
409
- throw new TypeError("Fullscreen wheel-scroll distance is invalid");
410
- }
411
- this.#terminal.setWheelScrollLines(value);
412
- } },
413
- }),
414
- });
415
- constructed.push(this.#unbindPiSettings);
331
+ this.#unbindSessionSettings = bindSessionSettings(this.backend.settings, initialPiSettings, {
332
+ terminal: this.#terminal,
333
+ root: this.root,
334
+ setTerminalProgress: enabled => {
335
+ this.#terminalProgressEnabled = enabled;
336
+ if (this.#terminal.running)
337
+ this.#syncTerminalProgress(this.view());
338
+ },
339
+ }, { customViewport: this.#customViewport });
340
+ constructed.push(this.#unbindSessionSettings);
416
341
  if (this.#customViewport && promptHistory !== undefined) {
417
- this.#promptHistoryImageSidecar = promptHistory.imageSidecar;
418
- const sidecar = this.#promptHistoryImageSidecar;
342
+ this.#historyBinding = { options: promptHistory, scope: promptHistory.scope ?? "unique", sessionId: promptHistory.sessionId, imageSidecar: promptHistory.imageSidecar };
419
343
  this.#promptHistory = new PromptHistoryController({
420
344
  editor: this.root.editor,
421
345
  store: promptHistory.store,
@@ -423,57 +347,33 @@ export class OwnedUiSessionPresenter {
423
347
  fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
424
348
  active: () => this.root.usesDefaultInputSurface(),
425
349
  render: () => this.#terminal.requestRender(),
426
- rehydrate: value => this.root.rehydrateHistoryText(value, id => sidecar?.readAttachment(id) ?? null),
350
+ rehydrate: value => this.root.rehydrateHistoryText(value, id => this.#historyBinding?.imageSidecar?.readAttachment(id) ?? null),
427
351
  });
428
352
  constructed.push(() => { void this.#promptHistory?.close().catch(() => false); });
429
353
  }
430
- this.#extensionBridge = createPiExtensionUiBridge({
431
- runtime: {
432
- getColumns: () => this.#terminal.viewport().columns,
433
- getRows: () => this.#terminal.viewport().rows,
434
- requestRender: () => this.#terminal.requestRender(),
435
- },
436
- agentDir: this.backend.identity.agentDir,
437
- setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
438
- setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
439
- showOverlay: (component, overlayOptions) => this.#terminal.showOverlay(component, overlayOptions),
440
- listenInput: handler => this.#terminal.addInputListener(handler),
441
- replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
442
- replaceHeader: component => this.root.setExtensionHeader(component),
443
- replaceFooter: component => this.root.setExtensionFooter(component),
444
- setStatus: (key, text) => this.root.setExtensionStatus(key, text),
445
- setWorking: (message, visible) => this.root.setExtensionWorking(message, visible),
446
- notify: (message, type) => this.root.addExtensionNotification(message, type),
447
- setTitle: title => this.#terminal.setTitle(title),
448
- getEditorText: () => this.root.editor.getText(),
449
- setEditorText: text => this.root.editor.setText(text),
450
- pasteToEditor: text => this.root.editor.insertText(text),
451
- addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
452
- setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
453
- getFooterData: () => this.root.extensionFooterData(),
454
- getToolsExpanded: () => this.root.toolsExpanded,
455
- setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
456
- });
354
+ this.#extensionBridge = this.#createExtensionBridge();
457
355
  constructed.push(() => this.#extensionBridge.dispose());
356
+ this.#concerns = this.#createConcerns();
458
357
  constructed.push(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
459
358
  this.backend.workflows.setWorkflowInteractionHost({
460
- startLogin: request => this.#startWorkflowLogin(request),
461
- prompt: request => this.#requestWorkflowInput(request),
462
- notify: event => this.#notifyWorkflowLogin(event),
359
+ startLogin: request => this.#login.start(request),
360
+ prompt: request => this.#login.requestInput(request),
361
+ notify: event => this.#login.notify(event),
463
362
  publish: message => {
464
363
  this.root.appendWorkflowMessage(message);
465
364
  this.#terminal.requestRender();
466
365
  },
467
- finishLogin: () => this.#finishWorkflowLogin(),
366
+ finishLogin: () => this.#login.finish(),
468
367
  });
469
368
  this.#installAutocompleteCommands();
470
369
  this.#unsubscribe = this.backend.session.onEvent(event => {
471
370
  // Invariant: once disposal begins no event touches the view; only the stop still settles the shell.
472
371
  if (this.#disposed) {
473
372
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
474
- this.#settleStoppedLifecycle();
373
+ this.#stop.settleAfter(() => this.dispose());
475
374
  return;
476
375
  }
376
+ this.#resident.observeLifecycle(event);
477
377
  // Performance: a streamed chunk names one block, and touching only that block is what keeps the
478
378
  // cost of a chunk the same in a long session as in a new one. Everything else
479
379
  // resynchronizes the view, which is cheap next to re-reading the transcript.
@@ -489,28 +389,14 @@ export class OwnedUiSessionPresenter {
489
389
  }
490
390
  if (event.type === "assistant-message-completed") {
491
391
  this.root.noteCompletedAssistantMessage();
492
- const identity = {
493
- sessionId: event.sessionId,
494
- sessionGeneration: event.sessionGeneration,
495
- runSequence: event.runSequence,
496
- responseSequence: event.responseSequence,
497
- model: event.model,
498
- };
499
- if (event.model === null)
500
- this.#promptSuggestions?.skip(identity, "no-model");
501
- else
502
- this.#promptSuggestions?.consider({ ...identity, model: event.model }, !event.successful ? "failed-response"
503
- : event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
504
- : event.stopReason !== "stop" ? "incomplete-response"
505
- : event.assistantMessageCount < 2 ? "early-conversation"
506
- : this.root.promptSuggestionPrepareBlockReason());
392
+ routeResponseToSuggestions(this.#promptSuggestions, event, () => this.root.promptSuggestionPrepareBlockReason());
507
393
  }
508
394
  const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
509
395
  const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.identity.sessionGeneration
510
396
  ? this.#syncBlock(event.block)
511
397
  : semanticOnly ? this.view() : this.#syncView();
512
398
  this.#syncTerminalProgress(view);
513
- const currentModelKey = modelKey(view);
399
+ const currentModelKey = suggestionModelKey(view);
514
400
  if (this.backend.identity.sessionGeneration !== this.#sessionGeneration || currentModelKey !== this.#suggestionModelKey) {
515
401
  this.#promptSuggestions?.invalidate();
516
402
  this.#suggestionModelKey = currentModelKey;
@@ -519,24 +405,10 @@ export class OwnedUiSessionPresenter {
519
405
  this.#promptSuggestions?.invalidate();
520
406
  }
521
407
  if (event.type === "agent-run-settled") {
522
- const identity = {
523
- sessionId: event.sessionId,
524
- sessionGeneration: event.sessionGeneration,
525
- runSequence: event.runSequence,
526
- responseSequence: event.responseSequence,
527
- model: event.model,
528
- };
529
- if (event.model === null)
530
- this.#promptSuggestions?.skip(identity, "no-model");
531
- else
532
- this.#promptSuggestions?.settle({ ...identity, model: event.model }, !event.successful ? "failed-response"
533
- : event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
534
- : event.stopReason !== "stop" ? "incomplete-response"
535
- : event.assistantMessageCount < 2 ? "early-conversation"
536
- : this.root.promptSuggestionPrepareBlockReason());
408
+ routeResponseToSuggestions(this.#promptSuggestions, event, () => this.root.promptSuggestionPrepareBlockReason());
537
409
  }
538
410
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
539
- this.#settleStoppedLifecycle();
411
+ this.#stop.settleAfter(() => this.dispose());
540
412
  });
541
413
  constructed.push(this.#unsubscribe);
542
414
  this.#unbindClipboardWriter = this.#responseCopy === null ? () => { } : this.backend.extensions.bindClipboardWriter(async (text) => {
@@ -549,7 +421,7 @@ export class OwnedUiSessionPresenter {
549
421
  });
550
422
  constructed.push(this.#unbindClipboardWriter);
551
423
  if (this.backend.session.view().lifecycle === "stopped")
552
- this.#resolveStopped?.();
424
+ this.#stop.resolve();
553
425
  }
554
426
  catch (error) {
555
427
  for (const release of constructed.reverse()) {
@@ -626,29 +498,25 @@ export class OwnedUiSessionPresenter {
626
498
  const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.#terminal.hasFocusedOverlay());
627
499
  if (routed.copySelection !== undefined) {
628
500
  const snapshot = routed.copySelection;
629
- const acknowledgement = copyAcknowledgement(snapshot);
630
- const intent = ++this.#copyIntentSequence;
631
- void this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite).then(result => {
632
- if (this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
633
- || result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
634
- return;
635
- if (acknowledgement.hasNonWhitespace) {
636
- this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
637
- }
638
- });
501
+ this.#trackCopy(this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite), copyCharacters(snapshot));
639
502
  }
503
+ if (routed.copyLastMessage)
504
+ void this.runWorkflow({ command: "copy", argument: "" });
640
505
  if (!routed.consumed)
641
506
  return routed.data === data ? undefined : { data: routed.data };
642
507
  return routed.data.length === 0 ? { consume: true } : { data: routed.data };
643
508
  }
509
+ #trackCopy(delivery, count) {
510
+ const intent = ++this.#copyIntentSequence;
511
+ void delivery?.then(({ outcome }) => {
512
+ if (count === undefined || this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
513
+ || outcome !== "delivered" && outcome !== "submitted-unverified")
514
+ return;
515
+ this.root.showCopyAcknowledgement(copyAcknowledgementText(count));
516
+ });
517
+ }
644
518
  #promptSuggestionPresentationBlockReason(identity) {
645
- const view = this.view();
646
- if (this.#disposed)
647
- return "disposed";
648
- if (identity.sessionId !== view.sessionId || identity.sessionGeneration !== this.backend.identity.sessionGeneration
649
- || modelKey(view) !== `${identity.model.providerId}/${identity.model.modelId}`)
650
- return "stale-identity";
651
- return this.root.promptSuggestionPresentationBlockReason();
519
+ return suggestionPresentationBlockReason(identity, { disposed: this.#disposed, view: this.view(), sessionGeneration: this.backend.identity.sessionGeneration }, () => this.root.promptSuggestionPresentationBlockReason());
652
520
  }
653
521
  start() {
654
522
  if (this.#started)
@@ -663,7 +531,7 @@ export class OwnedUiSessionPresenter {
663
531
  setImmediate(() => {
664
532
  if (this.#disposed || !this.#customViewport)
665
533
  return;
666
- this.#presentStartupRoute();
534
+ this.#dialogs.presentStartupRoute();
667
535
  void this.#loadLazySelectors().then(selectors => selectors.prepare()).catch(() => { });
668
536
  this.root.warmPastePreparation();
669
537
  this.#copyExecutor?.warm();
@@ -675,127 +543,14 @@ export class OwnedUiSessionPresenter {
675
543
  return () => this.#listeners.delete(listener);
676
544
  }
677
545
  waitUntilStopped() {
678
- return this.#stopped;
546
+ return this.#stop.stopped;
679
547
  }
680
548
  async submit(text) {
681
- return this.#submitWhenReady(text, () => this.#submit(text));
682
- }
683
- #submitWhenReady(draft, action) {
684
- const previous = this.#waitingImages.get(draft);
685
- if (previous !== undefined)
686
- return previous.result;
687
- if (!this.root.hasPendingPastes(draft))
688
- return this.#guardSubmission(draft, action);
689
- const controller = new AbortController();
690
- const generation = this.backend.identity.sessionGeneration;
691
- const result = this.#guardSubmission(draft, async () => {
692
- await this.root.waitForPromptPastes(draft, controller.signal);
693
- if (controller.signal.aborted || this.#disposed || this.backend.identity.sessionGeneration !== generation)
694
- return rejected("image submission canceled");
695
- return action();
696
- }).finally(() => {
697
- this.#waitingImages.delete(draft);
698
- if (!this.#disposed)
699
- this.#showWaitingImages();
700
- });
701
- this.#waitingImages.set(draft, { controller, result });
702
- if (this.root.editor.getText() === draft)
703
- this.root.editor.setText("");
704
- this.#showWaitingImages();
705
- return result;
706
- }
707
- #showWaitingImages() {
708
- const count = this.#waitingImages.size;
709
- this.root.setExtensionWidget("owned-image-preparation", count === 0 ? null : {
710
- render: width => [...renderPiShellStatusText(`Waiting for images (${count} submission${count === 1 ? "" : "s"}) — Esc cancels; dequeue restores`, width)],
711
- invalidate: () => { },
712
- }, "aboveEditor");
713
- this.#terminal.requestRender();
714
- }
715
- #cancelWaitingImages() {
716
- for (const item of this.#waitingImages.values())
717
- item.controller.abort();
718
- }
719
- #rememberInput(text, kind) {
720
- this.root.editor.addToHistory(text);
721
- if (this.#promptHistory !== null) {
722
- const reusable = this.root.prepareHistoryText(text);
723
- if (reusable.length === 0)
724
- this.#promptHistory.rememberRecovery(text);
725
- else {
726
- this.#persistImageSidecars(text);
727
- this.#promptHistory.capture(reusable, kind, this.#cwd, this.backend.identity.sessionId);
728
- }
729
- }
730
- }
731
- // Rationale: write the image chip payloads referenced by an editor draft to the sidecar
732
- // directory before their history row commits. Missing sidecar dependency = silent skip; the
733
- // recall path treats an absent sidecar the same as a corrupt one.
734
- #persistImageSidecars(text) {
735
- const sidecar = this.#promptHistoryImageSidecar;
736
- if (sidecar === undefined)
737
- return;
738
- for (const record of this.root.imageChipAttachmentsFromEditor(text)) {
739
- sidecar.write(record.id, { tag: record.tag, data: record.image.data, mimeType: record.image.mimeType, savedAt: new Date().toISOString() });
740
- }
741
- }
742
- async #submit(text) {
743
- this.#promptSuggestions?.invalidate();
744
- const displayInput = text.trim();
745
- if (!displayInput)
746
- return { outcome: "completed", diagnostic: null };
747
- if (displayInput.startsWith("/"))
748
- return this.#slashCommand(displayInput);
749
- const prepared = this.root.preparePromptSubmission(displayInput);
750
- assertPromptImages(prepared.images);
751
- const input = prepared.text.trim();
752
- if (input.startsWith("!")) {
753
- const excludeFromContext = input.startsWith("!!");
754
- const command = input.slice(excludeFromContext ? 2 : 1).trim();
755
- if (command) {
756
- this.#rememberInput(displayInput, "bash");
757
- try {
758
- const result = await this.backend.workflows.executeBashWorkflow(command, excludeFromContext);
759
- const workflow = {
760
- command: "debug",
761
- outcome: result.cancelled ? "cancelled" : result.exitCode === 0 || result.exitCode === undefined ? "completed" : "failed",
762
- message: result.cancelled ? "Bash command cancelled" : `Bash exited ${result.exitCode ?? 0}: ${command}`,
763
- ...(result.output ? { detail: result.output } : {}),
764
- };
765
- this.root.appendWorkflowResult(workflow);
766
- this.#terminal.requestRender();
767
- return workflow.outcome === "failed" ? rejected(workflow.message) : { outcome: "completed", diagnostic: null };
768
- }
769
- catch (error) {
770
- const message = `Bash command failed: ${error instanceof Error ? error.message : String(error)}`;
771
- this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message });
772
- this.#terminal.requestRender();
773
- return rejected(message);
774
- }
775
- }
776
- }
777
- // Compatibility: match interactive Pi: input during compaction is queued steering; the engine
778
- // shows it in the pending rows and delivers it when compaction ends.
779
- const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
780
- this.#rememberInput(this.root.omitUnsentPromptImages(displayInput).trim(), type);
781
- this.root.resumeViewportFollowing();
782
- const finishSending = prepared.images.length === 0 ? () => { } : this.root.beginImageSubmissionStatus();
783
- try {
784
- return await this.#execute({
785
- type,
786
- correlationId: this.#correlation(type),
787
- sessionId: this.backend.identity.sessionId,
788
- text: input,
789
- ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
790
- }, displayInput);
791
- }
792
- finally {
793
- finishSending();
794
- }
549
+ return this.#submission.submit(text);
795
550
  }
796
551
  async clearOrExit(now = Date.now()) {
797
552
  this.#promptSuggestions?.abortPending();
798
- if (now - this.#lastClearTime < 500)
553
+ if (now - this.#lastClearTime < CLEAR_EXIT_INTERVAL_MS)
799
554
  return this.shutdown();
800
555
  this.root.editor.setText("");
801
556
  this.#lastClearTime = now;
@@ -804,8 +559,8 @@ export class OwnedUiSessionPresenter {
804
559
  }
805
560
  async interrupt(now = Date.now()) {
806
561
  this.#promptSuggestions?.invalidate();
807
- if (this.#waitingImages.size > 0) {
808
- this.#cancelWaitingImages();
562
+ if (this.#submission.waiting) {
563
+ this.#submission.cancelWaiting();
809
564
  return { outcome: "completed", diagnostic: null };
810
565
  }
811
566
  if (this.view().lifecycle === "busy")
@@ -832,23 +587,23 @@ export class OwnedUiSessionPresenter {
832
587
  return { outcome: "completed", diagnostic: null };
833
588
  }
834
589
  async abort() {
835
- return this.#execute(this.#simple("abort"));
590
+ return this.#submission.execute(this.#simple("abort"));
836
591
  }
837
592
  async retry() {
838
593
  this.#promptSuggestions?.invalidate();
839
- return this.#execute(this.#simple("retry"));
594
+ return this.#submission.execute(this.#simple("retry"));
840
595
  }
841
596
  async compact() {
842
597
  this.#promptSuggestions?.invalidate();
843
- return this.#execute(this.#simple("compact"));
598
+ return this.#submission.execute(this.#simple("compact"));
844
599
  }
845
600
  async newSession() {
846
601
  this.#promptSuggestions?.invalidate();
847
- return this.#execute(this.#simple("new-session"));
602
+ return this.#submission.execute(this.#simple("new-session"));
848
603
  }
849
604
  async resumeSession(sessionPath) {
850
605
  this.#promptSuggestions?.invalidate();
851
- return this.#execute({
606
+ return this.#submission.execute({
852
607
  type: "resume-session",
853
608
  correlationId: this.#correlation("resume"),
854
609
  sessionId: this.backend.identity.sessionId,
@@ -857,7 +612,7 @@ export class OwnedUiSessionPresenter {
857
612
  }
858
613
  async setModel(providerId, modelId) {
859
614
  this.#promptSuggestions?.invalidate();
860
- return this.#execute({
615
+ return this.#submission.execute({
861
616
  type: "set-model",
862
617
  correlationId: this.#correlation("model"),
863
618
  sessionId: this.backend.identity.sessionId,
@@ -865,7 +620,7 @@ export class OwnedUiSessionPresenter {
865
620
  });
866
621
  }
867
622
  async setThinkingLevel(thinkingLevel) {
868
- return this.#execute({
623
+ return this.#submission.execute({
869
624
  type: "set-thinking-level",
870
625
  correlationId: this.#correlation("thinking"),
871
626
  sessionId: this.backend.identity.sessionId,
@@ -884,64 +639,13 @@ export class OwnedUiSessionPresenter {
884
639
  return workflowAdapterResult(result);
885
640
  }
886
641
  async queueFollowUp() {
887
- const draft = this.root.editor.getText();
888
- return this.#submitWhenReady(draft, () => this.#queueFollowUp(draft));
889
- }
890
- async #queueFollowUp(draft) {
891
- const displayInput = draft.trim();
892
- if (!displayInput)
893
- return rejected("nothing to queue");
894
- const prepared = this.root.preparePromptSubmission(displayInput);
895
- assertPromptImages(prepared.images);
896
- const text = prepared.text.trim();
897
- this.#rememberInput(displayInput, "follow-up");
898
- if (this.root.editor.getText() === draft)
899
- this.root.editor.setText("");
900
- this.root.resumeViewportFollowing();
901
- return this.#execute({
902
- type: "follow-up",
903
- correlationId: this.#correlation("follow-up"),
904
- sessionId: this.backend.identity.sessionId,
905
- text,
906
- ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
907
- }, displayInput);
642
+ return this.#submission.queueFollowUp(this.root.editor.getText());
908
643
  }
909
644
  restoreQueuedInput() {
910
- const queued = [...this.#waitingImages.keys(), ...this.backend.workflows.clearQueuedWorkflows()];
911
- this.#cancelWaitingImages();
912
- if (queued.length === 0)
913
- return;
914
- this.root.editor.setText(queued.join("\n"));
915
- this.#terminal.requestRender();
645
+ this.#submission.restoreQueued();
916
646
  }
917
647
  showSelector(title, options, onSelect, onCancel) {
918
- this.#dialogHandle?.hide();
919
- this.#dialogSource = "local";
920
- this.#dialogId = undefined;
921
- const component = createPiShellSelector({
922
- title,
923
- options,
924
- onSelect: id => {
925
- onSelect(id);
926
- this.#dialogHandle?.hide();
927
- this.#dialogHandle = undefined;
928
- this.#dialogSource = undefined;
929
- },
930
- onCancel: () => {
931
- this.#dialogHandle?.hide();
932
- this.#dialogHandle = undefined;
933
- this.#dialogSource = undefined;
934
- onCancel?.();
935
- },
936
- });
937
- const handle = this.#terminal.showOverlay(component, {
938
- width: "70%",
939
- maxHeight: "80%",
940
- anchor: "center",
941
- inputCoordination: "owned",
942
- });
943
- this.#dialogHandle = handle;
944
- return handle;
648
+ return this.#dialogs.showSelector(title, options, onSelect, onCancel);
945
649
  }
946
650
  // Rationale: presenters see the shell only as a dialog host; selectors open in the input dock and run choices as workflows.
947
651
  #createDialogHost() {
@@ -1009,15 +713,12 @@ export class OwnedUiSessionPresenter {
1009
713
  }
1010
714
  /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
1011
715
  showSkillsSelector(skills = this.#skillSummaries()) {
1012
- const close = () => {
1013
- this.root.setInputSurface(null);
1014
- this.#terminal.requestRender();
1015
- };
716
+ const close = this.#dialogs.closeInputSurface;
1016
717
  const component = createPiShellSkillsSelector({
1017
718
  skills,
1018
719
  onSelect: name => {
1019
720
  close();
1020
- void this.#submitSkillPrompt(skillPrompt(name), skillPrompt(name)).catch(() => this.#reportSubmissionError());
721
+ void this.#submission.sendSlashPrompt(skillPrompt(name), skillPrompt(name)).catch(() => this.#submission.reportError());
1021
722
  },
1022
723
  onCancel: close,
1023
724
  });
@@ -1034,10 +735,7 @@ export class OwnedUiSessionPresenter {
1034
735
  this.#terminal.requestRender();
1035
736
  return;
1036
737
  }
1037
- const close = () => {
1038
- this.root.setInputSurface(null);
1039
- this.#terminal.requestRender();
1040
- };
738
+ const close = this.#dialogs.closeInputSurface;
1041
739
  const component = createPiShellUserMessageSelector(options, selection => {
1042
740
  close();
1043
741
  void this.runWorkflow({ command: "fork", argument: "", selection });
@@ -1064,10 +762,7 @@ export class OwnedUiSessionPresenter {
1064
762
  this.#terminal.requestRender();
1065
763
  return;
1066
764
  }
1067
- const close = () => {
1068
- this.root.setInputSurface(null);
1069
- this.#terminal.requestRender();
1070
- };
765
+ const close = this.#dialogs.closeInputSurface;
1071
766
  const component = createPiShellAuthProviderSelector("logout", options, selection => {
1072
767
  close();
1073
768
  void this.runWorkflow({ command: "logout", argument: "", selection });
@@ -1087,10 +782,7 @@ export class OwnedUiSessionPresenter {
1087
782
  void this.runWorkflow({ command: "login", argument: "", selection });
1088
783
  return;
1089
784
  }
1090
- const close = () => {
1091
- this.root.setInputSurface(null);
1092
- this.#terminal.requestRender();
1093
- };
785
+ const close = this.#dialogs.closeInputSurface;
1094
786
  const labels = method.options.map(option => option.label);
1095
787
  const component = createPiShellExtensionSelector(method.title, labels, label => {
1096
788
  const selection = method.options.find(option => option.label === label)?.id;
@@ -1108,10 +800,7 @@ export class OwnedUiSessionPresenter {
1108
800
  }, initialSelectedId);
1109
801
  }
1110
802
  showLoginAuthTypeSelector() {
1111
- const close = () => {
1112
- this.root.setInputSurface(null);
1113
- this.#terminal.requestRender();
1114
- };
803
+ const close = this.#dialogs.closeInputSurface;
1115
804
  const labels = ["Sign in with an account", "Sign in with an API key"];
1116
805
  const component = createPiShellExtensionSelector("Select authentication method:", labels, label => {
1117
806
  close();
@@ -1127,10 +816,7 @@ export class OwnedUiSessionPresenter {
1127
816
  this.#terminal.requestRender();
1128
817
  return;
1129
818
  }
1130
- const close = () => {
1131
- this.root.setInputSurface(null);
1132
- this.#terminal.requestRender();
1133
- };
819
+ const close = this.#dialogs.closeInputSurface;
1134
820
  const component = createPiShellAuthProviderSelector("login", options, id => {
1135
821
  close();
1136
822
  void this.runWorkflow({ command: "login", argument: "", selection: id });
@@ -1149,10 +835,22 @@ export class OwnedUiSessionPresenter {
1149
835
  this.#presenters.openSettingsSelector(this.#dialogHost);
1150
836
  }
1151
837
  shutdown() {
1152
- this.#shutdownPromise ??= this.#shutdown();
838
+ this.#shutdownPromise ??= this.#shutdown().finally(() => {
839
+ // Invariant: a rejected resident detach is retryable; ordinary shutdown has disposed the shell already.
840
+ if (!this.#disposed)
841
+ this.#shutdownPromise = undefined;
842
+ });
1153
843
  return this.#shutdownPromise;
1154
844
  }
1155
845
  async #shutdown() {
846
+ const detached = await this.#resident.detach();
847
+ if (detached === true)
848
+ return { outcome: "completed", diagnostic: null };
849
+ if (detached !== undefined) {
850
+ this.root.appendWorkflowStatus(detached);
851
+ this.#terminal.requestRender();
852
+ return rejected("resident tab detach is unavailable");
853
+ }
1156
854
  try {
1157
855
  const quit = this.backend.workflows.executeWorkflow({ command: "quit", argument: "" });
1158
856
  // Invariant: a slow or hung engine quit never holds the terminal past the cleanup deadline. The
@@ -1165,9 +863,16 @@ export class OwnedUiSessionPresenter {
1165
863
  return workflowAdapterResult(result);
1166
864
  }
1167
865
  finally {
1168
- this.#resolveStoppedLifecycle();
866
+ this.#stop.resolve();
1169
867
  }
1170
868
  }
869
+ #currentSessionFile() {
870
+ const direct = this.backend.identity.currentSessionFile?.();
871
+ if (direct !== undefined && direct !== null)
872
+ return direct;
873
+ const resume = this.backend.identity.currentSessionResumeMetadata();
874
+ return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
875
+ }
1171
876
  async runWorkflow(request) {
1172
877
  if (request.command === "quit")
1173
878
  return this.shutdown();
@@ -1188,68 +893,17 @@ export class OwnedUiSessionPresenter {
1188
893
  if (request.command === "login" && request.selection !== undefined) {
1189
894
  const setup = this.backend.catalog.pinnedAmbientAuthentication(request.selection);
1190
895
  if (setup) {
1191
- const close = () => {
1192
- this.root.setInputSurface(null);
1193
- this.#terminal.requestRender();
1194
- };
1195
- const dialog = createPiShellLoginDialog({
1196
- getColumns: () => this.#terminal.viewport().columns,
1197
- getRows: () => this.#terminal.viewport().rows,
1198
- requestRender: () => this.#terminal.requestRender(),
1199
- }, setup.providerId, close, setup.providerName, setup.title);
896
+ const close = this.#dialogs.closeInputSurface;
897
+ const dialog = createPiShellLoginDialog(componentRuntime(this.#terminal), setup.providerId, close, setup.providerName, setup.title);
1200
898
  dialog.showInfo(setup.message, [], true);
1201
899
  this.root.setInputSurface(dialog);
1202
900
  this.#terminal.requestRender();
1203
901
  return { outcome: "completed", diagnostic: null };
1204
902
  }
1205
903
  }
1206
- if (request.command === "scoped-models" && request.selection === undefined && request.confirmed === undefined) {
1207
- this.showScopedModelsSelector();
1208
- return { outcome: "completed", diagnostic: null };
1209
- }
1210
- if (request.command === "models" && request.selection === undefined) {
1211
- this.showModelsDialog(request.argument.trim() || undefined);
1212
- return { outcome: "completed", diagnostic: null };
1213
- }
1214
- if (request.command === "model" && request.selection === undefined && request.confirmed === undefined && request.argument.trim().length === 0) {
1215
- this.showModelSelector();
1216
- return { outcome: "completed", diagnostic: null };
1217
- }
1218
- if (request.command === "fork" && request.selection === undefined && request.confirmed === undefined) {
1219
- this.showForkSelector();
1220
- return { outcome: "completed", diagnostic: null };
1221
- }
1222
- if (request.command === "thinking" && request.selection === undefined && request.argument.trim().length === 0) {
1223
- await this.showThinkingSelector();
1224
- return { outcome: "completed", diagnostic: null };
1225
- }
1226
- if (request.command === "trust" && request.selection === undefined && request.confirmed === undefined) {
1227
- this.showTrustSelector();
1228
- return { outcome: "completed", diagnostic: null };
1229
- }
1230
- if (request.command === "settings" && request.selection === undefined && request.confirmed === undefined) {
1231
- this.showSettingsSelector();
1232
- return { outcome: "completed", diagnostic: null };
1233
- }
1234
- if (request.command === "resume" && request.selection === undefined && request.confirmed === undefined && request.argument.trim().length === 0) {
1235
- await this.showSessionSelector();
1236
- return { outcome: "completed", diagnostic: null };
1237
- }
1238
- if (request.command === "login" && request.selection === undefined && request.confirmed === undefined) {
1239
- if (request.argument.trim().length === 0)
1240
- this.showLoginAuthTypeSelector();
1241
- else
1242
- this.showLoginMethodSelector(request.argument);
1243
- return { outcome: "completed", diagnostic: null };
1244
- }
1245
- if (request.command === "logout" && request.selection === undefined && request.confirmed === undefined) {
1246
- await this.showLogoutSelector();
1247
- return { outcome: "completed", diagnostic: null };
1248
- }
1249
- if (request.command === "tree" && request.selection === undefined && request.confirmed === undefined && request.argument.trim().length === 0) {
1250
- await this.showTreeSelector();
1251
- return { outcome: "completed", diagnostic: null };
1252
- }
904
+ const selected = openWorkflowSelector(request, this, { residentTab: this.#resident.active });
905
+ if (selected !== undefined)
906
+ return selected;
1253
907
  if (request.command === "reload") {
1254
908
  const blocked = this.backend.workflows.reloadBlockedResult();
1255
909
  if (blocked) {
@@ -1263,11 +917,7 @@ export class OwnedUiSessionPresenter {
1263
917
  ? await import("./share-operation.js")
1264
918
  : undefined;
1265
919
  const shareSurface = shareDialog === undefined ? undefined
1266
- : (this.#customViewport ? shareDialog.createPiShellShareOperationDialog : shareDialog.createPiShellOperationLoader)({
1267
- getColumns: () => this.#terminal.viewport().columns,
1268
- getRows: () => this.#terminal.viewport().rows,
1269
- requestRender: () => this.#terminal.requestRender(),
1270
- }, "Creating gist…");
920
+ : (this.#customViewport ? shareDialog.createPiShellShareOperationDialog : shareDialog.createPiShellOperationLoader)(componentRuntime(this.#terminal), "Creating gist…");
1271
921
  const operationSurface = shareSurface ?? (request.command === "reload" ? createPiShellReloadBox() : undefined);
1272
922
  const now = this.#reloadPresentation?.now ?? Date.now;
1273
923
  const shownAt = now();
@@ -1277,7 +927,11 @@ export class OwnedUiSessionPresenter {
1277
927
  }
1278
928
  let result;
1279
929
  try {
1280
- result = await this.backend.workflows.executeWorkflow(shareSurface === undefined ? request : { ...request, signal: shareSurface.signal });
930
+ result = await this.backend.workflows.executeWorkflow(shareSurface === undefined ? request : {
931
+ ...request,
932
+ signal: shareSurface.signal,
933
+ onProgress: message => { shareSurface.setMessage?.(message); this.#terminal.requestRender(); },
934
+ });
1281
935
  }
1282
936
  finally {
1283
937
  if (operationSurface) {
@@ -1347,10 +1001,7 @@ export class OwnedUiSessionPresenter {
1347
1001
  }
1348
1002
  showTrustSelector() {
1349
1003
  const context = this.backend.catalog.pinnedProjectTrustContext();
1350
- const close = () => {
1351
- this.root.setInputSurface(null);
1352
- this.#terminal.requestRender();
1353
- };
1004
+ const close = this.#dialogs.closeInputSurface;
1354
1005
  const component = createPiShellTrustSelector({
1355
1006
  ...context,
1356
1007
  onSelect: selection => {
@@ -1414,18 +1065,13 @@ export class OwnedUiSessionPresenter {
1414
1065
  this.#copyIntentSequence += 1;
1415
1066
  this.#responseCopy?.dispose();
1416
1067
  this.#copyExecutor?.dispose();
1417
- this.#cancelWaitingImages();
1068
+ this.#submission.cancelWaiting();
1418
1069
  let pasteCleanup = Promise.resolve();
1419
1070
  let historyCleanup = Promise.resolve(true);
1420
1071
  attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
1421
1072
  attempt(() => { pasteCleanup = this.root.disposePendingPastes(); });
1422
1073
  attempt(() => this.root.clearViewportPointerState());
1423
1074
  attempt(() => this.#streamPresentation.dispose());
1424
- attempt(() => {
1425
- if (this.#startupRouteTimer !== undefined)
1426
- clearTimeout(this.#startupRouteTimer);
1427
- this.#startupRouteTimer = undefined;
1428
- });
1429
1075
  attempt(() => this.#promptSuggestions?.dispose());
1430
1076
  attempt(() => this.#unsubscribePromptSuggestions());
1431
1077
  attempt(() => this.#unsubscribePromptImages());
@@ -1433,21 +1079,18 @@ export class OwnedUiSessionPresenter {
1433
1079
  attempt(() => this.#unsubscribeSettings());
1434
1080
  let exitText = "";
1435
1081
  attempt(() => {
1436
- const exitMode = this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput;
1437
- const resume = this.backend.identity.currentSessionResumeMetadata();
1438
- const resumeHint = resume === null ? "" : `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
1439
- // Invariant: bare A1 leaves only the hint behind; the pinned comparison profile still
1440
- // honors fullscreenExitOutput, including the styled transcript.
1441
- exitText = terminal.mode !== "fullscreen" ? ""
1442
- : this.#customViewport || exitMode === "resume-hint" ? resumeHint
1443
- : [this.root.exitTranscript(terminal.columns), resumeHint].filter(Boolean).join("\n\n");
1082
+ exitText = composeExitText({
1083
+ terminalMode: terminal.mode,
1084
+ customViewport: this.#customViewport,
1085
+ exitMode: this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput,
1086
+ resume: this.backend.identity.currentSessionResumeMetadata(),
1087
+ transcript: () => this.root.exitTranscript(terminal.columns),
1088
+ });
1444
1089
  });
1445
1090
  attempt(() => this.#unbindClipboardWriter());
1446
- attempt(() => this.#unbindPiSettings());
1447
- attempt(() => this.#unbindTerminalSettings());
1448
- attempt(() => this.#unbindShutdownSettings());
1091
+ attempt(() => this.#unbindSessionSettings());
1449
1092
  attempt(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
1450
- attempt(() => this.#dialogHandle?.hide());
1093
+ this.#concerns.dispose(attempt);
1451
1094
  attempt(() => this.#extensionBridge.dispose());
1452
1095
  attempt(() => this.#programStatus.clear());
1453
1096
  // Invariant: the host settles a presenter only once the terminal is restored, so these never stall it.
@@ -1461,20 +1104,14 @@ export class OwnedUiSessionPresenter {
1461
1104
  }
1462
1105
  await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
1463
1106
  await boundedCleanup(() => pasteCleanup).catch(error => settled.push(error));
1107
+ await boundedCleanup(() => this.#resident.gracefulStop(this.#currentSessionFile()))
1108
+ .catch(error => settled.push(error));
1464
1109
  await boundedCleanup(() => this.backend.extensions.unbindExtensionUi()).catch(error => settled.push(error));
1465
1110
  return settled;
1466
1111
  };
1467
1112
  this.#released = { exitText, failures, settle };
1468
1113
  return this.#released;
1469
1114
  }
1470
- #settleStoppedLifecycle() {
1471
- void this.dispose().then(() => this.#resolveStoppedLifecycle(), () => this.#resolveStoppedLifecycle());
1472
- }
1473
- #resolveStoppedLifecycle() {
1474
- const resolve = this.#resolveStopped;
1475
- this.#resolveStopped = undefined;
1476
- resolve?.();
1477
- }
1478
1115
  // Invariant: incremental block updates notify listeners with the same complete view contract.
1479
1116
  #syncBlock(block) {
1480
1117
  this.root.applyTranscriptBlock(block);
@@ -1492,6 +1129,121 @@ export class OwnedUiSessionPresenter {
1492
1129
  listener(view);
1493
1130
  return view;
1494
1131
  }
1132
+ // Rationale: extensions reach the shell only through this bridge; every surface they draw lands in the root.
1133
+ #createExtensionBridge() {
1134
+ return createPiExtensionUiBridge({
1135
+ runtime: componentRuntime(this.#terminal),
1136
+ agentDir: this.backend.identity.agentDir,
1137
+ setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
1138
+ setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
1139
+ showOverlay: (component, overlayOptions) => this.#terminal.showOverlay(component, overlayOptions),
1140
+ listenInput: handler => this.#terminal.addInputListener(handler),
1141
+ replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
1142
+ replaceHeader: component => this.root.setExtensionHeader(component),
1143
+ replaceFooter: component => this.root.setExtensionFooter(component),
1144
+ setStatus: (key, text) => this.root.setExtensionStatus(key, text),
1145
+ setWorking: (message, visible) => this.root.setExtensionWorking(message, visible),
1146
+ notify: (message, type) => this.root.addExtensionNotification(message, type),
1147
+ setTitle: title => this.#terminal.setTitle(title),
1148
+ getEditorText: () => this.root.editor.getText(),
1149
+ setEditorText: text => this.root.editor.setText(text),
1150
+ pasteToEditor: text => this.root.editor.insertText(text),
1151
+ addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
1152
+ setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
1153
+ getFooterData: () => this.root.extensionFooterData(),
1154
+ getToolsExpanded: () => this.root.toolsExpanded,
1155
+ setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
1156
+ });
1157
+ }
1158
+ #createSubmission(cwd) {
1159
+ return new PromptSubmission({
1160
+ backend: this.backend,
1161
+ root: this.root,
1162
+ terminal: this.#terminal,
1163
+ residentTab: this.#resident,
1164
+ cwd,
1165
+ promptHistory: () => this.#promptHistory,
1166
+ imageSidecar: () => this.#historyBinding?.imageSidecar,
1167
+ disposed: () => this.#disposed,
1168
+ correlation: prefix => this.#correlation(prefix),
1169
+ lifecycle: () => this.view().lifecycle,
1170
+ currentSessionFile: () => this.#currentSessionFile(),
1171
+ slashCommand: text => this.#slashCommand(text),
1172
+ invalidateSuggestions: () => this.#promptSuggestions?.invalidate(),
1173
+ });
1174
+ }
1175
+ // Invariant: the order is the reset order on session replacement; copy intent before pastes before the input surface.
1176
+ #createConcerns() {
1177
+ return new SessionConcerns([
1178
+ { onSessionReplaced: () => { this.#copyIntentSequence += 1; this.#responseCopy?.reset(); } },
1179
+ this.#submission,
1180
+ { onSessionReplaced: () => this.root.resetPendingPastes() },
1181
+ { onSessionReplaced: () => this.#promptSuggestions?.invalidate() },
1182
+ { onSessionReplaced: () => { this.#sessionGeneration = this.backend.identity.sessionGeneration; this.#programStatus.reset(); } },
1183
+ { onSessionReplaced: () => this.#rebindTranscript() },
1184
+ this.#login,
1185
+ { onSessionReplaced: () => this.#extensionBridge.reset() },
1186
+ // Invariant: a replaced session takes its transient viewport and owned-route state with it.
1187
+ { onSessionReplaced: () => this.root.resetViewport() },
1188
+ this.#dialogs,
1189
+ { onSessionReplaced: () => this.root.setInputSurface(null) },
1190
+ { onSessionReplaced: () => this.root.resetExtensionUi() },
1191
+ { onSessionReplaced: () => this.root.resetWorkflowPresentation() },
1192
+ this.#resident,
1193
+ ]);
1194
+ }
1195
+ // Invariant: delivery recovery does not replace a store; conversation or live scope changes can.
1196
+ #rebindTranscript(scope) {
1197
+ const replaced = this.#sessionBindingGeneration !== this.backend.identity.sessionBindingGeneration;
1198
+ if (!replaced && scope === undefined)
1199
+ return;
1200
+ if (replaced) {
1201
+ this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
1202
+ this.root.resetTranscript();
1203
+ }
1204
+ const binding = this.#historyBinding;
1205
+ if (binding?.options.forSession !== undefined || binding?.options.forScope !== undefined) {
1206
+ const sessionId = this.backend.identity.currentHistorySessionId();
1207
+ const selectedScope = scope ?? binding.scope;
1208
+ if (selectedScope !== binding.scope || sessionId !== binding.sessionId) {
1209
+ if (!replaced)
1210
+ this.root.editor.recall?.restoreDraft?.();
1211
+ const previous = this.#promptHistory;
1212
+ this.#promptHistory = null;
1213
+ binding.imageSidecar = undefined;
1214
+ binding.scope = selectedScope;
1215
+ binding.sessionId = sessionId ?? undefined;
1216
+ void previous?.close().catch(() => false);
1217
+ const destination = binding.options.forScope !== undefined
1218
+ ? binding.options.forScope(selectedScope, sessionId)
1219
+ : sessionId === null ? null : binding.options.forSession(sessionId);
1220
+ if (destination !== null) {
1221
+ binding.imageSidecar = destination.imageSidecar;
1222
+ const controller = new PromptHistoryController({
1223
+ editor: this.root.editor,
1224
+ store: destination.store,
1225
+ limit: binding.options.limit,
1226
+ fallback: replaced ? this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []) : [],
1227
+ active: () => this.root.usesDefaultInputSurface(),
1228
+ render: () => this.#terminal.requestRender(),
1229
+ rehydrate: value => this.root.rehydrateHistoryText(value, id => this.#historyBinding?.imageSidecar?.readAttachment(id) ?? null),
1230
+ });
1231
+ this.#promptHistory = controller;
1232
+ if (this.#started)
1233
+ controller.start();
1234
+ }
1235
+ else {
1236
+ // Security: missing identity must not reopen Shared or retain another session's recall.
1237
+ const fallback = replaced ? this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []).reverse() : [];
1238
+ this.root.editor.recall?.reset();
1239
+ this.root.editor.recall?.replace(fallback);
1240
+ }
1241
+ return;
1242
+ }
1243
+ }
1244
+ if (replaced)
1245
+ this.#promptHistory?.reset(this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
1246
+ }
1495
1247
  #syncTerminalProgress(view) {
1496
1248
  if (!this.#terminal.running)
1497
1249
  return;
@@ -1511,34 +1263,10 @@ export class OwnedUiSessionPresenter {
1511
1263
  #syncView() {
1512
1264
  this.#streamPresentation.noteImmediatePresentation();
1513
1265
  const view = this.view();
1514
- if (this.backend.identity.sessionGeneration !== this.#sessionGeneration) {
1515
- this.#copyIntentSequence += 1;
1516
- this.#responseCopy?.reset();
1517
- this.#cancelWaitingImages();
1518
- this.root.resetPendingPastes();
1519
- this.#promptSuggestions?.invalidate();
1520
- this.#sessionGeneration = this.backend.identity.sessionGeneration;
1521
- this.#programStatus.reset();
1522
- // Invariant: delivery recovery invalidates callbacks, not same-session local recall or its draft.
1523
- if (this.#sessionBindingGeneration !== this.backend.identity.sessionBindingGeneration) {
1524
- this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
1525
- this.root.resetTranscript();
1526
- this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
1527
- }
1528
- this.#activeLoginDialog = undefined;
1529
- this.#extensionBridge.reset();
1530
- // Invariant: a replaced session takes its transient viewport and owned-route state with it.
1531
- this.root.resetViewport();
1532
- this.#dialogHandle?.hide();
1533
- this.#dialogHandle = undefined;
1534
- this.#dialogId = undefined;
1535
- this.#dialogSource = undefined;
1536
- this.root.setInputSurface(null);
1537
- this.root.resetExtensionUi();
1538
- this.root.resetWorkflowPresentation();
1539
- }
1266
+ if (this.backend.identity.sessionGeneration !== this.#sessionGeneration)
1267
+ this.#concerns.replaceSession();
1540
1268
  this.root.update(view);
1541
- this.#syncDialog(view.dialog);
1269
+ this.#dialogs.syncBackend(view.dialog);
1542
1270
  this.#presentStartupTrustWarning(view);
1543
1271
  this.#terminal.requestRender();
1544
1272
  for (const listener of this.#listeners)
@@ -1556,100 +1284,34 @@ export class OwnedUiSessionPresenter {
1556
1284
  this.#startupTrustHandled = true;
1557
1285
  this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
1558
1286
  }
1559
- #presentStartupRoute() {
1560
- const pending = this.#startupRoute;
1561
- if (pending === undefined || this.#disposed || !this.#terminal.running)
1287
+ openOwnedRoute(route) {
1288
+ if (this.#disposed)
1562
1289
  return;
1563
- if (this.#terminal.hasOverlay() || !this.root.usesDefaultInputSurface()) {
1564
- this.#startupRouteTimer = setTimeout(() => {
1565
- this.#startupRouteTimer = undefined;
1566
- this.#presentStartupRoute();
1567
- }, 25);
1568
- this.#startupRouteTimer.unref?.();
1290
+ // Invariant: a route requested before the runtime runs (the resident Settings bridge route arrives
1291
+ // before start()) is retained, latest wins, and presented through the startup-route path once started.
1292
+ if (!this.#terminal.running) {
1293
+ this.#dialogs.retainStartupRoute(route);
1569
1294
  return;
1570
1295
  }
1571
- this.#startupRoute = undefined;
1572
- this.#openOwnedRoute(pending.route, pending.input, pending.onClosed);
1573
- }
1574
- #openOwnedRoute(route, input, onClosed) {
1575
- const surface = this.#routeHost?.open(route, input) ?? null;
1576
- if (surface === null)
1577
- return { outcome: "failed", diagnostic: `route is unavailable: ${route}` };
1578
- if (!this.#terminal.running)
1579
- return { outcome: "failed", diagnostic: "runtime is not active" };
1580
- this.root.dismissNotice();
1581
- this.#dialogHandle?.hide();
1582
- this.#dialogSource = "route";
1583
- const handle = this.#terminal.openRoute(surface, {
1584
- exit: () => { void this.shutdown(); },
1585
- closed: rendered => {
1586
- if (this.#dialogHandle === handle) {
1587
- this.#dialogHandle = undefined;
1588
- this.#dialogId = undefined;
1589
- this.#dialogSource = undefined;
1590
- }
1591
- if (rendered) {
1592
- try {
1593
- void onClosed?.()?.catch(() => undefined);
1594
- }
1595
- catch { /* Rationale: a failed acknowledgement stays pending. */ }
1596
- }
1597
- },
1598
- });
1599
- this.#dialogHandle = handle;
1600
- this.#dialogId = surface.id;
1601
- return { outcome: "completed", diagnostic: null };
1602
- }
1603
- #syncDialog(dialog) {
1604
- if (!this.#terminal.running)
1605
- return;
1606
- if (dialog === null) {
1607
- // Invariant: locally owned routes (notably /settings) are independent of backend
1608
- // lifecycle/status events and remain open while an agent is working.
1609
- if (this.#dialogSource !== "backend")
1610
- return;
1611
- this.#dialogHandle?.hide();
1612
- this.#dialogHandle = undefined;
1613
- this.#dialogId = undefined;
1614
- this.#dialogSource = undefined;
1615
- return;
1296
+ const result = this.#dialogs.openRoute(route);
1297
+ if (result.outcome !== "completed" && result.diagnostic !== null) {
1298
+ this.root.appendWorkflowMessage({ kind: "warning", message: result.diagnostic });
1616
1299
  }
1617
- if (this.#dialogSource === "backend" && this.#dialogId === dialog.id)
1618
- return;
1619
- this.#dialogHandle?.hide();
1620
- this.#dialogSource = "backend";
1621
- const component = createPiShellDialog(dialog, {
1622
- onSelect: () => {
1623
- this.#dialogHandle?.hide();
1624
- this.#dialogHandle = undefined;
1625
- this.#dialogId = undefined;
1626
- this.#dialogSource = undefined;
1627
- },
1628
- onCancel: () => {
1629
- this.#dialogHandle?.hide();
1630
- this.#dialogHandle = undefined;
1631
- this.#dialogId = undefined;
1632
- this.#dialogSource = undefined;
1633
- },
1634
- });
1635
- this.#dialogHandle = this.#terminal.showOverlay(component, {
1636
- width: "70%",
1637
- maxHeight: "80%",
1638
- anchor: "center",
1639
- inputCoordination: "owned",
1640
- });
1641
- this.#dialogId = dialog.id;
1642
1300
  }
1643
1301
  hotkeysPresentation() {
1644
- return this.root.hotkeysPresentation();
1302
+ const presentation = this.root.hotkeysPresentation();
1303
+ return this.#resident.active ? { ...presentation, residentTabs: true } : presentation;
1645
1304
  }
1646
1305
  async #slashCommand(text) {
1647
1306
  const body = text.slice(1).trim();
1648
1307
  const separator = body.search(/\s/);
1649
1308
  const name = separator < 0 ? body : body.slice(0, separator);
1650
1309
  const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
1651
- if (this.#routeHost?.claims(name))
1652
- return this.#openOwnedRoute(name);
1310
+ const utilityTab = this.#resident.active ? RESIDENT_UTILITY_TAB_REFUSALS[name] : undefined;
1311
+ if (utilityTab !== undefined)
1312
+ return rejected(utilityTab);
1313
+ if (this.#dialogs.claimsRoute(name))
1314
+ return this.#dialogs.openRoute(name);
1653
1315
  if (this.#isWorkflowRoute(name))
1654
1316
  return this.runWorkflow({ command: name, argument });
1655
1317
  if (this.#skillsCollapsed()) {
@@ -1657,17 +1319,10 @@ export class OwnedUiSessionPresenter {
1657
1319
  return this.#runSkillsCommand(text, argument);
1658
1320
  const rewritten = rewriteSkillsTunnelSubmission(text);
1659
1321
  if (rewritten !== text)
1660
- return this.#submitSkillPrompt(rewritten, text);
1322
+ return this.#submission.sendSlashPrompt(rewritten, text);
1661
1323
  }
1662
1324
  // Compatibility: unknown slash input, prompt templates, skills, and extension commands remain Pi prompt input.
1663
- this.#rememberInput(text, "slash");
1664
- this.root.resumeViewportFollowing();
1665
- return this.#execute({
1666
- type: this.view().lifecycle === "busy" ? "steer" : "prompt",
1667
- correlationId: this.#correlation("prompt-command"),
1668
- sessionId: this.backend.identity.sessionId,
1669
- text,
1670
- });
1325
+ return this.#submission.sendSlashPrompt(text);
1671
1326
  }
1672
1327
  #installAutocompleteCommands() {
1673
1328
  this.root.editor.setAutocompleteCommands(this.backend.workflows.workflowAutocompleteCommands());
@@ -1701,18 +1356,7 @@ export class OwnedUiSessionPresenter {
1701
1356
  this.#terminal.requestRender();
1702
1357
  return { outcome: "failed", diagnostic: message };
1703
1358
  }
1704
- return this.#submitSkillPrompt(skillPrompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1705
- }
1706
- // Rationale: engine expands skill arguments; history retains typed input for recall.
1707
- #submitSkillPrompt(prompt, typed) {
1708
- this.#rememberInput(typed, "slash");
1709
- this.root.resumeViewportFollowing();
1710
- return this.#execute({
1711
- type: this.view().lifecycle === "busy" ? "steer" : "prompt",
1712
- correlationId: this.#correlation("prompt-command"),
1713
- sessionId: this.backend.identity.sessionId,
1714
- text: prompt,
1715
- });
1359
+ return this.#submission.sendSlashPrompt(skillPrompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1716
1360
  }
1717
1361
  #treeSummary() {
1718
1362
  return new Promise(resolve => {
@@ -1753,139 +1397,6 @@ export class OwnedUiSessionPresenter {
1753
1397
  if (result.diagnostic === "Branch summarization cancelled")
1754
1398
  await this.showTreeSelector(entryId);
1755
1399
  }
1756
- #startWorkflowLogin(request) {
1757
- this.#finishWorkflowLogin();
1758
- const dialog = createPiShellLoginDialog({
1759
- getColumns: () => this.#terminal.viewport().columns,
1760
- getRows: () => this.#terminal.viewport().rows,
1761
- requestRender: () => this.#terminal.requestRender(),
1762
- }, request.providerId, success => {
1763
- if (!success)
1764
- this.#finishWorkflowLogin();
1765
- }, request.providerName);
1766
- this.#activeLoginDialog = dialog;
1767
- this.root.setInputSurface(dialog);
1768
- this.#programStatus.setBlocked("authentication", {
1769
- kind: "auth",
1770
- message: `Log in to ${request.providerName}`,
1771
- });
1772
- this.#syncTerminalProgress(this.view());
1773
- this.#terminal.requestRender();
1774
- }
1775
- #requestWorkflowInput(request) {
1776
- const dialog = this.#activeLoginDialog;
1777
- if (!dialog)
1778
- return Promise.resolve(null);
1779
- if (request.type === "select") {
1780
- return new Promise(resolve => {
1781
- const options = request.options ?? [];
1782
- const labels = options.map(option => option.label);
1783
- const restoreDialog = () => {
1784
- if (this.#activeLoginDialog === dialog)
1785
- this.root.setInputSurface(dialog);
1786
- this.#terminal.requestRender();
1787
- };
1788
- const selector = createPiShellExtensionSelector(request.message, labels, label => {
1789
- const id = options.find(option => option.label === label)?.id;
1790
- restoreDialog();
1791
- resolve(id ?? null);
1792
- }, () => {
1793
- restoreDialog();
1794
- resolve(null);
1795
- });
1796
- this.root.setInputSurface(selector, false);
1797
- this.#terminal.requestRender();
1798
- });
1799
- }
1800
- const response = request.type === "manual-code"
1801
- ? dialog.showManualInput(request.message)
1802
- : dialog.showPrompt(request.message, request.placeholder);
1803
- this.#terminal.requestRender();
1804
- return response.then(value => value, () => null);
1805
- }
1806
- #notifyWorkflowLogin(event) {
1807
- const dialog = this.#activeLoginDialog;
1808
- if (!dialog)
1809
- return;
1810
- if (event.type === "auth_url")
1811
- dialog.showAuth(event.url, event.instructions);
1812
- else if (event.type === "device_code") {
1813
- dialog.showDeviceCode(event);
1814
- dialog.showWaiting("Waiting for authentication...");
1815
- }
1816
- else if (event.type === "info")
1817
- dialog.showInfo(event.message, event.links);
1818
- else if (event.type === "waiting")
1819
- dialog.showWaiting(event.message);
1820
- else
1821
- dialog.showProgress(event.message);
1822
- this.#terminal.requestRender();
1823
- }
1824
- #finishWorkflowLogin() {
1825
- if (!this.#activeLoginDialog)
1826
- return;
1827
- this.#activeLoginDialog = undefined;
1828
- this.root.setInputSurface(null);
1829
- this.#programStatus.setBlocked("authentication", undefined);
1830
- this.#syncTerminalProgress(this.view());
1831
- this.#terminal.requestRender();
1832
- }
1833
- async #execute(command, draft) {
1834
- if (draft === undefined)
1835
- return this.backend.session.execute(command);
1836
- const revision = this.#editorRevision;
1837
- try {
1838
- assertOwnedUiCommand(command);
1839
- }
1840
- catch (error) {
1841
- return this.#recoverSubmission(draft, revision, error);
1842
- }
1843
- try {
1844
- const result = await this.backend.session.execute(command);
1845
- if (result.outcome === "rejected")
1846
- return this.#recoverSubmission(draft, revision);
1847
- return result;
1848
- }
1849
- catch {
1850
- // Security: dispatch might already have reached the provider. Never retry automatically.
1851
- this.root.editor.addToHistory(draft);
1852
- this.#promptHistory?.rememberRecovery(draft);
1853
- this.#reportSubmissionError(undefined, "Submission failed; delivery is uncertain. Check the conversation before retrying. Press Up to recover the draft.");
1854
- return { outcome: "failed", diagnostic: "submission delivery is uncertain" };
1855
- }
1856
- }
1857
- async #guardSubmission(draft, action) {
1858
- const revision = this.#editorRevision;
1859
- try {
1860
- return await action();
1861
- }
1862
- catch (error) {
1863
- if (error instanceof ImageAttachmentError && error.code === "image-canceled")
1864
- return rejected("image submission canceled");
1865
- return this.#recoverSubmission(draft, revision, error);
1866
- }
1867
- }
1868
- #recoverSubmission(draft, revision, error) {
1869
- this.root.editor.addToHistory(draft);
1870
- this.#promptHistory?.rememberRecovery(draft);
1871
- // Concurrency: never overwrite input typed (even typed and cleared) after this submission.
1872
- if (revision === this.#editorRevision && this.root.editor.getText().length === 0)
1873
- this.root.editor.setText(draft);
1874
- const message = error instanceof ImageAttachmentError ? error.message : "Submission rejected. Check the prompt and attachments.";
1875
- this.#reportSubmissionError(error, error instanceof ImageAttachmentError && error.code === "image-count"
1876
- ? message
1877
- : `${message} Press Up to recover the draft.`);
1878
- return rejected(message);
1879
- }
1880
- #reportSubmissionError(error, message) {
1881
- // Security: arbitrary provider/extension error messages can contain the entire request.
1882
- try {
1883
- this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message: message
1884
- ?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") }, error instanceof ImageAttachmentError ? error.code : undefined);
1885
- this.#terminal.requestRender();
1886
- }
1887
- catch { /* Security: error presentation cannot create another rejected submission callback. */ }
1888
- }
1889
1400
  #simple(type) {
1890
1401
  return { type, correlationId: this.#correlation(type), sessionId: this.backend.identity.sessionId };
1891
1402
  }
@@ -1920,38 +1431,19 @@ export class OwnedUiSessionShell extends OwnedUiSessionPresenter {
1920
1431
  function isCoalescedStreamBlock(kind) {
1921
1432
  return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
1922
1433
  }
1923
- function copyAcknowledgement(snapshot) {
1434
+ function copyCharacters(snapshot) {
1924
1435
  if (snapshot.literal !== true)
1925
- return { characters: snapshot.sourceUnits, hasNonWhitespace: true };
1926
- let characters = Math.max(0, snapshot.rows.length - 1);
1927
- let hasNonWhitespace = false;
1928
- for (const row of snapshot.rows) {
1929
- characters += row.text.length;
1930
- hasNonWhitespace ||= /\S/u.test(row.text);
1931
- }
1932
- return { characters, hasNonWhitespace };
1933
- }
1934
- export function formatSessionResumeCommand(metadata) {
1935
- const args = [PRODUCT_TEXT.commandName];
1936
- if (!metadata.usesDefaultSessionDir)
1937
- args.push("--session-dir", quoteCommandArgument(metadata.sessionDir));
1938
- args.push("--session", metadata.sessionId);
1939
- return args.join(" ");
1940
- }
1941
- export function quoteCommandArgument(value) {
1942
- if (value.length > 0 && !/[^a-zA-Z0-9_\-./~:@]/.test(value))
1943
- return value;
1944
- return `'${value.replaceAll("'", `'\\''`)}'`;
1945
- }
1946
- function dim(value) {
1947
- return `\u001b[2m${value}\u001b[22m`;
1436
+ return snapshot.sourceUnits;
1437
+ let count = Math.max(0, snapshot.rows.length - 1), hasContent = false;
1438
+ for (const { text } of snapshot.rows) {
1439
+ count += text.length;
1440
+ hasContent ||= /\S/u.test(text);
1441
+ }
1442
+ return hasContent ? count : undefined;
1948
1443
  }
1949
1444
  function rejected(diagnostic) {
1950
1445
  return { outcome: "rejected", diagnostic };
1951
1446
  }
1952
- function modelKey(view) {
1953
- return view.activeModel === null ? "" : `${view.activeModel.providerId}/${view.activeModel.modelId}`;
1954
- }
1955
1447
  function workflowAdapterResult(result) {
1956
1448
  if (result.outcome === "completed")
1957
1449
  return { outcome: "completed", diagnostic: null };