@timurproko/a1 0.2.6-dev.736 → 0.2.7-dev.1221

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 (423) 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 +196 -908
  37. package/dist/app/session-shell/session-shell.d.ts +3 -8
  38. package/dist/app/session-shell/session-shell.js +324 -835
  39. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -2
  40. package/dist/app/session-shell/session-viewport-controller.js +29 -15
  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 +102 -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 +205 -153
  70. package/dist/composition/resident-tab-bridge.d.ts +25 -0
  71. package/dist/composition/resident-tab-bridge.js +180 -0
  72. package/dist/composition/session-backend.d.ts +19 -0
  73. package/dist/composition/session-backend.js +1 -0
  74. package/dist/composition/session-runtime-registry.d.ts +20 -0
  75. package/dist/composition/session-runtime-registry.js +85 -0
  76. package/dist/composition/settings-route-host.d.ts +16 -1
  77. package/dist/composition/settings-route-host.js +29 -10
  78. package/dist/composition/settings-search-input.d.ts +9 -0
  79. package/dist/composition/settings-search-input.js +58 -0
  80. package/dist/composition/startup-route.d.ts +32 -0
  81. package/dist/composition/startup-route.js +72 -0
  82. package/dist/composition/utility-tab.d.ts +28 -0
  83. package/dist/composition/utility-tab.js +242 -0
  84. package/dist/contracts/owned-ui/index.d.ts +1 -1
  85. package/dist/contracts/owned-ui/model.d.ts +3 -0
  86. package/dist/contracts/owned-ui/session-backend.d.ts +3 -0
  87. package/dist/contracts/owned-ui/session-workflows.d.ts +11 -4
  88. package/dist/contracts/presentation/index.d.ts +11 -0
  89. package/dist/contracts/presentation/index.js +12 -0
  90. package/dist/features/launch/development-launch.js +6 -0
  91. package/dist/features/launch/index.d.ts +1 -1
  92. package/dist/features/launch/intent.d.ts +3 -1
  93. package/dist/features/launch/intent.js +7 -4
  94. package/dist/features/launch/profile-paths.js +4 -0
  95. package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
  96. package/dist/features/owned-ui/plus-menu-dialog.js +76 -0
  97. package/dist/features/owned-ui/project-trust-dialog.d.ts +1 -1
  98. package/dist/features/owned-ui/project-trust-dialog.js +6 -8
  99. package/dist/features/owned-ui/project-trust-prompt.d.ts +1 -0
  100. package/dist/features/owned-ui/project-trust-prompt.js +7 -3
  101. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -0
  102. package/dist/features/owned-ui/reference-screen-app.js +12 -2
  103. package/dist/features/owned-ui/resources/release-notes.json +4 -0
  104. package/dist/features/owned-ui/settings-app.d.ts +12 -2
  105. package/dist/features/owned-ui/settings-app.js +119 -84
  106. package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
  107. package/dist/features/owned-ui/settings-search-input.js +1 -0
  108. package/dist/features/prompt-history/index.d.ts +1 -1
  109. package/dist/features/prompt-history/paths.d.ts +2 -1
  110. package/dist/features/prompt-history/paths.js +10 -2
  111. package/dist/features/prompt-history/service.d.ts +5 -0
  112. package/dist/features/prompt-history/service.js +1 -1
  113. package/dist/features/resident-tab/auto-name.d.ts +11 -0
  114. package/dist/features/resident-tab/auto-name.js +44 -0
  115. package/dist/features/resident-tab/index.d.ts +5 -0
  116. package/dist/features/resident-tab/index.js +7 -0
  117. package/dist/features/resident-tab/shortcut-bindings-file.d.ts +8 -0
  118. package/dist/features/resident-tab/shortcut-bindings-file.js +40 -0
  119. package/dist/features/resident-tab/shortcut-declarations.d.ts +13 -0
  120. package/dist/features/resident-tab/shortcut-declarations.js +26 -0
  121. package/dist/features/resident-tab/shortcut-projection.d.ts +29 -0
  122. package/dist/features/resident-tab/shortcut-projection.js +64 -0
  123. package/dist/features/resident-tab/status-deriver.d.ts +10 -0
  124. package/dist/features/resident-tab/status-deriver.js +12 -0
  125. package/dist/foundation/launch-context/index.d.ts +12 -0
  126. package/dist/foundation/launch-context/index.js +26 -0
  127. package/dist/foundation/launch-guardian/exit-watcher.d.ts +14 -0
  128. package/dist/foundation/launch-guardian/exit-watcher.js +94 -0
  129. package/dist/foundation/launch-guardian/main.d.ts +7 -1
  130. package/dist/foundation/launch-guardian/main.js +60 -7
  131. package/dist/foundation/lifecycle/session-repository-context.js +16 -2
  132. package/dist/foundation/process-containment/{native-guardian-containment.d.ts → addon-containment.d.ts} +7 -5
  133. package/dist/foundation/process-containment/addon-containment.js +139 -0
  134. package/dist/foundation/process-containment/addon-process-inspector.d.ts +11 -0
  135. package/dist/foundation/process-containment/addon-process-inspector.js +36 -0
  136. package/dist/foundation/process-containment/addon.d.ts +42 -0
  137. package/dist/foundation/process-containment/addon.js +57 -0
  138. package/dist/foundation/process-containment/artifact.d.ts +1 -1
  139. package/dist/foundation/process-containment/artifact.js +6 -5
  140. package/dist/foundation/process-containment/contracts.d.ts +2 -0
  141. package/dist/foundation/process-containment/index.d.ts +4 -4
  142. package/dist/foundation/process-containment/index.js +3 -3
  143. package/dist/foundation/process-containment/native-process-inspector.d.ts +2 -1
  144. package/dist/foundation/process-containment/native-process-inspector.js +10 -10
  145. package/dist/foundation/release/automatic-update.d.ts +37 -0
  146. package/dist/foundation/release/automatic-update.js +265 -0
  147. package/dist/foundation/release/bootstrap.d.ts +7 -9
  148. package/dist/foundation/release/bootstrap.js +107 -24
  149. package/dist/foundation/release/cohort-state.d.ts +1 -1
  150. package/dist/foundation/release/cohort-state.js +6 -3
  151. package/dist/foundation/release/dependency-certification.js +54 -19
  152. package/dist/foundation/release/early-accent.d.ts +12 -0
  153. package/dist/foundation/release/early-accent.js +59 -0
  154. package/dist/foundation/release/index.d.ts +2 -2
  155. package/dist/foundation/release/index.js +2 -2
  156. package/dist/foundation/release/latest-release.d.ts +24 -2
  157. package/dist/foundation/release/latest-release.js +51 -1
  158. package/dist/foundation/release/progress-palette.generated.d.ts +137 -3
  159. package/dist/foundation/release/progress-palette.generated.js +137 -3
  160. package/dist/foundation/release/release-gc.d.ts +3 -0
  161. package/dist/foundation/release/release-gc.js +59 -8
  162. package/dist/foundation/release/release-store.d.ts +12 -0
  163. package/dist/foundation/release/release-store.js +124 -2
  164. package/dist/foundation/release/update-execution-lease.d.ts +48 -0
  165. package/dist/foundation/release/update-execution-lease.js +161 -0
  166. package/dist/foundation/release/update.d.ts +12 -7
  167. package/dist/foundation/release/update.js +281 -196
  168. package/dist/foundation/resident-tabs/bounded-host-command.d.ts +18 -0
  169. package/dist/foundation/resident-tabs/bounded-host-command.js +39 -0
  170. package/dist/foundation/resident-tabs/bridge-client.d.ts +70 -0
  171. package/dist/foundation/resident-tabs/bridge-client.js +321 -0
  172. package/dist/foundation/resident-tabs/doctor.d.ts +28 -0
  173. package/dist/foundation/resident-tabs/doctor.js +166 -0
  174. package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
  175. package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
  176. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +36 -0
  177. package/dist/foundation/resident-tabs/ensure-terminal-host.js +151 -0
  178. package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
  179. package/dist/foundation/resident-tabs/fault-injection.js +26 -0
  180. package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
  181. package/dist/foundation/resident-tabs/fixture-control.js +35 -0
  182. package/dist/foundation/resident-tabs/host-entry.d.ts +1 -0
  183. package/dist/foundation/resident-tabs/host-entry.js +17 -0
  184. package/dist/foundation/resident-tabs/index.d.ts +12 -0
  185. package/dist/foundation/resident-tabs/index.js +6 -0
  186. package/dist/foundation/resident-tabs/maintenance.d.ts +39 -0
  187. package/dist/foundation/resident-tabs/maintenance.js +75 -0
  188. package/dist/foundation/resident-tabs/progress-observation-schema.d.ts +34 -0
  189. package/dist/foundation/resident-tabs/progress-observation-schema.js +1 -0
  190. package/dist/foundation/resident-tabs/progress-observation.d.ts +4 -0
  191. package/dist/foundation/resident-tabs/progress-observation.js +150 -0
  192. package/dist/foundation/resident-tabs/prompt-journal.d.ts +63 -0
  193. package/dist/foundation/resident-tabs/prompt-journal.js +213 -0
  194. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  195. package/dist/foundation/resident-tabs/protocol.js +195 -0
  196. package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
  197. package/dist/foundation/resident-tabs/recovery.js +205 -0
  198. package/dist/foundation/resident-tabs/release-holds.d.ts +28 -0
  199. package/dist/foundation/resident-tabs/release-holds.js +75 -0
  200. package/dist/foundation/resident-tabs/routes.d.ts +5 -0
  201. package/dist/foundation/resident-tabs/routes.js +5 -0
  202. package/dist/foundation/resident-tabs/routing.d.ts +11 -0
  203. package/dist/foundation/resident-tabs/routing.js +10 -0
  204. package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
  205. package/dist/foundation/resident-tabs/session-sync.js +37 -0
  206. package/dist/foundation/resident-tabs/settings.d.ts +12 -0
  207. package/dist/foundation/resident-tabs/settings.js +58 -0
  208. package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +14 -0
  209. package/dist/foundation/resident-tabs/terminal-host-artifact.js +78 -0
  210. package/dist/foundation/resident-tabs/zip-archive.d.ts +9 -0
  211. package/dist/foundation/resident-tabs/zip-archive.js +56 -0
  212. package/dist/foundation/session-writer-lock/index.d.ts +30 -0
  213. package/dist/foundation/session-writer-lock/index.js +203 -0
  214. package/dist/foundation/startup/startup-descriptor.js +2 -2
  215. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  216. package/dist/foundation/terminal-cleanup/index.js +1 -1
  217. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +6 -0
  218. package/dist/foundation/terminal-cleanup/terminal-reset.js +6 -0
  219. package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
  220. package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
  221. package/dist/integrations/pi/components/modal-frame.js +48 -9
  222. package/dist/integrations/pi/components/models-dialog.js +4 -52
  223. package/dist/integrations/pi/components/owned-editor-ux.d.ts +6 -1
  224. package/dist/integrations/pi/components/owned-editor-ux.js +90 -6
  225. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
  226. package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
  227. package/dist/integrations/pi/components/share-operation-dialog.d.ts +2 -0
  228. package/dist/integrations/pi/components/share-operation-dialog.js +2 -1
  229. package/dist/integrations/pi/components/shell-editor-autocomplete.js +58 -36
  230. package/dist/integrations/pi/components/shell-extension-ui.js +79 -60
  231. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  232. package/dist/integrations/pi/components/shell-footer-status.js +21 -5
  233. package/dist/integrations/pi/components/shell-hotkey-sections.js +1 -1
  234. package/dist/integrations/pi/components/shell-presenters-info.d.ts +5 -2
  235. package/dist/integrations/pi/components/shell-presenters-info.js +18 -4
  236. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
  237. package/dist/integrations/pi/components/shell-selectors-dialogs.js +5 -1
  238. package/dist/integrations/pi/components/shell-shared-facade.d.ts +12 -1
  239. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  240. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  241. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
  242. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  243. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  244. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  245. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  246. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  247. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  248. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  249. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  250. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +8 -6
  251. package/dist/integrations/pi/components/upstream/components/owned-editor.js +16 -9
  252. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  253. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +4 -4
  254. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
  255. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  256. package/dist/integrations/pi/components/upstream/components/session-footer.js +31 -25
  257. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  258. package/dist/integrations/pi/components/upstream/components/session-selector.js +8 -7
  259. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  260. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  261. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
  262. package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
  263. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  264. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  265. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +8 -7
  266. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +10 -9
  267. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +2 -6
  268. package/dist/integrations/pi/components/upstream/components/tool-execution.js +103 -101
  269. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +9 -9
  270. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -21
  271. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  272. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  273. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
  274. package/dist/integrations/pi/components/upstream/history/editor-core.js +7 -5
  275. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  276. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  277. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  278. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  279. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  280. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  281. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  282. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  283. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  284. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  285. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +5 -0
  286. package/dist/integrations/pi/components/upstream/theme/theme.js +29 -2
  287. package/dist/integrations/pi/engine/adapter.d.ts +5 -77
  288. package/dist/integrations/pi/engine/adapter.js +95 -394
  289. package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
  290. package/dist/integrations/pi/engine/command-dispatch.js +57 -0
  291. package/dist/integrations/pi/engine/host.js +2 -2
  292. package/dist/integrations/pi/engine/offline-settings-port.d.ts +23 -0
  293. package/dist/integrations/pi/engine/offline-settings-port.js +53 -0
  294. package/dist/integrations/pi/engine/pi-settings-metadata.json +14 -2
  295. package/dist/integrations/pi/engine/ports.d.ts +56 -0
  296. package/dist/integrations/pi/engine/ports.js +93 -0
  297. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
  298. package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
  299. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  300. package/dist/integrations/pi/engine/runtime-integration.js +102 -29
  301. package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
  302. package/dist/integrations/pi/engine/session-runtime.js +13 -5
  303. package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
  304. package/dist/integrations/pi/engine/session-selection.js +45 -9
  305. package/dist/integrations/pi/engine/settings-bridge.d.ts +5 -0
  306. package/dist/integrations/pi/engine/settings-bridge.js +15 -0
  307. package/dist/integrations/pi/engine/settings-port.js +11 -15
  308. package/dist/integrations/pi/engine/view-state.d.ts +42 -0
  309. package/dist/integrations/pi/engine/view-state.js +126 -0
  310. package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
  311. package/dist/integrations/pi/engine/workflow-runner.js +73 -4
  312. package/dist/integrations/pi/engine/workflow-support.d.ts +12 -0
  313. package/dist/integrations/pi/engine/workflow-support.js +36 -0
  314. package/dist/integrations/pi/engine/workflows.d.ts +3 -1
  315. package/dist/integrations/pi/engine/workflows.js +1 -0
  316. package/dist/integrations/pi/startup-public.js +257 -310
  317. package/dist/integrations/pi/startup-public.manifest.json +564 -440
  318. package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
  319. package/dist/integrations/pi/tui-runtime/adapter.js +32 -1
  320. package/dist/integrations/pi/tui-runtime/contracts.d.ts +5 -0
  321. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
  322. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +31 -2
  323. package/dist/native/darwin-arm64/manifest.json +10 -7
  324. package/dist/native/darwin-arm64/process-guardian.node +0 -0
  325. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
  326. package/dist/native/darwin-arm64/terminal-host.manifest.json +46 -0
  327. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  328. package/dist/native/darwin-x64/manifest.json +23 -0
  329. package/dist/native/darwin-x64/process-guardian.node +0 -0
  330. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
  331. package/dist/native/darwin-x64/terminal-host.manifest.json +46 -0
  332. package/dist/native/darwin-x64/terminal-host.node +0 -0
  333. package/dist/native/linux-x64/manifest.json +10 -7
  334. package/dist/native/linux-x64/process-guardian.node +0 -0
  335. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
  336. package/dist/native/linux-x64/terminal-host.manifest.json +46 -0
  337. package/dist/native/linux-x64/terminal-host.node +0 -0
  338. package/dist/native/win32-x64/manifest.json +10 -7
  339. package/dist/native/win32-x64/process-guardian.node +0 -0
  340. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
  341. package/dist/native/win32-x64/terminal-host.manifest.json +46 -0
  342. package/dist/native/win32-x64/terminal-host.node +0 -0
  343. package/dist/product-identity.d.ts +3 -3
  344. package/dist/product-identity.js +4 -4
  345. package/dist/product-identity.json +16 -2
  346. package/dist/runtime-payload-inventory.json +6330 -6677
  347. package/dist/ui/apps/host.d.ts +2 -0
  348. package/dist/ui/apps/host.js +7 -1
  349. package/dist/ui/components/dialog-panel.js +35 -30
  350. package/dist/ui/components/index.d.ts +2 -2
  351. package/dist/ui/components/index.js +1 -1
  352. package/dist/ui/components/line-input.d.ts +4 -1
  353. package/dist/ui/components/line-input.js +3 -1
  354. package/dist/ui/components/prompt-input.d.ts +11 -8
  355. package/dist/ui/components/prompt-input.js +15 -13
  356. package/dist/ui/components/selection-copy.d.ts +2 -0
  357. package/dist/ui/components/selection-copy.js +13 -0
  358. package/dist/ui/components/spans.js +2 -2
  359. package/dist/ui/components/submitted-prompt.d.ts +9 -6
  360. package/dist/ui/components/submitted-prompt.js +20 -12
  361. package/dist/ui/components/surface.js +4 -2
  362. package/dist/ui/components/text.d.ts +3 -6
  363. package/dist/ui/components/text.js +6 -8
  364. package/dist/ui/components/transcript-viewport.d.ts +9 -2
  365. package/dist/ui/components/transcript-viewport.js +176 -55
  366. package/dist/ui/components/value-menu.d.ts +6 -3
  367. package/dist/ui/components/value-menu.js +6 -5
  368. package/dist/ui/settings/declarations.d.ts +90 -16
  369. package/dist/ui/settings/declarations.js +67 -14
  370. package/dist/ui/settings/early-accent.d.ts +81 -0
  371. package/dist/ui/settings/early-accent.js +50 -0
  372. package/dist/ui/settings/index.d.ts +4 -2
  373. package/dist/ui/settings/index.js +2 -1
  374. package/dist/ui/settings/manager.d.ts +16 -3
  375. package/dist/ui/settings/manager.js +122 -33
  376. package/dist/ui/settings/migrations.d.ts +6 -0
  377. package/dist/ui/settings/migrations.js +73 -0
  378. package/dist/ui/settings/profile-document.d.ts +26 -0
  379. package/dist/ui/settings/profile-document.js +164 -0
  380. package/dist/ui/settings/progress-palette.generated.d.ts +139 -0
  381. package/dist/ui/settings/progress-palette.generated.js +139 -0
  382. package/dist/ui/settings/sections.d.ts +4 -0
  383. package/dist/ui/settings/sections.js +9 -3
  384. package/docs/architecture/boundaries.md +25 -10
  385. package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
  386. package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
  387. package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
  388. package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
  389. package/docs/architecture/evidence/terminal-host-spike/manual-one-pane-terminal-host.md +70 -0
  390. package/docs/architecture/evidence/terminal-host-spike/manual-two-by-two-basic-terminal-host.md +32 -0
  391. package/docs/architecture/evidence/terminal-host-spike/milestone-baseline.json +115 -0
  392. package/docs/architecture/evidence/terminal-host-spike/owned-ui-foundation-prerequisite.json +48 -0
  393. package/docs/architecture/evidence/terminal-host-spike/terminal-host-provenance.json +125 -0
  394. package/docs/architecture/evidence/terminal-host-spike/terminal-host-task-5-1.json +56 -0
  395. package/docs/architecture/evidence/terminal-host-spike/terminal-host-task-5-3.json +174 -0
  396. package/docs/architecture/evidence/terminal-host-spike/terminal-spike-acceptance-record.json +18 -0
  397. package/docs/architecture/internal-naming.md +3 -1
  398. package/docs/architecture/process-guardian-provenance.md +18 -10
  399. package/docs/architecture/project-structure.md +8 -0
  400. package/docs/architecture/resident-tabs-requirements.md +18 -0
  401. package/docs/architecture/resident-tabs.md +648 -0
  402. package/docs/architecture/response-copy-delivery.md +1 -1
  403. package/docs/architecture/terminal-host-proof-gate.md +3 -1
  404. package/docs/architecture/terminal-host-provenance.md +16 -4
  405. package/docs/architecture/terminal-host-spike-evidence.md +1 -1
  406. package/docs/architecture/tool-image-presentation.md +2 -1
  407. package/docs/architecture/toolchain.md +11 -9
  408. package/docs/ci-release-runbook.md +14 -7
  409. package/docs/features/launch-profiles.md +11 -8
  410. package/docs/features/prompt-history.md +41 -20
  411. package/docs/manual-resident-tabs-acceptance.md +51 -0
  412. package/docs/openspec-archive-automation.md +49 -22
  413. package/docs/releases/0.2.6.md +44 -0
  414. package/docs/validation.md +49 -9
  415. package/package.json +19 -6
  416. package/dist/foundation/process-containment/darwin-process-inspector.d.ts +0 -5
  417. package/dist/foundation/process-containment/darwin-process-inspector.js +0 -7
  418. package/dist/foundation/process-containment/native-guardian-containment.js +0 -152
  419. package/dist/foundation/process-containment/windows-process-inspector.d.ts +0 -19
  420. package/dist/foundation/process-containment/windows-process-inspector.js +0 -56
  421. package/dist/native/darwin-arm64/process-guardian +0 -0
  422. package/dist/native/linux-x64/process-guardian +0 -0
  423. package/dist/native/win32-x64/process-guardian.exe +0 -0
@@ -1,27 +1,41 @@
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";
9
17
  import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
10
18
  import { workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
11
19
  import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
12
20
  import { ProgramStatusReporter } from "../../integrations/pi/components/upstream/program-status-reporter.js";
13
21
  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";
22
+ import { createPiShellAuthProviderSelector, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellReloadBox, createPiShellSkillsSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
23
+ import { OwnedUiTerminalHost, componentRuntime, sessionTerminalHostOptions, } from "./shell-host.js";
17
24
  import { runImageWorker } from "./image-preparation-client.js";
18
25
  import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
19
26
  import { writeSystemClipboardText, } from "./system-clipboard.js";
20
27
  import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
21
28
  // Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
22
29
  const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
30
+ /** Resident Agents refuse the commands their tab bar serves instead, naming where they live. */
31
+ const RESIDENT_UTILITY_TAB_REFUSALS = Object.freeze({
32
+ settings: "Settings is available from the global Settings tab.",
33
+ changelog: "The changelog opens as its own tab from the command palette (Alt+/).",
34
+ name: "Rename the session by renaming its tab (F2).",
35
+ });
23
36
  export { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
24
37
  export { OwnedUiTerminalHost, sessionTerminalHostOptions } from "./shell-host.js";
38
+ export { formatSessionResumeCommand, quoteCommandArgument } from "./session-lifecycle.js";
25
39
  /**
26
40
  * Presents one engine session: its transcript root, editor, controllers, submission, workflows, and dialogs.
27
41
  * It reaches the terminal only through its terminal host's handle, and paints only while it is attached.
@@ -33,7 +47,10 @@ export class OwnedUiSessionPresenter {
33
47
  root;
34
48
  #host;
35
49
  #terminal;
36
- #cwd;
50
+ #resident;
51
+ #submission;
52
+ #login;
53
+ #concerns;
37
54
  #listeners = new Set();
38
55
  #unsubscribe;
39
56
  #unsubscribePromptSuggestions;
@@ -44,18 +61,11 @@ export class OwnedUiSessionPresenter {
44
61
  #unsubscribeSkills;
45
62
  #installedCommandSignature = "";
46
63
  #promptHistory = null;
47
- #promptHistoryImageSidecar;
64
+ #historyBinding;
48
65
  #extensionBridge;
49
- #stopped;
50
- #resolveStopped;
51
- #dialogId;
52
- #dialogSource;
53
- #routeHost;
54
- #startupRoute;
55
- #startupRouteTimer;
56
- #dialogHandle;
66
+ #stop = new SessionStopLatch();
67
+ #dialogs;
57
68
  #sequence = 0;
58
- #editorRevision = 0;
59
69
  #started = false;
60
70
  #disposed = false;
61
71
  #released;
@@ -77,18 +87,12 @@ export class OwnedUiSessionPresenter {
77
87
  #streamPresentation;
78
88
  #inputCoordination;
79
89
  #unsubscribeSettings;
80
- #unbindPiSettings;
81
- #unbindTerminalSettings;
82
- #unbindShutdownSettings;
90
+ #unbindSessionSettings;
83
91
  #terminalProgressEnabled = false;
84
92
  #programStatus;
85
- #showImages = true;
86
- #imageWidthCells = 80;
87
93
  #fullscreenExitOutput = "transcript";
88
- #waitingImages = new Map();
89
94
  #lastClearTime = 0;
90
95
  #lastEscapeTime = 0;
91
- #activeLoginDialog;
92
96
  #sessionGeneration;
93
97
  #sessionBindingGeneration;
94
98
  #suggestionModelKey;
@@ -102,10 +106,7 @@ export class OwnedUiSessionPresenter {
102
106
  this.#presenters = options.presenters;
103
107
  this.#sessionGeneration = this.backend.identity.sessionGeneration;
104
108
  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;
109
+ this.#suggestionModelKey = suggestionModelKey(this.backend.session.view());
109
110
  this.#customViewport = sessionLayout === "custom-viewport";
110
111
  this.#inputCoordination = inputPresentation?.coordination !== false;
111
112
  this.#host = host;
@@ -113,9 +114,6 @@ export class OwnedUiSessionPresenter {
113
114
  // Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
114
115
  this.#dialogHost = this.#createDialogHost();
115
116
  this.#lazySelectors = options.diagnostics?.lazySelectors;
116
- this.#stopped = new Promise(resolve => {
117
- this.#resolveStopped = resolve;
118
- });
119
117
  let promptSuggestionController = null;
120
118
  // Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
121
119
  this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
@@ -124,31 +122,24 @@ export class OwnedUiSessionPresenter {
124
122
  // throw releases them in reverse, so a shell that never finished constructing holds nothing.
125
123
  const constructed = [() => host.disconnect(this)];
126
124
  try {
127
- const terminalCopy = terminal !== undefined || hasAsyncClipboardOutput();
128
125
  this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
129
126
  execute: responseCopy?.execute ?? (this.#copyExecutor = createResponseCopyExecutor({
130
127
  ...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.copy }),
131
128
  ...(terminal === undefined && clipboard === undefined ? {} : { destination: "terminal" }),
132
129
  ...(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
- } } } : {}),
130
+ // Invariant: the owned host is the route contract. Resident and pseudoterminal children must not infer
131
+ // clipboard reachability from Node's view of stdout, and inactive presenters are fenced here.
132
+ terminal: { submit: async (control, signal) => {
133
+ if (signal.aborted || this.#disposed || !this.#terminal.running || !this.#terminal.attached)
134
+ throw new Error("Copy canceled");
135
+ // Performance: never grow the real terminal's pending buffer with another clipboard payload.
136
+ if (terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
137
+ throw new Error("Clipboard terminal is busy");
138
+ }
139
+ this.#terminal.writeControl(control);
140
+ } },
142
141
  })),
143
142
  ...(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
143
  }) : null;
153
144
  constructed.push(() => { this.#responseCopy?.dispose(); this.#copyExecutor?.dispose(); });
154
145
  const readClipboard = async (signal) => {
@@ -183,10 +174,12 @@ export class OwnedUiSessionPresenter {
183
174
  replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
184
175
  }),
185
176
  enableDockInputReuse: inputPresentation?.viewportReuse !== false,
177
+ footerSessionName: options.engine.residentTab === undefined,
178
+ hideUtilityTabCommands: options.engine.residentTab !== undefined,
186
179
  persistentHistory: this.#customViewport && promptHistory !== undefined,
187
180
  ...(promptHistory === undefined ? {} : { historyEditor: promptHistory.editor }),
188
- onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
189
- onPasteRejected: error => this.#reportSubmissionError(error),
181
+ onSubmit: text => { void this.submit(text).catch(() => this.#submission.reportError()); },
182
+ onPasteRejected: error => this.#submission.reportError(error),
190
183
  onInterrupt: () => { void this.interrupt(); },
191
184
  onClear: () => { void this.clearOrExit(); },
192
185
  onExit: () => { void this.shutdown(); },
@@ -198,10 +191,11 @@ export class OwnedUiSessionPresenter {
198
191
  this.#terminal.requestRender();
199
192
  },
200
193
  onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
201
- onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
194
+ onFollowUp: () => { void this.queueFollowUp().catch(() => this.#submission.reportError()); },
202
195
  onDequeue: () => this.restoreQueuedInput(),
203
196
  onEditorChange: text => {
204
- this.#editorRevision++;
197
+ this.#submission.noteEditorChange();
198
+ this.#resident.checkpoint(text);
205
199
  promptSuggestionController?.abortPending();
206
200
  if (text.length === 0) {
207
201
  // Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
@@ -218,9 +212,9 @@ export class OwnedUiSessionPresenter {
218
212
  promptSuggestionController?.invalidate();
219
213
  this.#promptHistory?.synchronize();
220
214
  },
221
- onCopyText: text => {
215
+ onCopyText: (text, cut) => {
222
216
  if (this.#responseCopy !== null) {
223
- void this.#responseCopy.submitText(text);
217
+ this.#trackCopy(this.#responseCopy.submitText(text), cut ? undefined : text.length);
224
218
  return;
225
219
  }
226
220
  const write = () => {
@@ -263,8 +257,27 @@ export class OwnedUiSessionPresenter {
263
257
  // Invariant: the presenter owns its root (theme subscription and helper clients) and disposes it once the
264
258
  // terminal no longer renders it.
265
259
  constructed.push(() => this.root.dispose());
260
+ this.#resident = new ResidentTabCoordinator(options.engine.residentTab, { root: this.root });
261
+ constructed.push(() => this.#resident.dispose());
262
+ const startupRoute = options.engine.startupRoute;
263
+ this.#dialogs = new SessionDialogCoordinator({
264
+ terminal: this.#terminal,
265
+ root: this.root,
266
+ routeHost: routeHost ?? null,
267
+ disposed: () => this.#disposed,
268
+ exit: () => { void this.shutdown(); },
269
+ routeClosed: route => this.#resident.closeOwnedRoute(route),
270
+ ...(startupRoute === undefined ? {} : { startupRoute }),
271
+ });
272
+ constructed.push(() => this.#dialogs.dispose());
266
273
  const initialPiSettings = this.backend.settings.snapshot();
267
274
  this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
275
+ this.#login = new LoginInteraction({
276
+ terminal: this.#terminal, root: this.root, programStatus: this.#programStatus,
277
+ syncProgress: () => this.#syncTerminalProgress(this.view()),
278
+ });
279
+ this.#submission = this.#createSubmission(cwd);
280
+ this.#resident.restoreInput(options.history?.imageSidecar);
268
281
  this.#reloadPresentation = reloadPresentation;
269
282
  const presentationInterval = streamPresentationOptions?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
270
283
  this.#streamPresentation = streamPresentationOptions?.scheduler === undefined
@@ -276,18 +289,7 @@ export class OwnedUiSessionPresenter {
276
289
  generator: promptSuggestionOptions.generator,
277
290
  enabled: promptSuggestionOptions.enabled(),
278
291
  ...(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
- },
292
+ surface: promptSuggestionSurface(this.root, identity => this.#promptSuggestionPresentationBlockReason(identity), () => this.#terminal.requestRender()),
291
293
  });
292
294
  this.#promptSuggestions = promptSuggestionController;
293
295
  constructed.push(() => this.#promptSuggestions?.dispose());
@@ -310,35 +312,7 @@ export class OwnedUiSessionPresenter {
310
312
  this.#terminal.requestRender();
311
313
  });
312
314
  constructed.push(this.#unsubscribeSkills);
313
- this.#terminal.setHardwareCursor(initialPiSettings.showHardwareCursor);
314
- this.#terminal.setClearOnShrink(initialPiSettings.clearOnShrink);
315
- this.#terminalProgressEnabled = initialPiSettings.showTerminalProgress;
316
315
  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
316
  const applyViewportSettings = () => {
343
317
  const snapshot = viewportSettings?.snapshot();
344
318
  this.root.setViewportConfig(snapshot ?? {
@@ -348,74 +322,23 @@ export class OwnedUiSessionPresenter {
348
322
  });
349
323
  };
350
324
  applyViewportSettings();
351
- this.#unsubscribeSettings = this.#customViewport && viewportSettings
352
- ? viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
353
- : () => { };
325
+ const unsubscribeViewport = this.#customViewport && viewportSettings ? viewportSettings.onChange(settings => this.root.setViewportConfig(settings)) : () => { };
326
+ const unsubscribeHistoryScope = this.#customViewport && promptHistory?.onScopeChange?.(scope => this.#rebindTranscript(scope));
327
+ this.#unsubscribeSettings = () => { unsubscribeViewport(); if (unsubscribeHistoryScope)
328
+ unsubscribeHistoryScope(); };
354
329
  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);
330
+ this.#unbindSessionSettings = bindSessionSettings(this.backend.settings, initialPiSettings, {
331
+ terminal: this.#terminal,
332
+ root: this.root,
333
+ setTerminalProgress: enabled => {
334
+ this.#terminalProgressEnabled = enabled;
335
+ if (this.#terminal.running)
336
+ this.#syncTerminalProgress(this.view());
337
+ },
338
+ }, { customViewport: this.#customViewport });
339
+ constructed.push(this.#unbindSessionSettings);
416
340
  if (this.#customViewport && promptHistory !== undefined) {
417
- this.#promptHistoryImageSidecar = promptHistory.imageSidecar;
418
- const sidecar = this.#promptHistoryImageSidecar;
341
+ this.#historyBinding = { options: promptHistory, scope: promptHistory.scope ?? "unique", sessionId: promptHistory.sessionId, imageSidecar: promptHistory.imageSidecar };
419
342
  this.#promptHistory = new PromptHistoryController({
420
343
  editor: this.root.editor,
421
344
  store: promptHistory.store,
@@ -423,57 +346,33 @@ export class OwnedUiSessionPresenter {
423
346
  fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
424
347
  active: () => this.root.usesDefaultInputSurface(),
425
348
  render: () => this.#terminal.requestRender(),
426
- rehydrate: value => this.root.rehydrateHistoryText(value, id => sidecar?.readAttachment(id) ?? null),
349
+ rehydrate: value => this.root.rehydrateHistoryText(value, id => this.#historyBinding?.imageSidecar?.readAttachment(id) ?? null),
427
350
  });
428
351
  constructed.push(() => { void this.#promptHistory?.close().catch(() => false); });
429
352
  }
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
- });
353
+ this.#extensionBridge = this.#createExtensionBridge();
457
354
  constructed.push(() => this.#extensionBridge.dispose());
355
+ this.#concerns = this.#createConcerns();
458
356
  constructed.push(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
459
357
  this.backend.workflows.setWorkflowInteractionHost({
460
- startLogin: request => this.#startWorkflowLogin(request),
461
- prompt: request => this.#requestWorkflowInput(request),
462
- notify: event => this.#notifyWorkflowLogin(event),
358
+ startLogin: request => this.#login.start(request),
359
+ prompt: request => this.#login.requestInput(request),
360
+ notify: event => this.#login.notify(event),
463
361
  publish: message => {
464
362
  this.root.appendWorkflowMessage(message);
465
363
  this.#terminal.requestRender();
466
364
  },
467
- finishLogin: () => this.#finishWorkflowLogin(),
365
+ finishLogin: () => this.#login.finish(),
468
366
  });
469
367
  this.#installAutocompleteCommands();
470
368
  this.#unsubscribe = this.backend.session.onEvent(event => {
471
369
  // Invariant: once disposal begins no event touches the view; only the stop still settles the shell.
472
370
  if (this.#disposed) {
473
371
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
474
- this.#settleStoppedLifecycle();
372
+ this.#stop.settleAfter(() => this.dispose());
475
373
  return;
476
374
  }
375
+ this.#resident.observeLifecycle(event);
477
376
  // Performance: a streamed chunk names one block, and touching only that block is what keeps the
478
377
  // cost of a chunk the same in a long session as in a new one. Everything else
479
378
  // resynchronizes the view, which is cheap next to re-reading the transcript.
@@ -489,28 +388,14 @@ export class OwnedUiSessionPresenter {
489
388
  }
490
389
  if (event.type === "assistant-message-completed") {
491
390
  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());
391
+ routeResponseToSuggestions(this.#promptSuggestions, event, () => this.root.promptSuggestionPrepareBlockReason());
507
392
  }
508
393
  const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
509
394
  const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.identity.sessionGeneration
510
395
  ? this.#syncBlock(event.block)
511
396
  : semanticOnly ? this.view() : this.#syncView();
512
397
  this.#syncTerminalProgress(view);
513
- const currentModelKey = modelKey(view);
398
+ const currentModelKey = suggestionModelKey(view);
514
399
  if (this.backend.identity.sessionGeneration !== this.#sessionGeneration || currentModelKey !== this.#suggestionModelKey) {
515
400
  this.#promptSuggestions?.invalidate();
516
401
  this.#suggestionModelKey = currentModelKey;
@@ -519,24 +404,10 @@ export class OwnedUiSessionPresenter {
519
404
  this.#promptSuggestions?.invalidate();
520
405
  }
521
406
  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());
407
+ routeResponseToSuggestions(this.#promptSuggestions, event, () => this.root.promptSuggestionPrepareBlockReason());
537
408
  }
538
409
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
539
- this.#settleStoppedLifecycle();
410
+ this.#stop.settleAfter(() => this.dispose());
540
411
  });
541
412
  constructed.push(this.#unsubscribe);
542
413
  this.#unbindClipboardWriter = this.#responseCopy === null ? () => { } : this.backend.extensions.bindClipboardWriter(async (text) => {
@@ -549,7 +420,7 @@ export class OwnedUiSessionPresenter {
549
420
  });
550
421
  constructed.push(this.#unbindClipboardWriter);
551
422
  if (this.backend.session.view().lifecycle === "stopped")
552
- this.#resolveStopped?.();
423
+ this.#stop.resolve();
553
424
  }
554
425
  catch (error) {
555
426
  for (const release of constructed.reverse()) {
@@ -626,29 +497,23 @@ export class OwnedUiSessionPresenter {
626
497
  const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.#terminal.hasFocusedOverlay());
627
498
  if (routed.copySelection !== undefined) {
628
499
  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
- });
500
+ this.#trackCopy(this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite), copyCharacters(snapshot));
639
501
  }
640
502
  if (!routed.consumed)
641
503
  return routed.data === data ? undefined : { data: routed.data };
642
504
  return routed.data.length === 0 ? { consume: true } : { data: routed.data };
643
505
  }
506
+ #trackCopy(delivery, count) {
507
+ const intent = ++this.#copyIntentSequence;
508
+ void delivery?.then(({ outcome }) => {
509
+ if (count === undefined || this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
510
+ || outcome !== "delivered" && outcome !== "submitted-unverified")
511
+ return;
512
+ this.root.showCopyAcknowledgement(`copied ${count} chars to clipboard`);
513
+ });
514
+ }
644
515
  #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();
516
+ return suggestionPresentationBlockReason(identity, { disposed: this.#disposed, view: this.view(), sessionGeneration: this.backend.identity.sessionGeneration }, () => this.root.promptSuggestionPresentationBlockReason());
652
517
  }
653
518
  start() {
654
519
  if (this.#started)
@@ -663,7 +528,7 @@ export class OwnedUiSessionPresenter {
663
528
  setImmediate(() => {
664
529
  if (this.#disposed || !this.#customViewport)
665
530
  return;
666
- this.#presentStartupRoute();
531
+ this.#dialogs.presentStartupRoute();
667
532
  void this.#loadLazySelectors().then(selectors => selectors.prepare()).catch(() => { });
668
533
  this.root.warmPastePreparation();
669
534
  this.#copyExecutor?.warm();
@@ -675,127 +540,14 @@ export class OwnedUiSessionPresenter {
675
540
  return () => this.#listeners.delete(listener);
676
541
  }
677
542
  waitUntilStopped() {
678
- return this.#stopped;
543
+ return this.#stop.stopped;
679
544
  }
680
545
  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
- }
546
+ return this.#submission.submit(text);
795
547
  }
796
548
  async clearOrExit(now = Date.now()) {
797
549
  this.#promptSuggestions?.abortPending();
798
- if (now - this.#lastClearTime < 500)
550
+ if (now - this.#lastClearTime < CLEAR_EXIT_INTERVAL_MS)
799
551
  return this.shutdown();
800
552
  this.root.editor.setText("");
801
553
  this.#lastClearTime = now;
@@ -804,8 +556,8 @@ export class OwnedUiSessionPresenter {
804
556
  }
805
557
  async interrupt(now = Date.now()) {
806
558
  this.#promptSuggestions?.invalidate();
807
- if (this.#waitingImages.size > 0) {
808
- this.#cancelWaitingImages();
559
+ if (this.#submission.waiting) {
560
+ this.#submission.cancelWaiting();
809
561
  return { outcome: "completed", diagnostic: null };
810
562
  }
811
563
  if (this.view().lifecycle === "busy")
@@ -832,23 +584,23 @@ export class OwnedUiSessionPresenter {
832
584
  return { outcome: "completed", diagnostic: null };
833
585
  }
834
586
  async abort() {
835
- return this.#execute(this.#simple("abort"));
587
+ return this.#submission.execute(this.#simple("abort"));
836
588
  }
837
589
  async retry() {
838
590
  this.#promptSuggestions?.invalidate();
839
- return this.#execute(this.#simple("retry"));
591
+ return this.#submission.execute(this.#simple("retry"));
840
592
  }
841
593
  async compact() {
842
594
  this.#promptSuggestions?.invalidate();
843
- return this.#execute(this.#simple("compact"));
595
+ return this.#submission.execute(this.#simple("compact"));
844
596
  }
845
597
  async newSession() {
846
598
  this.#promptSuggestions?.invalidate();
847
- return this.#execute(this.#simple("new-session"));
599
+ return this.#submission.execute(this.#simple("new-session"));
848
600
  }
849
601
  async resumeSession(sessionPath) {
850
602
  this.#promptSuggestions?.invalidate();
851
- return this.#execute({
603
+ return this.#submission.execute({
852
604
  type: "resume-session",
853
605
  correlationId: this.#correlation("resume"),
854
606
  sessionId: this.backend.identity.sessionId,
@@ -857,7 +609,7 @@ export class OwnedUiSessionPresenter {
857
609
  }
858
610
  async setModel(providerId, modelId) {
859
611
  this.#promptSuggestions?.invalidate();
860
- return this.#execute({
612
+ return this.#submission.execute({
861
613
  type: "set-model",
862
614
  correlationId: this.#correlation("model"),
863
615
  sessionId: this.backend.identity.sessionId,
@@ -865,7 +617,7 @@ export class OwnedUiSessionPresenter {
865
617
  });
866
618
  }
867
619
  async setThinkingLevel(thinkingLevel) {
868
- return this.#execute({
620
+ return this.#submission.execute({
869
621
  type: "set-thinking-level",
870
622
  correlationId: this.#correlation("thinking"),
871
623
  sessionId: this.backend.identity.sessionId,
@@ -884,64 +636,13 @@ export class OwnedUiSessionPresenter {
884
636
  return workflowAdapterResult(result);
885
637
  }
886
638
  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);
639
+ return this.#submission.queueFollowUp(this.root.editor.getText());
908
640
  }
909
641
  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();
642
+ this.#submission.restoreQueued();
916
643
  }
917
644
  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;
645
+ return this.#dialogs.showSelector(title, options, onSelect, onCancel);
945
646
  }
946
647
  // Rationale: presenters see the shell only as a dialog host; selectors open in the input dock and run choices as workflows.
947
648
  #createDialogHost() {
@@ -1009,15 +710,12 @@ export class OwnedUiSessionPresenter {
1009
710
  }
1010
711
  /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
1011
712
  showSkillsSelector(skills = this.#skillSummaries()) {
1012
- const close = () => {
1013
- this.root.setInputSurface(null);
1014
- this.#terminal.requestRender();
1015
- };
713
+ const close = this.#dialogs.closeInputSurface;
1016
714
  const component = createPiShellSkillsSelector({
1017
715
  skills,
1018
716
  onSelect: name => {
1019
717
  close();
1020
- void this.#submitSkillPrompt(skillPrompt(name), skillPrompt(name)).catch(() => this.#reportSubmissionError());
718
+ void this.#submission.sendSlashPrompt(skillPrompt(name), skillPrompt(name)).catch(() => this.#submission.reportError());
1021
719
  },
1022
720
  onCancel: close,
1023
721
  });
@@ -1034,10 +732,7 @@ export class OwnedUiSessionPresenter {
1034
732
  this.#terminal.requestRender();
1035
733
  return;
1036
734
  }
1037
- const close = () => {
1038
- this.root.setInputSurface(null);
1039
- this.#terminal.requestRender();
1040
- };
735
+ const close = this.#dialogs.closeInputSurface;
1041
736
  const component = createPiShellUserMessageSelector(options, selection => {
1042
737
  close();
1043
738
  void this.runWorkflow({ command: "fork", argument: "", selection });
@@ -1064,10 +759,7 @@ export class OwnedUiSessionPresenter {
1064
759
  this.#terminal.requestRender();
1065
760
  return;
1066
761
  }
1067
- const close = () => {
1068
- this.root.setInputSurface(null);
1069
- this.#terminal.requestRender();
1070
- };
762
+ const close = this.#dialogs.closeInputSurface;
1071
763
  const component = createPiShellAuthProviderSelector("logout", options, selection => {
1072
764
  close();
1073
765
  void this.runWorkflow({ command: "logout", argument: "", selection });
@@ -1087,10 +779,7 @@ export class OwnedUiSessionPresenter {
1087
779
  void this.runWorkflow({ command: "login", argument: "", selection });
1088
780
  return;
1089
781
  }
1090
- const close = () => {
1091
- this.root.setInputSurface(null);
1092
- this.#terminal.requestRender();
1093
- };
782
+ const close = this.#dialogs.closeInputSurface;
1094
783
  const labels = method.options.map(option => option.label);
1095
784
  const component = createPiShellExtensionSelector(method.title, labels, label => {
1096
785
  const selection = method.options.find(option => option.label === label)?.id;
@@ -1108,10 +797,7 @@ export class OwnedUiSessionPresenter {
1108
797
  }, initialSelectedId);
1109
798
  }
1110
799
  showLoginAuthTypeSelector() {
1111
- const close = () => {
1112
- this.root.setInputSurface(null);
1113
- this.#terminal.requestRender();
1114
- };
800
+ const close = this.#dialogs.closeInputSurface;
1115
801
  const labels = ["Sign in with an account", "Sign in with an API key"];
1116
802
  const component = createPiShellExtensionSelector("Select authentication method:", labels, label => {
1117
803
  close();
@@ -1127,10 +813,7 @@ export class OwnedUiSessionPresenter {
1127
813
  this.#terminal.requestRender();
1128
814
  return;
1129
815
  }
1130
- const close = () => {
1131
- this.root.setInputSurface(null);
1132
- this.#terminal.requestRender();
1133
- };
816
+ const close = this.#dialogs.closeInputSurface;
1134
817
  const component = createPiShellAuthProviderSelector("login", options, id => {
1135
818
  close();
1136
819
  void this.runWorkflow({ command: "login", argument: "", selection: id });
@@ -1149,10 +832,22 @@ export class OwnedUiSessionPresenter {
1149
832
  this.#presenters.openSettingsSelector(this.#dialogHost);
1150
833
  }
1151
834
  shutdown() {
1152
- this.#shutdownPromise ??= this.#shutdown();
835
+ this.#shutdownPromise ??= this.#shutdown().finally(() => {
836
+ // Invariant: a rejected resident detach is retryable; ordinary shutdown has disposed the shell already.
837
+ if (!this.#disposed)
838
+ this.#shutdownPromise = undefined;
839
+ });
1153
840
  return this.#shutdownPromise;
1154
841
  }
1155
842
  async #shutdown() {
843
+ const detached = await this.#resident.detach();
844
+ if (detached === true)
845
+ return { outcome: "completed", diagnostic: null };
846
+ if (detached !== undefined) {
847
+ this.root.appendWorkflowStatus(detached);
848
+ this.#terminal.requestRender();
849
+ return rejected("resident tab detach is unavailable");
850
+ }
1156
851
  try {
1157
852
  const quit = this.backend.workflows.executeWorkflow({ command: "quit", argument: "" });
1158
853
  // Invariant: a slow or hung engine quit never holds the terminal past the cleanup deadline. The
@@ -1165,9 +860,16 @@ export class OwnedUiSessionPresenter {
1165
860
  return workflowAdapterResult(result);
1166
861
  }
1167
862
  finally {
1168
- this.#resolveStoppedLifecycle();
863
+ this.#stop.resolve();
1169
864
  }
1170
865
  }
866
+ #currentSessionFile() {
867
+ const direct = this.backend.identity.currentSessionFile?.();
868
+ if (direct !== undefined && direct !== null)
869
+ return direct;
870
+ const resume = this.backend.identity.currentSessionResumeMetadata();
871
+ return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
872
+ }
1171
873
  async runWorkflow(request) {
1172
874
  if (request.command === "quit")
1173
875
  return this.shutdown();
@@ -1188,68 +890,17 @@ export class OwnedUiSessionPresenter {
1188
890
  if (request.command === "login" && request.selection !== undefined) {
1189
891
  const setup = this.backend.catalog.pinnedAmbientAuthentication(request.selection);
1190
892
  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);
893
+ const close = this.#dialogs.closeInputSurface;
894
+ const dialog = createPiShellLoginDialog(componentRuntime(this.#terminal), setup.providerId, close, setup.providerName, setup.title);
1200
895
  dialog.showInfo(setup.message, [], true);
1201
896
  this.root.setInputSurface(dialog);
1202
897
  this.#terminal.requestRender();
1203
898
  return { outcome: "completed", diagnostic: null };
1204
899
  }
1205
900
  }
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
- }
901
+ const selected = openWorkflowSelector(request, this, { residentTab: this.#resident.active });
902
+ if (selected !== undefined)
903
+ return selected;
1253
904
  if (request.command === "reload") {
1254
905
  const blocked = this.backend.workflows.reloadBlockedResult();
1255
906
  if (blocked) {
@@ -1263,11 +914,7 @@ export class OwnedUiSessionPresenter {
1263
914
  ? await import("./share-operation.js")
1264
915
  : undefined;
1265
916
  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…");
917
+ : (this.#customViewport ? shareDialog.createPiShellShareOperationDialog : shareDialog.createPiShellOperationLoader)(componentRuntime(this.#terminal), "Creating gist…");
1271
918
  const operationSurface = shareSurface ?? (request.command === "reload" ? createPiShellReloadBox() : undefined);
1272
919
  const now = this.#reloadPresentation?.now ?? Date.now;
1273
920
  const shownAt = now();
@@ -1277,7 +924,11 @@ export class OwnedUiSessionPresenter {
1277
924
  }
1278
925
  let result;
1279
926
  try {
1280
- result = await this.backend.workflows.executeWorkflow(shareSurface === undefined ? request : { ...request, signal: shareSurface.signal });
927
+ result = await this.backend.workflows.executeWorkflow(shareSurface === undefined ? request : {
928
+ ...request,
929
+ signal: shareSurface.signal,
930
+ onProgress: message => { shareSurface.setMessage?.(message); this.#terminal.requestRender(); },
931
+ });
1281
932
  }
1282
933
  finally {
1283
934
  if (operationSurface) {
@@ -1347,10 +998,7 @@ export class OwnedUiSessionPresenter {
1347
998
  }
1348
999
  showTrustSelector() {
1349
1000
  const context = this.backend.catalog.pinnedProjectTrustContext();
1350
- const close = () => {
1351
- this.root.setInputSurface(null);
1352
- this.#terminal.requestRender();
1353
- };
1001
+ const close = this.#dialogs.closeInputSurface;
1354
1002
  const component = createPiShellTrustSelector({
1355
1003
  ...context,
1356
1004
  onSelect: selection => {
@@ -1414,18 +1062,13 @@ export class OwnedUiSessionPresenter {
1414
1062
  this.#copyIntentSequence += 1;
1415
1063
  this.#responseCopy?.dispose();
1416
1064
  this.#copyExecutor?.dispose();
1417
- this.#cancelWaitingImages();
1065
+ this.#submission.cancelWaiting();
1418
1066
  let pasteCleanup = Promise.resolve();
1419
1067
  let historyCleanup = Promise.resolve(true);
1420
1068
  attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
1421
1069
  attempt(() => { pasteCleanup = this.root.disposePendingPastes(); });
1422
1070
  attempt(() => this.root.clearViewportPointerState());
1423
1071
  attempt(() => this.#streamPresentation.dispose());
1424
- attempt(() => {
1425
- if (this.#startupRouteTimer !== undefined)
1426
- clearTimeout(this.#startupRouteTimer);
1427
- this.#startupRouteTimer = undefined;
1428
- });
1429
1072
  attempt(() => this.#promptSuggestions?.dispose());
1430
1073
  attempt(() => this.#unsubscribePromptSuggestions());
1431
1074
  attempt(() => this.#unsubscribePromptImages());
@@ -1433,21 +1076,18 @@ export class OwnedUiSessionPresenter {
1433
1076
  attempt(() => this.#unsubscribeSettings());
1434
1077
  let exitText = "";
1435
1078
  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");
1079
+ exitText = composeExitText({
1080
+ terminalMode: terminal.mode,
1081
+ customViewport: this.#customViewport,
1082
+ exitMode: this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput,
1083
+ resume: this.backend.identity.currentSessionResumeMetadata(),
1084
+ transcript: () => this.root.exitTranscript(terminal.columns),
1085
+ });
1444
1086
  });
1445
1087
  attempt(() => this.#unbindClipboardWriter());
1446
- attempt(() => this.#unbindPiSettings());
1447
- attempt(() => this.#unbindTerminalSettings());
1448
- attempt(() => this.#unbindShutdownSettings());
1088
+ attempt(() => this.#unbindSessionSettings());
1449
1089
  attempt(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
1450
- attempt(() => this.#dialogHandle?.hide());
1090
+ this.#concerns.dispose(attempt);
1451
1091
  attempt(() => this.#extensionBridge.dispose());
1452
1092
  attempt(() => this.#programStatus.clear());
1453
1093
  // Invariant: the host settles a presenter only once the terminal is restored, so these never stall it.
@@ -1461,20 +1101,14 @@ export class OwnedUiSessionPresenter {
1461
1101
  }
1462
1102
  await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
1463
1103
  await boundedCleanup(() => pasteCleanup).catch(error => settled.push(error));
1104
+ await boundedCleanup(() => this.#resident.gracefulStop(this.#currentSessionFile()))
1105
+ .catch(error => settled.push(error));
1464
1106
  await boundedCleanup(() => this.backend.extensions.unbindExtensionUi()).catch(error => settled.push(error));
1465
1107
  return settled;
1466
1108
  };
1467
1109
  this.#released = { exitText, failures, settle };
1468
1110
  return this.#released;
1469
1111
  }
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
1112
  // Invariant: incremental block updates notify listeners with the same complete view contract.
1479
1113
  #syncBlock(block) {
1480
1114
  this.root.applyTranscriptBlock(block);
@@ -1492,6 +1126,121 @@ export class OwnedUiSessionPresenter {
1492
1126
  listener(view);
1493
1127
  return view;
1494
1128
  }
1129
+ // Rationale: extensions reach the shell only through this bridge; every surface they draw lands in the root.
1130
+ #createExtensionBridge() {
1131
+ return createPiExtensionUiBridge({
1132
+ runtime: componentRuntime(this.#terminal),
1133
+ agentDir: this.backend.identity.agentDir,
1134
+ setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
1135
+ setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
1136
+ showOverlay: (component, overlayOptions) => this.#terminal.showOverlay(component, overlayOptions),
1137
+ listenInput: handler => this.#terminal.addInputListener(handler),
1138
+ replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
1139
+ replaceHeader: component => this.root.setExtensionHeader(component),
1140
+ replaceFooter: component => this.root.setExtensionFooter(component),
1141
+ setStatus: (key, text) => this.root.setExtensionStatus(key, text),
1142
+ setWorking: (message, visible) => this.root.setExtensionWorking(message, visible),
1143
+ notify: (message, type) => this.root.addExtensionNotification(message, type),
1144
+ setTitle: title => this.#terminal.setTitle(title),
1145
+ getEditorText: () => this.root.editor.getText(),
1146
+ setEditorText: text => this.root.editor.setText(text),
1147
+ pasteToEditor: text => this.root.editor.insertText(text),
1148
+ addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
1149
+ setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
1150
+ getFooterData: () => this.root.extensionFooterData(),
1151
+ getToolsExpanded: () => this.root.toolsExpanded,
1152
+ setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
1153
+ });
1154
+ }
1155
+ #createSubmission(cwd) {
1156
+ return new PromptSubmission({
1157
+ backend: this.backend,
1158
+ root: this.root,
1159
+ terminal: this.#terminal,
1160
+ residentTab: this.#resident,
1161
+ cwd,
1162
+ promptHistory: () => this.#promptHistory,
1163
+ imageSidecar: () => this.#historyBinding?.imageSidecar,
1164
+ disposed: () => this.#disposed,
1165
+ correlation: prefix => this.#correlation(prefix),
1166
+ lifecycle: () => this.view().lifecycle,
1167
+ currentSessionFile: () => this.#currentSessionFile(),
1168
+ slashCommand: text => this.#slashCommand(text),
1169
+ invalidateSuggestions: () => this.#promptSuggestions?.invalidate(),
1170
+ });
1171
+ }
1172
+ // Invariant: the order is the reset order on session replacement; copy intent before pastes before the input surface.
1173
+ #createConcerns() {
1174
+ return new SessionConcerns([
1175
+ { onSessionReplaced: () => { this.#copyIntentSequence += 1; this.#responseCopy?.reset(); } },
1176
+ this.#submission,
1177
+ { onSessionReplaced: () => this.root.resetPendingPastes() },
1178
+ { onSessionReplaced: () => this.#promptSuggestions?.invalidate() },
1179
+ { onSessionReplaced: () => { this.#sessionGeneration = this.backend.identity.sessionGeneration; this.#programStatus.reset(); } },
1180
+ { onSessionReplaced: () => this.#rebindTranscript() },
1181
+ this.#login,
1182
+ { onSessionReplaced: () => this.#extensionBridge.reset() },
1183
+ // Invariant: a replaced session takes its transient viewport and owned-route state with it.
1184
+ { onSessionReplaced: () => this.root.resetViewport() },
1185
+ this.#dialogs,
1186
+ { onSessionReplaced: () => this.root.setInputSurface(null) },
1187
+ { onSessionReplaced: () => this.root.resetExtensionUi() },
1188
+ { onSessionReplaced: () => this.root.resetWorkflowPresentation() },
1189
+ this.#resident,
1190
+ ]);
1191
+ }
1192
+ // Invariant: delivery recovery does not replace a store; conversation or live scope changes can.
1193
+ #rebindTranscript(scope) {
1194
+ const replaced = this.#sessionBindingGeneration !== this.backend.identity.sessionBindingGeneration;
1195
+ if (!replaced && scope === undefined)
1196
+ return;
1197
+ if (replaced) {
1198
+ this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
1199
+ this.root.resetTranscript();
1200
+ }
1201
+ const binding = this.#historyBinding;
1202
+ if (binding?.options.forSession !== undefined || binding?.options.forScope !== undefined) {
1203
+ const sessionId = this.backend.identity.currentHistorySessionId();
1204
+ const selectedScope = scope ?? binding.scope;
1205
+ if (selectedScope !== binding.scope || sessionId !== binding.sessionId) {
1206
+ if (!replaced)
1207
+ this.root.editor.recall?.restoreDraft?.();
1208
+ const previous = this.#promptHistory;
1209
+ this.#promptHistory = null;
1210
+ binding.imageSidecar = undefined;
1211
+ binding.scope = selectedScope;
1212
+ binding.sessionId = sessionId ?? undefined;
1213
+ void previous?.close().catch(() => false);
1214
+ const destination = binding.options.forScope !== undefined
1215
+ ? binding.options.forScope(selectedScope, sessionId)
1216
+ : sessionId === null ? null : binding.options.forSession(sessionId);
1217
+ if (destination !== null) {
1218
+ binding.imageSidecar = destination.imageSidecar;
1219
+ const controller = new PromptHistoryController({
1220
+ editor: this.root.editor,
1221
+ store: destination.store,
1222
+ limit: binding.options.limit,
1223
+ fallback: replaced ? this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []) : [],
1224
+ active: () => this.root.usesDefaultInputSurface(),
1225
+ render: () => this.#terminal.requestRender(),
1226
+ rehydrate: value => this.root.rehydrateHistoryText(value, id => this.#historyBinding?.imageSidecar?.readAttachment(id) ?? null),
1227
+ });
1228
+ this.#promptHistory = controller;
1229
+ if (this.#started)
1230
+ controller.start();
1231
+ }
1232
+ else {
1233
+ // Security: missing identity must not reopen Shared or retain another session's recall.
1234
+ const fallback = replaced ? this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []).reverse() : [];
1235
+ this.root.editor.recall?.reset();
1236
+ this.root.editor.recall?.replace(fallback);
1237
+ }
1238
+ return;
1239
+ }
1240
+ }
1241
+ if (replaced)
1242
+ this.#promptHistory?.reset(this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
1243
+ }
1495
1244
  #syncTerminalProgress(view) {
1496
1245
  if (!this.#terminal.running)
1497
1246
  return;
@@ -1511,34 +1260,10 @@ export class OwnedUiSessionPresenter {
1511
1260
  #syncView() {
1512
1261
  this.#streamPresentation.noteImmediatePresentation();
1513
1262
  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
- }
1263
+ if (this.backend.identity.sessionGeneration !== this.#sessionGeneration)
1264
+ this.#concerns.replaceSession();
1540
1265
  this.root.update(view);
1541
- this.#syncDialog(view.dialog);
1266
+ this.#dialogs.syncBackend(view.dialog);
1542
1267
  this.#presentStartupTrustWarning(view);
1543
1268
  this.#terminal.requestRender();
1544
1269
  for (const listener of this.#listeners)
@@ -1556,100 +1281,34 @@ export class OwnedUiSessionPresenter {
1556
1281
  this.#startupTrustHandled = true;
1557
1282
  this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
1558
1283
  }
1559
- #presentStartupRoute() {
1560
- const pending = this.#startupRoute;
1561
- if (pending === undefined || this.#disposed || !this.#terminal.running)
1284
+ openOwnedRoute(route) {
1285
+ if (this.#disposed)
1562
1286
  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?.();
1287
+ // Invariant: a route requested before the runtime runs (the resident Settings bridge route arrives
1288
+ // before start()) is retained, latest wins, and presented through the startup-route path once started.
1289
+ if (!this.#terminal.running) {
1290
+ this.#dialogs.retainStartupRoute(route);
1569
1291
  return;
1570
1292
  }
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;
1293
+ const result = this.#dialogs.openRoute(route);
1294
+ if (result.outcome !== "completed" && result.diagnostic !== null) {
1295
+ this.root.appendWorkflowMessage({ kind: "warning", message: result.diagnostic });
1616
1296
  }
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
1297
  }
1643
1298
  hotkeysPresentation() {
1644
- return this.root.hotkeysPresentation();
1299
+ const presentation = this.root.hotkeysPresentation();
1300
+ return this.#resident.active ? { ...presentation, residentTabs: true } : presentation;
1645
1301
  }
1646
1302
  async #slashCommand(text) {
1647
1303
  const body = text.slice(1).trim();
1648
1304
  const separator = body.search(/\s/);
1649
1305
  const name = separator < 0 ? body : body.slice(0, separator);
1650
1306
  const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
1651
- if (this.#routeHost?.claims(name))
1652
- return this.#openOwnedRoute(name);
1307
+ const utilityTab = this.#resident.active ? RESIDENT_UTILITY_TAB_REFUSALS[name] : undefined;
1308
+ if (utilityTab !== undefined)
1309
+ return rejected(utilityTab);
1310
+ if (this.#dialogs.claimsRoute(name))
1311
+ return this.#dialogs.openRoute(name);
1653
1312
  if (this.#isWorkflowRoute(name))
1654
1313
  return this.runWorkflow({ command: name, argument });
1655
1314
  if (this.#skillsCollapsed()) {
@@ -1657,17 +1316,10 @@ export class OwnedUiSessionPresenter {
1657
1316
  return this.#runSkillsCommand(text, argument);
1658
1317
  const rewritten = rewriteSkillsTunnelSubmission(text);
1659
1318
  if (rewritten !== text)
1660
- return this.#submitSkillPrompt(rewritten, text);
1319
+ return this.#submission.sendSlashPrompt(rewritten, text);
1661
1320
  }
1662
1321
  // 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
- });
1322
+ return this.#submission.sendSlashPrompt(text);
1671
1323
  }
1672
1324
  #installAutocompleteCommands() {
1673
1325
  this.root.editor.setAutocompleteCommands(this.backend.workflows.workflowAutocompleteCommands());
@@ -1701,18 +1353,7 @@ export class OwnedUiSessionPresenter {
1701
1353
  this.#terminal.requestRender();
1702
1354
  return { outcome: "failed", diagnostic: message };
1703
1355
  }
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
- });
1356
+ return this.#submission.sendSlashPrompt(skillPrompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1716
1357
  }
1717
1358
  #treeSummary() {
1718
1359
  return new Promise(resolve => {
@@ -1753,139 +1394,6 @@ export class OwnedUiSessionPresenter {
1753
1394
  if (result.diagnostic === "Branch summarization cancelled")
1754
1395
  await this.showTreeSelector(entryId);
1755
1396
  }
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
1397
  #simple(type) {
1890
1398
  return { type, correlationId: this.#correlation(type), sessionId: this.backend.identity.sessionId };
1891
1399
  }
@@ -1920,38 +1428,19 @@ export class OwnedUiSessionShell extends OwnedUiSessionPresenter {
1920
1428
  function isCoalescedStreamBlock(kind) {
1921
1429
  return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
1922
1430
  }
1923
- function copyAcknowledgement(snapshot) {
1431
+ function copyCharacters(snapshot) {
1924
1432
  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`;
1433
+ return snapshot.sourceUnits;
1434
+ let count = Math.max(0, snapshot.rows.length - 1), hasContent = false;
1435
+ for (const { text } of snapshot.rows) {
1436
+ count += text.length;
1437
+ hasContent ||= /\S/u.test(text);
1438
+ }
1439
+ return hasContent ? count : undefined;
1948
1440
  }
1949
1441
  function rejected(diagnostic) {
1950
1442
  return { outcome: "rejected", diagnostic };
1951
1443
  }
1952
- function modelKey(view) {
1953
- return view.activeModel === null ? "" : `${view.activeModel.providerId}/${view.activeModel.modelId}`;
1954
- }
1955
1444
  function workflowAdapterResult(result) {
1956
1445
  if (result.outcome === "completed")
1957
1446
  return { outcome: "completed", diagnostic: null };