@timurproko/a1 0.2.6 → 0.2.7-dev.1222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +41 -3
  2. package/bin/automatic-update.js +7 -0
  3. package/bin/cli.js +4 -0
  4. package/bin/guardian.js +5 -0
  5. package/bin/tab-launch-context.d.ts +27 -0
  6. package/bin/tab-launch-context.js +94 -0
  7. package/bin/ui.js +107 -59
  8. package/dist/app/session-shell/clipboard-diagnostics.js +1 -1
  9. package/dist/app/session-shell/dialog-coordinator.d.ts +42 -0
  10. package/dist/app/session-shell/dialog-coordinator.js +133 -0
  11. package/dist/app/session-shell/dock-chrome.d.ts +34 -0
  12. package/dist/app/session-shell/dock-chrome.js +109 -0
  13. package/dist/app/session-shell/exit-notice.d.ts +1 -1
  14. package/dist/app/session-shell/exit-notice.js +1 -1
  15. package/dist/app/session-shell/extension-surface.d.ts +30 -0
  16. package/dist/app/session-shell/extension-surface.js +105 -0
  17. package/dist/app/session-shell/helper-pool.d.ts +3 -5
  18. package/dist/app/session-shell/helper-pool.js +3 -5
  19. package/dist/app/session-shell/input-surface-switch.d.ts +36 -0
  20. package/dist/app/session-shell/input-surface-switch.js +85 -0
  21. package/dist/app/session-shell/login-interaction.d.ts +27 -0
  22. package/dist/app/session-shell/login-interaction.js +91 -0
  23. package/dist/app/session-shell/prompt-submission.d.ts +48 -0
  24. package/dist/app/session-shell/prompt-submission.js +276 -0
  25. package/dist/app/session-shell/resident-tab-coordinator.d.ts +38 -0
  26. package/dist/app/session-shell/resident-tab-coordinator.js +77 -0
  27. package/dist/app/session-shell/response-copy-helper.js +21 -6
  28. package/dist/app/session-shell/response-copy-protocol.d.ts +1 -1
  29. package/dist/app/session-shell/response-copy-transport.d.ts +16 -6
  30. package/dist/app/session-shell/response-copy-transport.js +69 -17
  31. package/dist/app/session-shell/session-concern.d.ts +22 -0
  32. package/dist/app/session-shell/session-concern.js +29 -0
  33. package/dist/app/session-shell/session-lifecycle.d.ts +29 -0
  34. package/dist/app/session-shell/session-lifecycle.js +49 -0
  35. package/dist/app/session-shell/session-shell-root.d.ts +42 -6
  36. package/dist/app/session-shell/session-shell-root.js +197 -908
  37. package/dist/app/session-shell/session-shell.d.ts +3 -8
  38. package/dist/app/session-shell/session-shell.js +327 -835
  39. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -1
  40. package/dist/app/session-shell/session-viewport-controller.js +41 -38
  41. package/dist/app/session-shell/settings-bindings.d.ts +17 -0
  42. package/dist/app/session-shell/settings-bindings.js +91 -0
  43. package/dist/app/session-shell/shell-editor-factory.d.ts +27 -0
  44. package/dist/app/session-shell/shell-editor-factory.js +105 -0
  45. package/dist/app/session-shell/shell-host.d.ts +6 -0
  46. package/dist/app/session-shell/shell-host.js +34 -7
  47. package/dist/app/session-shell/shell-layout.d.ts +64 -0
  48. package/dist/app/session-shell/shell-layout.js +18 -0
  49. package/dist/app/session-shell/suggestion-surface.d.ts +25 -0
  50. package/dist/app/session-shell/suggestion-surface.js +63 -0
  51. package/dist/app/session-shell/transcript-document.d.ts +78 -0
  52. package/dist/app/session-shell/transcript-document.js +365 -0
  53. package/dist/app/session-shell/viewport-layout.d.ts +31 -0
  54. package/dist/app/session-shell/viewport-layout.js +160 -0
  55. package/dist/app/session-shell/workflow-output.d.ts +33 -0
  56. package/dist/app/session-shell/workflow-output.js +174 -0
  57. package/dist/app/session-shell/workflow-router.d.ts +25 -0
  58. package/dist/app/session-shell/workflow-router.js +39 -0
  59. package/dist/cli/dispatch.d.ts +11 -1
  60. package/dist/cli/dispatch.js +79 -5
  61. package/dist/cli/tabs-request.d.ts +7 -0
  62. package/dist/cli/tabs-request.js +1 -0
  63. package/dist/cli/tabs.d.ts +14 -0
  64. package/dist/cli/tabs.js +151 -0
  65. package/dist/cli/version-stats.js +38 -3
  66. package/dist/composition/owned-settings.d.ts +12 -0
  67. package/dist/composition/owned-settings.js +36 -0
  68. package/dist/composition/owned-ui.d.ts +27 -28
  69. package/dist/composition/owned-ui.js +207 -153
  70. package/dist/composition/reference-selection-options.d.ts +3 -0
  71. package/dist/composition/reference-selection-options.js +16 -0
  72. package/dist/composition/resident-tab-bridge.d.ts +25 -0
  73. package/dist/composition/resident-tab-bridge.js +180 -0
  74. package/dist/composition/session-backend.d.ts +19 -0
  75. package/dist/composition/session-backend.js +1 -0
  76. package/dist/composition/session-runtime-registry.d.ts +20 -0
  77. package/dist/composition/session-runtime-registry.js +85 -0
  78. package/dist/composition/settings-route-host.d.ts +16 -2
  79. package/dist/composition/settings-route-host.js +42 -17
  80. package/dist/composition/settings-search-input.d.ts +9 -0
  81. package/dist/composition/settings-search-input.js +61 -0
  82. package/dist/composition/startup-route.d.ts +32 -0
  83. package/dist/composition/startup-route.js +72 -0
  84. package/dist/composition/utility-tab.d.ts +28 -0
  85. package/dist/composition/utility-tab.js +244 -0
  86. package/dist/contracts/owned-ui/index.d.ts +1 -1
  87. package/dist/contracts/owned-ui/model.d.ts +3 -0
  88. package/dist/contracts/owned-ui/session-backend.d.ts +3 -0
  89. package/dist/contracts/owned-ui/session-workflows.d.ts +11 -4
  90. package/dist/contracts/presentation/index.d.ts +11 -0
  91. package/dist/contracts/presentation/index.js +12 -0
  92. package/dist/features/launch/development-launch.js +6 -0
  93. package/dist/features/launch/index.d.ts +1 -1
  94. package/dist/features/launch/intent.d.ts +3 -1
  95. package/dist/features/launch/intent.js +7 -4
  96. package/dist/features/launch/profile-paths.js +4 -0
  97. package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
  98. package/dist/features/owned-ui/plus-menu-dialog.js +84 -0
  99. package/dist/features/owned-ui/project-trust-dialog.d.ts +1 -1
  100. package/dist/features/owned-ui/project-trust-dialog.js +6 -8
  101. package/dist/features/owned-ui/project-trust-prompt.d.ts +1 -0
  102. package/dist/features/owned-ui/project-trust-prompt.js +7 -3
  103. package/dist/features/owned-ui/reference-document-selection.d.ts +30 -0
  104. package/dist/features/owned-ui/reference-document-selection.js +179 -0
  105. package/dist/features/owned-ui/reference-screen-app.d.ts +6 -1
  106. package/dist/features/owned-ui/reference-screen-app.js +51 -10
  107. package/dist/features/owned-ui/settings-app.d.ts +13 -2
  108. package/dist/features/owned-ui/settings-app.js +136 -97
  109. package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
  110. package/dist/features/owned-ui/settings-search-input.js +1 -0
  111. package/dist/features/prompt-history/index.d.ts +1 -1
  112. package/dist/features/prompt-history/paths.d.ts +2 -1
  113. package/dist/features/prompt-history/paths.js +10 -2
  114. package/dist/features/prompt-history/service.d.ts +5 -0
  115. package/dist/features/prompt-history/service.js +1 -1
  116. package/dist/features/resident-tab/auto-name.d.ts +11 -0
  117. package/dist/features/resident-tab/auto-name.js +44 -0
  118. package/dist/features/resident-tab/index.d.ts +5 -0
  119. package/dist/features/resident-tab/index.js +7 -0
  120. package/dist/features/resident-tab/shortcut-bindings-file.d.ts +8 -0
  121. package/dist/features/resident-tab/shortcut-bindings-file.js +40 -0
  122. package/dist/features/resident-tab/shortcut-declarations.d.ts +13 -0
  123. package/dist/features/resident-tab/shortcut-declarations.js +26 -0
  124. package/dist/features/resident-tab/shortcut-projection.d.ts +29 -0
  125. package/dist/features/resident-tab/shortcut-projection.js +64 -0
  126. package/dist/features/resident-tab/status-deriver.d.ts +10 -0
  127. package/dist/features/resident-tab/status-deriver.js +12 -0
  128. package/dist/foundation/launch-context/index.d.ts +12 -0
  129. package/dist/foundation/launch-context/index.js +26 -0
  130. package/dist/foundation/launch-guardian/exit-watcher.d.ts +14 -0
  131. package/dist/foundation/launch-guardian/exit-watcher.js +94 -0
  132. package/dist/foundation/launch-guardian/main.d.ts +7 -1
  133. package/dist/foundation/launch-guardian/main.js +60 -7
  134. package/dist/foundation/lifecycle/session-repository-context.js +16 -2
  135. package/dist/foundation/process-containment/{native-guardian-containment.d.ts → addon-containment.d.ts} +7 -5
  136. package/dist/foundation/process-containment/addon-containment.js +139 -0
  137. package/dist/foundation/process-containment/addon-process-inspector.d.ts +11 -0
  138. package/dist/foundation/process-containment/addon-process-inspector.js +36 -0
  139. package/dist/foundation/process-containment/addon.d.ts +42 -0
  140. package/dist/foundation/process-containment/addon.js +57 -0
  141. package/dist/foundation/process-containment/artifact.d.ts +1 -1
  142. package/dist/foundation/process-containment/artifact.js +6 -5
  143. package/dist/foundation/process-containment/contracts.d.ts +2 -0
  144. package/dist/foundation/process-containment/index.d.ts +4 -4
  145. package/dist/foundation/process-containment/index.js +3 -3
  146. package/dist/foundation/process-containment/native-process-inspector.d.ts +2 -1
  147. package/dist/foundation/process-containment/native-process-inspector.js +10 -10
  148. package/dist/foundation/release/automatic-update.d.ts +37 -0
  149. package/dist/foundation/release/automatic-update.js +265 -0
  150. package/dist/foundation/release/bootstrap.d.ts +7 -9
  151. package/dist/foundation/release/bootstrap.js +107 -24
  152. package/dist/foundation/release/cohort-state.d.ts +1 -1
  153. package/dist/foundation/release/cohort-state.js +1 -1
  154. package/dist/foundation/release/dependency-certification.js +54 -19
  155. package/dist/foundation/release/early-accent.d.ts +12 -0
  156. package/dist/foundation/release/early-accent.js +59 -0
  157. package/dist/foundation/release/index.d.ts +2 -2
  158. package/dist/foundation/release/index.js +2 -2
  159. package/dist/foundation/release/latest-release.d.ts +24 -2
  160. package/dist/foundation/release/latest-release.js +51 -1
  161. package/dist/foundation/release/progress-palette.generated.d.ts +137 -3
  162. package/dist/foundation/release/progress-palette.generated.js +137 -3
  163. package/dist/foundation/release/release-gc.d.ts +3 -0
  164. package/dist/foundation/release/release-gc.js +59 -8
  165. package/dist/foundation/release/release-store.d.ts +12 -0
  166. package/dist/foundation/release/release-store.js +124 -2
  167. package/dist/foundation/release/update-execution-lease.d.ts +48 -0
  168. package/dist/foundation/release/update-execution-lease.js +161 -0
  169. package/dist/foundation/release/update.d.ts +12 -7
  170. package/dist/foundation/release/update.js +281 -196
  171. package/dist/foundation/resident-tabs/bounded-host-command.d.ts +18 -0
  172. package/dist/foundation/resident-tabs/bounded-host-command.js +39 -0
  173. package/dist/foundation/resident-tabs/bridge-client.d.ts +70 -0
  174. package/dist/foundation/resident-tabs/bridge-client.js +321 -0
  175. package/dist/foundation/resident-tabs/doctor.d.ts +28 -0
  176. package/dist/foundation/resident-tabs/doctor.js +166 -0
  177. package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
  178. package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
  179. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +36 -0
  180. package/dist/foundation/resident-tabs/ensure-terminal-host.js +151 -0
  181. package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
  182. package/dist/foundation/resident-tabs/fault-injection.js +26 -0
  183. package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
  184. package/dist/foundation/resident-tabs/fixture-control.js +35 -0
  185. package/dist/foundation/resident-tabs/host-entry.d.ts +1 -0
  186. package/dist/foundation/resident-tabs/host-entry.js +17 -0
  187. package/dist/foundation/resident-tabs/index.d.ts +12 -0
  188. package/dist/foundation/resident-tabs/index.js +6 -0
  189. package/dist/foundation/resident-tabs/maintenance.d.ts +39 -0
  190. package/dist/foundation/resident-tabs/maintenance.js +75 -0
  191. package/dist/foundation/resident-tabs/progress-observation-schema.d.ts +34 -0
  192. package/dist/foundation/resident-tabs/progress-observation-schema.js +1 -0
  193. package/dist/foundation/resident-tabs/progress-observation.d.ts +4 -0
  194. package/dist/foundation/resident-tabs/progress-observation.js +150 -0
  195. package/dist/foundation/resident-tabs/prompt-journal.d.ts +63 -0
  196. package/dist/foundation/resident-tabs/prompt-journal.js +213 -0
  197. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  198. package/dist/foundation/resident-tabs/protocol.js +195 -0
  199. package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
  200. package/dist/foundation/resident-tabs/recovery.js +205 -0
  201. package/dist/foundation/resident-tabs/release-holds.d.ts +28 -0
  202. package/dist/foundation/resident-tabs/release-holds.js +75 -0
  203. package/dist/foundation/resident-tabs/routes.d.ts +5 -0
  204. package/dist/foundation/resident-tabs/routes.js +5 -0
  205. package/dist/foundation/resident-tabs/routing.d.ts +11 -0
  206. package/dist/foundation/resident-tabs/routing.js +10 -0
  207. package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
  208. package/dist/foundation/resident-tabs/session-sync.js +37 -0
  209. package/dist/foundation/resident-tabs/settings.d.ts +12 -0
  210. package/dist/foundation/resident-tabs/settings.js +58 -0
  211. package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +14 -0
  212. package/dist/foundation/resident-tabs/terminal-host-artifact.js +78 -0
  213. package/dist/foundation/resident-tabs/zip-archive.d.ts +9 -0
  214. package/dist/foundation/resident-tabs/zip-archive.js +56 -0
  215. package/dist/foundation/session-writer-lock/index.d.ts +30 -0
  216. package/dist/foundation/session-writer-lock/index.js +203 -0
  217. package/dist/foundation/startup/startup-descriptor.js +2 -2
  218. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  219. package/dist/foundation/terminal-cleanup/index.js +1 -1
  220. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +6 -0
  221. package/dist/foundation/terminal-cleanup/terminal-reset.js +6 -0
  222. package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
  223. package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
  224. package/dist/integrations/pi/components/modal-frame.js +48 -9
  225. package/dist/integrations/pi/components/models-dialog.js +4 -52
  226. package/dist/integrations/pi/components/owned-editor-ux.d.ts +6 -1
  227. package/dist/integrations/pi/components/owned-editor-ux.js +138 -11
  228. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
  229. package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
  230. package/dist/integrations/pi/components/share-operation-dialog.d.ts +2 -0
  231. package/dist/integrations/pi/components/share-operation-dialog.js +2 -1
  232. package/dist/integrations/pi/components/shell-editor-autocomplete.js +58 -36
  233. package/dist/integrations/pi/components/shell-extension-ui.js +79 -60
  234. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  235. package/dist/integrations/pi/components/shell-footer-status.js +21 -5
  236. package/dist/integrations/pi/components/shell-hotkey-sections.js +1 -1
  237. package/dist/integrations/pi/components/shell-presenters-info.d.ts +5 -2
  238. package/dist/integrations/pi/components/shell-presenters-info.js +18 -4
  239. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
  240. package/dist/integrations/pi/components/shell-selectors-dialogs.js +5 -1
  241. package/dist/integrations/pi/components/shell-shared-facade.d.ts +12 -1
  242. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  243. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  244. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
  245. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  246. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  247. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  248. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  249. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  250. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  251. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  252. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  253. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +8 -6
  254. package/dist/integrations/pi/components/upstream/components/owned-editor.js +16 -9
  255. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  256. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +4 -4
  257. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
  258. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  259. package/dist/integrations/pi/components/upstream/components/session-footer.js +31 -25
  260. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  261. package/dist/integrations/pi/components/upstream/components/session-selector.js +8 -7
  262. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  263. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  264. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
  265. package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
  266. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  267. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  268. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +8 -7
  269. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +10 -9
  270. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +2 -6
  271. package/dist/integrations/pi/components/upstream/components/tool-execution.js +103 -101
  272. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +9 -9
  273. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -21
  274. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  275. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  276. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
  277. package/dist/integrations/pi/components/upstream/history/editor-core.js +7 -5
  278. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  279. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  280. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  281. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  282. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  283. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  284. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  285. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  286. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  287. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  288. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +5 -0
  289. package/dist/integrations/pi/components/upstream/theme/theme.js +29 -2
  290. package/dist/integrations/pi/engine/adapter.d.ts +5 -77
  291. package/dist/integrations/pi/engine/adapter.js +95 -394
  292. package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
  293. package/dist/integrations/pi/engine/command-dispatch.js +57 -0
  294. package/dist/integrations/pi/engine/host.js +2 -2
  295. package/dist/integrations/pi/engine/offline-settings-port.d.ts +23 -0
  296. package/dist/integrations/pi/engine/offline-settings-port.js +53 -0
  297. package/dist/integrations/pi/engine/pi-settings-metadata.json +14 -2
  298. package/dist/integrations/pi/engine/ports.d.ts +56 -0
  299. package/dist/integrations/pi/engine/ports.js +93 -0
  300. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
  301. package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
  302. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  303. package/dist/integrations/pi/engine/runtime-integration.js +102 -29
  304. package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
  305. package/dist/integrations/pi/engine/session-runtime.js +13 -5
  306. package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
  307. package/dist/integrations/pi/engine/session-selection.js +45 -9
  308. package/dist/integrations/pi/engine/settings-bridge.d.ts +5 -0
  309. package/dist/integrations/pi/engine/settings-bridge.js +15 -0
  310. package/dist/integrations/pi/engine/settings-port.js +11 -15
  311. package/dist/integrations/pi/engine/view-state.d.ts +42 -0
  312. package/dist/integrations/pi/engine/view-state.js +126 -0
  313. package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
  314. package/dist/integrations/pi/engine/workflow-runner.js +74 -5
  315. package/dist/integrations/pi/engine/workflow-support.d.ts +12 -0
  316. package/dist/integrations/pi/engine/workflow-support.js +36 -0
  317. package/dist/integrations/pi/engine/workflows.d.ts +3 -1
  318. package/dist/integrations/pi/engine/workflows.js +1 -0
  319. package/dist/integrations/pi/startup-public.js +257 -310
  320. package/dist/integrations/pi/startup-public.manifest.json +564 -440
  321. package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
  322. package/dist/integrations/pi/tui-runtime/adapter.js +32 -1
  323. package/dist/integrations/pi/tui-runtime/contracts.d.ts +5 -0
  324. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
  325. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +31 -2
  326. package/dist/native/darwin-arm64/manifest.json +10 -7
  327. package/dist/native/darwin-arm64/process-guardian.node +0 -0
  328. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
  329. package/dist/native/darwin-arm64/terminal-host.manifest.json +46 -0
  330. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  331. package/dist/native/darwin-x64/manifest.json +23 -0
  332. package/dist/native/darwin-x64/process-guardian.node +0 -0
  333. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
  334. package/dist/native/darwin-x64/terminal-host.manifest.json +46 -0
  335. package/dist/native/darwin-x64/terminal-host.node +0 -0
  336. package/dist/native/linux-x64/manifest.json +10 -7
  337. package/dist/native/linux-x64/process-guardian.node +0 -0
  338. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
  339. package/dist/native/linux-x64/terminal-host.manifest.json +46 -0
  340. package/dist/native/linux-x64/terminal-host.node +0 -0
  341. package/dist/native/win32-x64/manifest.json +10 -7
  342. package/dist/native/win32-x64/process-guardian.node +0 -0
  343. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
  344. package/dist/native/win32-x64/terminal-host.manifest.json +46 -0
  345. package/dist/native/win32-x64/terminal-host.node +0 -0
  346. package/dist/product-identity.d.ts +3 -3
  347. package/dist/product-identity.js +4 -4
  348. package/dist/product-identity.json +16 -2
  349. package/dist/runtime-payload-inventory.json +6330 -6677
  350. package/dist/ui/apps/host.d.ts +2 -0
  351. package/dist/ui/apps/host.js +7 -1
  352. package/dist/ui/components/dialog-panel.d.ts +17 -8
  353. package/dist/ui/components/dialog-panel.js +117 -41
  354. package/dist/ui/components/index.d.ts +5 -5
  355. package/dist/ui/components/index.js +4 -4
  356. package/dist/ui/components/line-input.d.ts +4 -1
  357. package/dist/ui/components/line-input.js +3 -1
  358. package/dist/ui/components/prompt-input.d.ts +11 -8
  359. package/dist/ui/components/prompt-input.js +15 -13
  360. package/dist/ui/components/scrollbar.d.ts +1 -1
  361. package/dist/ui/components/scrollbar.js +1 -1
  362. package/dist/ui/components/selection-copy.d.ts +3 -3
  363. package/dist/ui/components/selection-copy.js +16 -1
  364. package/dist/ui/components/spans.js +2 -2
  365. package/dist/ui/components/submitted-prompt.d.ts +9 -6
  366. package/dist/ui/components/submitted-prompt.js +20 -12
  367. package/dist/ui/components/surface.js +4 -2
  368. package/dist/ui/components/text-selection.d.ts +1 -2
  369. package/dist/ui/components/text-selection.js +5 -2
  370. package/dist/ui/components/text.d.ts +3 -6
  371. package/dist/ui/components/text.js +6 -8
  372. package/dist/ui/components/transcript-viewport.d.ts +9 -2
  373. package/dist/ui/components/transcript-viewport.js +179 -58
  374. package/dist/ui/settings/declarations.d.ts +90 -16
  375. package/dist/ui/settings/declarations.js +67 -14
  376. package/dist/ui/settings/early-accent.d.ts +81 -0
  377. package/dist/ui/settings/early-accent.js +50 -0
  378. package/dist/ui/settings/index.d.ts +4 -2
  379. package/dist/ui/settings/index.js +2 -1
  380. package/dist/ui/settings/manager.d.ts +16 -3
  381. package/dist/ui/settings/manager.js +122 -33
  382. package/dist/ui/settings/migrations.d.ts +6 -0
  383. package/dist/ui/settings/migrations.js +73 -0
  384. package/dist/ui/settings/profile-document.d.ts +26 -0
  385. package/dist/ui/settings/profile-document.js +164 -0
  386. package/dist/ui/settings/progress-palette.generated.d.ts +139 -0
  387. package/dist/ui/settings/progress-palette.generated.js +139 -0
  388. package/dist/ui/settings/sections.d.ts +4 -0
  389. package/dist/ui/settings/sections.js +9 -3
  390. package/docs/architecture/boundaries.md +24 -9
  391. package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
  392. package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
  393. package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
  394. package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
  395. package/docs/architecture/internal-naming.md +3 -1
  396. package/docs/architecture/process-guardian-provenance.md +18 -10
  397. package/docs/architecture/project-structure.md +8 -0
  398. package/docs/architecture/resident-tabs-requirements.md +18 -0
  399. package/docs/architecture/resident-tabs.md +649 -0
  400. package/docs/architecture/response-copy-delivery.md +1 -1
  401. package/docs/architecture/terminal-host-proof-gate.md +2 -0
  402. package/docs/architecture/terminal-host-provenance.md +16 -4
  403. package/docs/architecture/tool-image-presentation.md +2 -1
  404. package/docs/architecture/toolchain.md +11 -9
  405. package/docs/ci-release-runbook.md +14 -7
  406. package/docs/features/launch-profiles.md +11 -8
  407. package/docs/features/prompt-history.md +41 -20
  408. package/docs/manual-resident-tabs-acceptance.md +51 -0
  409. package/docs/openspec-archive-automation.md +49 -22
  410. package/docs/validation.md +49 -9
  411. package/package.json +19 -6
  412. package/dist/foundation/process-containment/darwin-process-inspector.d.ts +0 -5
  413. package/dist/foundation/process-containment/darwin-process-inspector.js +0 -7
  414. package/dist/foundation/process-containment/native-guardian-containment.js +0 -152
  415. package/dist/foundation/process-containment/windows-process-inspector.d.ts +0 -19
  416. package/dist/foundation/process-containment/windows-process-inspector.js +0 -56
  417. package/dist/native/darwin-arm64/process-guardian +0 -0
  418. package/dist/native/linux-x64/process-guardian +0 -0
  419. package/dist/native/win32-x64/process-guardian.exe +0 -0
@@ -1,35 +1,32 @@
1
- import { acceptsTranscriptUpdate, ImageAttachmentError } from "../../contracts/owned-ui/index.js";
2
- import { caretCell } from "../../ui/components/line-input.js";
3
- import { PromptInput, promptArrow } from "../../ui/components/prompt-input.js";
4
- import { composeSubmittedPromptRows, formatSubmittedPromptTime, submittedPromptLayout, } from "../../ui/components/submitted-prompt.js";
5
- import { backgroundSgrSpan, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, } from "../../ui/components/spans.js";
1
+ import { ImageAttachmentError } from "../../contracts/owned-ui/index.js";
2
+ import { promptArrow } from "../../ui/components/prompt-input.js";
3
+ import { composeSubmittedPromptRows, submittedPromptLayout, } from "../../ui/components/submitted-prompt.js";
4
+ import { nativeHyperlinkStyle } from "../../ui/components/spans.js";
5
+ import { paintTextSelection } from "../../ui/components/text-selection.js";
6
6
  import { progressStatusFrame, progressStatusText } from "../../ui/components/progress-status.js";
7
- import { displayWidth, faint, stripAnsi } from "../../ui/components/text.js";
8
- import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
9
7
  import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
10
- import { createPiShellArmin, createPiShellEarendilAnnouncement, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
11
- import { createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellCommandMessage, renderPiShellPackageUpdateNotice, renderPiShellReleaseUpdateBanner, renderPiShellReleaseUpdateNotice, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
12
- import { createPiShellTranscriptComponent, isPiPromptStyleCompaction, paintPiSubmittedPromptTimestamp, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
13
- import { onPiThemeChange, PINNED_PI_LAYOUT, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
14
- import { piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, } from "../../integrations/pi/components/shell-shared-facade.js";
15
- import { classifyPiTuiInput, } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
8
+ import { createPiShellTranscriptComponent, } from "../../integrations/pi/components/shell-presenters-transcript.js";
9
+ import { onPiThemeChange, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
10
+ import { piShellTruncateToWidth, } from "../../integrations/pi/components/shell-shared-facade.js";
11
+ import {} from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
16
12
  import { PromptChipStore } from "./prompt-chips.js";
17
- import { EditorHyperlinkBudget } from "./editor-hyperlink-budget.js";
18
13
  import { SessionViewportController } from "./session-viewport-controller.js";
19
- import { canPreparePasteInline } from "./paste-text-preparation.js";
20
- /** Composes backend session state into the owned transcript, viewport, editor, and dock presentation. */
14
+ import { createShellEditor } from "./shell-editor-factory.js";
15
+ import { ExtensionSurface } from "./extension-surface.js";
16
+ import { DockChrome } from "./dock-chrome.js";
17
+ import { WorkflowOutput } from "./workflow-output.js";
18
+ import { InputSurfaceSwitch } from "./input-surface-switch.js";
19
+ import { PinnedLayout } from "./shell-layout.js";
20
+ import { ViewportLayout } from "./viewport-layout.js";
21
+ import { nativeTranscriptLinkColor, TranscriptDocument } from "./transcript-document.js";
22
+ /** Composes session state into the owned transcript, viewport, editor, and dock. */
21
23
  export class OwnedUiSessionShellRoot {
22
24
  editor;
23
25
  header;
24
26
  resources;
25
27
  #cwd;
26
- #transcript = new Map();
27
- #blocksById = new Map();
28
- #renderedRows = new Map();
29
- // Performance: document layouts are shared by wheel, rail, jump, and selection frames.
30
- #documentLayouts = new Map();
28
+ #document;
31
29
  #themeUnsubscribe;
32
- #transcriptOrder = [];
33
30
  #view;
34
31
  #status;
35
32
  #footer;
@@ -37,15 +34,10 @@ export class OwnedUiSessionShellRoot {
37
34
  #extensionRenderers;
38
35
  #imageAssets;
39
36
  #promptChips;
40
- #editorHyperlinks = new EditorHyperlinkBudget();
41
37
  #pasteDiagnosticId = 0;
42
- #terminalPasteDiagnosticRequest;
43
38
  #customViewport;
44
39
  #submittedPromptComposer;
45
40
  #viewportController;
46
- #viewportTheme;
47
- #onViewportFrame;
48
- #onInputSurfaceChanged;
49
41
  #onPromptSuggestionEligibilityChanged;
50
42
  #componentRuntime;
51
43
  #toolsExpanded = false;
@@ -55,33 +47,11 @@ export class OwnedUiSessionShellRoot {
55
47
  #showImages = true;
56
48
  #imageWidthCells = 80;
57
49
  #outputPad = 1;
58
- #workflowTranscriptSequence = 0;
59
- #workflowStatusAnchors = new Map();
60
- #workflowStatusMessages = new Map();
61
- #lastWorkflowStatusId;
62
- // Invariant: the notice is dock chrome, never transcript content; the custom viewport alone uses it.
63
- #dockNotice;
64
- // Invariant: bare A1 docks the release notice above the live status; closing it lasts for this session only.
65
- #releaseNoticeDismissed = false;
66
- #releaseCloseTarget;
67
- #copyAcknowledgement;
68
- #copyAcknowledgementTimer;
69
- #inputSurface;
70
- #inputSurfaceCoordination = "editor";
71
- #dockInputReuseEnabled;
72
- #dockInputCandidate = false;
73
- // Invariant: receipt-time eligibility is evidence, never authorization for composition-time reuse.
74
- #dockInputRevision;
75
- #visibleViewportSnapshot;
76
- #fullViewportCompositions = 0;
77
- #dockOnlyViewportCompositions = 0;
78
- #transcriptBlockRenders = 0;
79
- #extensionHeader = null;
80
- #extensionFooter = null;
81
- #extensionWidgets = new Map();
82
- #extensionStatuses = new Map();
83
- #extensionWorkingMessage;
84
- #extensionWorkingVisible = true;
50
+ #workflowOutput;
51
+ #input;
52
+ #layout;
53
+ #extensions = new ExtensionSurface();
54
+ #dockChrome;
85
55
  #imageSubmissionsSending = 0;
86
56
  constructor(view, cwd, handlers, startup = {}, agentDir, extensionRenderers = {
87
57
  getMessageRenderer: () => undefined,
@@ -94,21 +64,13 @@ export class OwnedUiSessionShellRoot {
94
64
  ...(handlers.pasteDiagnostics === undefined ? {} : { onEvent: handlers.pasteDiagnostics }),
95
65
  ...(handlers.pastePreparation === undefined ? {} : { preparation: handlers.pastePreparation }),
96
66
  ...(handlers.promptImageLimit === undefined ? {} : { imageLimit: handlers.promptImageLimit }) });
97
- this.#submittedPromptComposer = this.#customViewport
98
- ? {
99
- layout: submittedPromptLayout,
100
- compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, { ...style, prefix: text => promptArrow(text, piTheme()) })
101
- .map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor)),
102
- }
103
- : undefined;
67
+ this.#submittedPromptComposer = this.#customViewport ? this.#createSubmittedPromptComposer() : undefined;
104
68
  this.#cwd = cwd;
69
+ this.#document = this.#createDocument(cwd);
105
70
  this.#extensionRenderers = extensionRenderers;
106
71
  this.#imageAssets = imageAssets;
107
72
  this.#componentRuntime = handlers;
108
- this.#onViewportFrame = handlers.onViewportFrame;
109
- this.#onInputSurfaceChanged = handlers.onInputSurfaceChanged;
110
73
  this.#onPromptSuggestionEligibilityChanged = handlers.onPromptSuggestionEligibilityChanged;
111
- this.#dockInputReuseEnabled = handlers.enableDockInputReuse ?? true;
112
74
  this.header = createPiShellHeader({
113
75
  ...startup,
114
76
  ...(this.#customViewport ? { getKeybindings: () => this.editor.keybindingConfig() } : {}),
@@ -119,81 +81,16 @@ export class OwnedUiSessionShellRoot {
119
81
  frame: progressStatusFrame,
120
82
  }, handlers);
121
83
  this.#status.setProgressPresentation(this.#customViewport ? "custom-viewport" : "pinned");
122
- this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel);
123
- this.editor = createPiShellEditor({
124
- ...handlers,
125
- keybindingProfile: this.#customViewport ? "a1" : "pi",
126
- ...(handlers.keybindings === undefined ? {} : { keybindings: handlers.keybindings }),
127
- paintEditorSelection: (line, from, to, atomic) => backgroundSgrSpan(line, from, to, atomic ? "\u001b[7m" : "\u001b[27m\u001b[48;2;38;79;120m", atomic ? "\u001b[27m" : "\u001b[49m", piShellVisibleWidth),
128
- transformPastedContent: content => {
129
- try {
130
- return this.#promptChips.transformPastedContent(content, this.editor.getText());
131
- }
132
- catch (error) {
133
- handlers.onPasteRejected?.(error);
134
- return "";
135
- }
136
- },
137
- ...(this.#customViewport ? {
138
- beginClipboardPaste: () => this.#promptChips.beginPaste(this.editor.getText(), handlers.captureClipboardPaste?.() ?? { kind: "provided", read: signal => handlers.readClipboardContent?.(signal) ?? Promise.resolve(null) }, error => handlers.onPasteRejected?.(error), () => handlers.requestRender()),
139
- onPasteInput: (bytes, phase) => {
140
- try {
141
- if (phase === "framing")
142
- this.#terminalPasteDiagnosticRequest = --this.#pasteDiagnosticId;
143
- const request = this.#terminalPasteDiagnosticRequest ?? --this.#pasteDiagnosticId;
144
- handlers.pasteDiagnostics?.({ request, phase, bytes, atMs: performance.now(), pending: 0, transport: "terminal",
145
- ...(phase === "settled" ? { outcome: "ready" } : {}) });
146
- if (phase === "settled")
147
- this.#terminalPasteDiagnosticRequest = undefined;
148
- }
149
- catch { /* Invariant: payload-free diagnostics cannot interfere with the input path. */ }
150
- },
151
- deferTextPaste: text => !canPreparePasteInline(text),
152
- beginTextPaste: text => this.#promptChips.beginPaste(this.editor.getText(), { kind: "text", text }, error => handlers.onPasteRejected?.(error)),
153
- } : {}),
154
- editorAtomicRanges: line => this.#promptChips.atomicRanges(line),
155
- editorHiddenRanges: line => this.#promptChips.hiddenRanges(line),
156
- decorateEditorRow: (row, width, rowIndex) => {
157
- if (rowIndex === 0)
158
- this.#editorHyperlinks.reset();
159
- const plain = stripAnsi(row);
160
- const dimmed = this.#promptChips.unsentRanges(plain).reduce((result, range) => backgroundSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), "\u001b[2m", "\u001b[22m", piShellVisibleWidth), row);
161
- const ranges = this.#promptChips.hyperlinkRanges(plain);
162
- if (ranges.length === 0 || !this.#editorHyperlinks.takeCleanup())
163
- return dimmed;
164
- const linkResetAndTail = `\u001b]8;;\u001b\\\u001b[24m${" ".repeat(Math.max(0, width - piShellVisibleWidth(dimmed)))}`;
165
- // Platform: VS15 is zero-column and default-ignorable. It breaks Windows Terminal's
166
- // plain-text URL detector only in the held-button paint; semantic text stays exact.
167
- const paintRow = this.#viewportController.editorPointerSelecting
168
- ? dimmed.replaceAll("https://", "https:\uFE0E//").replaceAll("http://", "http:\uFE0E//")
169
- : dimmed;
170
- const decorated = this.#viewportController.editorPointerSelecting
171
- ? ranges.reduce((result, range) => backgroundSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), "\u001b[4:4m", "\u001b[24m", piShellVisibleWidth), paintRow)
172
- : ranges.filter(range => this.#editorHyperlinks.take(range.target)).reduce((result, range) => hyperlinkSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), range.target, piShellVisibleWidth), dimmed);
173
- // Platform: Windows Terminal can retain stale native dotted-link cells when a mutable
174
- // prompt replaces a longer URL. Explicit non-link spaces overwrite that tail.
175
- return `${decorated}${linkResetAndTail}`;
176
- },
177
- expandCopiedEditorText: text => this.#promptChips.expandCopiedText(text),
84
+ this.#footer = createPiShellFooter(this.#extensions.withStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel, handlers.footerSessionName ?? true);
85
+ this.editor = createShellEditor({
86
+ handlers,
87
+ customViewport: this.#customViewport,
178
88
  cwd,
179
89
  ...(agentDir === undefined ? {} : { agentDir }),
180
- onToolsExpand: () => this.#setToolsExpanded(!this.#toolsExpanded),
181
- ...(this.#customViewport ? {
182
- promptPresentation: {
183
- input: new PromptInput({ fg: (token, text) => piTheme().fg(token, text) }, {
184
- measure: piShellVisibleWidth,
185
- truncate: piShellTruncateToWidth,
186
- }),
187
- styleSuggestion: faint,
188
- styleSuggestionCaret: caretCell,
189
- },
190
- } : {}),
191
- onChange: text => {
192
- handlers.onEditorChange?.(text);
193
- // Concurrency: Pi clears before onSubmit; capture waiting references before pruning removed chips.
194
- queueMicrotask(() => this.#promptChips.reconcileDraft(this.editor.getText()));
195
- },
196
- ...(handlers.onPromptSuggestionAccepted === undefined ? {} : { onPromptSuggestionAccepted: handlers.onPromptSuggestionAccepted }),
90
+ promptChips: this.#promptChips,
91
+ editorPointerSelecting: () => this.#viewportController.editorPointerSelecting,
92
+ nextPasteDiagnosticRequest: () => --this.#pasteDiagnosticId,
93
+ toggleToolsExpanded: () => this.#setToolsExpanded(!this.#toolsExpanded),
197
94
  });
198
95
  this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned", this.#customViewport ? () => this.editor.keybindingConfig() : undefined);
199
96
  this.#viewportController = new SessionViewportController({
@@ -205,33 +102,85 @@ export class OwnedUiSessionShellRoot {
205
102
  ...(handlers.pasteDiagnostics === undefined ? {} : { pasteDiagnostics: handlers.pasteDiagnostics }),
206
103
  nextPasteDiagnosticRequest: () => --this.#pasteDiagnosticId,
207
104
  });
208
- // Performance: stable painter identities let the neutral viewport retain row-level
209
- // selection variants while each callback still resolves the live Pi theme.
210
- this.#viewportTheme = {
211
- track: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("dim", text)}`,
212
- thumb: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("accent", text)}`,
213
- sticky: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", withoutTerminalBackground(text))),
214
- quietSticky: text => `\u001b[2m${text.replace(/\u001b\[(?:0|22)m/g, "$&\u001b[2m")}\u001b[22m`,
215
- bottomControl: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", text)),
216
- selection: (line, from, to) => backgroundSgrSpan(line, from, to, "\u001b[48;2;38;79;120m", "\u001b[49m"),
217
- };
105
+ this.#dockChrome = new DockChrome({
106
+ invalidateChrome: () => this.#invalidateChrome(),
107
+ requestRender: () => this.#componentRuntime.requestRender(),
108
+ viewport: this.#viewportController,
109
+ });
110
+ this.#workflowOutput = new WorkflowOutput({
111
+ customViewport: this.#customViewport,
112
+ document: this.#document,
113
+ dock: this.#dockChrome,
114
+ transcriptLength: () => this.#view.transcript.length,
115
+ outputPad: () => this.#outputPad,
116
+ hotkeysPresentation: () => this.hotkeysPresentation(),
117
+ componentRuntime: this.#componentRuntime,
118
+ invalidate: () => this.invalidate(),
119
+ });
218
120
  this.editor.setThinkingLevel(view.thinkingLevel);
219
- this.#inputSurface = this.editor;
121
+ this.#input = new InputSurfaceSwitch(this.editor, {
122
+ dockInputReuse: this.#customViewport && (handlers.enableDockInputReuse ?? true),
123
+ viewport: this.#viewportController,
124
+ ...(handlers.onInputSurfaceChanged === undefined ? {} : { onChanged: handlers.onInputSurfaceChanged }),
125
+ });
126
+ this.#layout = this.#createLayout(handlers.onViewportFrame);
220
127
  for (const block of view.transcript) {
221
128
  if (block.kind === "user")
222
129
  this.editor.addToHistory(block.text);
223
130
  }
224
- this.#syncTranscript(view.transcript);
225
- // Invariant: colours come from the active theme, so rendered rows outlive their revision only
226
- // until the theme under them changes.
131
+ this.#document.sync(view.transcript);
132
+ // Invariant: rendered rows expire when
133
+ // their active theme changes.
227
134
  this.#themeUnsubscribe = onPiThemeChange(() => {
228
135
  this.invalidate();
229
136
  this.#componentRuntime.requestRender(true);
230
137
  });
231
138
  }
139
+ // Invariant: the document reads the shell's chrome lazily, so it may be built before the header and viewport.
140
+ #createDocument(cwd) {
141
+ return new TranscriptDocument({
142
+ customViewport: this.#customViewport,
143
+ cwd,
144
+ mount: block => this.#mountTranscript(block),
145
+ chrome: {
146
+ diagnostics: () => this.#view.diagnostics,
147
+ headerRows: width => this.#extensions.header !== null ? this.#extensions.header.render(width)
148
+ : this.#customViewport ? [] : this.header.render(width),
149
+ resourceRows: width => this.#customViewport ? [] : this.resources.render(width),
150
+ hasExtensionHeader: () => this.#extensions.header !== null,
151
+ thinkingVisible: () => this.#thinkingVisible,
152
+ timeFormat: () => this.#viewportController.config.timeFormat,
153
+ },
154
+ });
155
+ }
156
+ // Invariant: one layout per root; the render path never consults the layout mode again.
157
+ #createLayout(onFrame) {
158
+ const ports = {
159
+ viewport: this.#viewportController,
160
+ input: this.#input,
161
+ editor: this.editor,
162
+ promptChips: this.#promptChips,
163
+ dockChrome: this.#dockChrome,
164
+ theme: VIEWPORT_THEME,
165
+ ...(onFrame === undefined ? {} : { onFrame }),
166
+ rows: () => this.#componentRuntime.getRows(),
167
+ dock: width => this.#renderDockLayout(width),
168
+ document: width => this.#document.layout(width),
169
+ queued: width => this.#renderQueued(width),
170
+ liveStatus: width => this.#renderStatus(width),
171
+ diagnostics: () => this.#view.diagnostics,
172
+ };
173
+ return this.#customViewport ? new ViewportLayout(ports) : new PinnedLayout(ports);
174
+ }
175
+ #createSubmittedPromptComposer() {
176
+ return {
177
+ layout: (width, source) => submittedPromptLayout(width, source, this.#viewportController.config.timeFormat ?? "8:00 PM"),
178
+ compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, { ...style, prefix: text => promptArrow(text, piTheme()) }, this.#viewportController.config.timeFormat ?? "8:00 PM").map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor)),
179
+ };
180
+ }
232
181
  setEditorPaddingX(padding) {
233
182
  this.editor.setPaddingX(padding);
234
- this.#documentLayouts.clear();
183
+ this.#document.invalidate("editor-padding");
235
184
  }
236
185
  setAutocompleteMaxVisible(maxVisible) {
237
186
  this.editor.setAutocompleteMaxVisible(maxVisible);
@@ -241,7 +190,7 @@ export class OwnedUiSessionShellRoot {
241
190
  return;
242
191
  this.#outputPad = padding;
243
192
  this.#status.setOutputPad(padding);
244
- for (const component of this.#transcript.values())
193
+ for (const component of this.#document.components())
245
194
  component.setOutputPad(padding);
246
195
  this.#invalidateTranscriptPresentation();
247
196
  }
@@ -249,7 +198,7 @@ export class OwnedUiSessionShellRoot {
249
198
  if (this.#thinkingVisible === !hidden)
250
199
  return;
251
200
  this.#thinkingVisible = !hidden;
252
- for (const component of this.#transcript.values())
201
+ for (const component of this.#document.components())
253
202
  component.setHideThinkingBlock(hidden);
254
203
  this.#invalidateTranscriptPresentation();
255
204
  }
@@ -257,7 +206,7 @@ export class OwnedUiSessionShellRoot {
257
206
  if (this.#mermaidRenderingMode === mode)
258
207
  return;
259
208
  this.#mermaidRenderingMode = mode;
260
- for (const component of this.#transcript.values())
209
+ for (const component of this.#document.components())
261
210
  component.setMermaidRenderingMode(mode);
262
211
  this.#invalidateTranscriptPresentation();
263
212
  }
@@ -266,7 +215,7 @@ export class OwnedUiSessionShellRoot {
266
215
  return;
267
216
  this.#showImages = showImages;
268
217
  this.#imageWidthCells = imageWidthCells;
269
- for (const component of this.#transcript.values())
218
+ for (const component of this.#document.components())
270
219
  component.setImagePresentation(showImages, imageWidthCells);
271
220
  this.#invalidateTranscriptPresentation();
272
221
  }
@@ -286,7 +235,7 @@ export class OwnedUiSessionShellRoot {
286
235
  return this.#promptChips.prepareSubmission(text);
287
236
  }
288
237
  reconcilePromptImageLimitNotice(text = this.editor.getText()) {
289
- if (this.#dockNotice?.errorCode !== "image-count" || !this.#promptChips.imageLimitState(text).corrected)
238
+ if (this.#dockChrome.notice?.errorCode !== "image-count" || !this.#promptChips.imageLimitState(text).corrected)
290
239
  return false;
291
240
  this.dismissNotice();
292
241
  return true;
@@ -332,22 +281,22 @@ export class OwnedUiSessionShellRoot {
332
281
  const becameReady = this.#view.lifecycle !== "ready" && view.lifecycle === "ready";
333
282
  if (view.diagnostics.length !== this.#view.diagnostics.length
334
283
  || view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
335
- this.#documentLayouts.clear();
284
+ this.#document.invalidate("diagnostics");
336
285
  }
337
286
  const promptAccepted = this.#view.lifecycle !== "busy" && view.lifecycle === "busy";
338
287
  this.#view = view;
339
288
  // Invariant: the idle new-session confirmation yields to the first truthful live status;
340
289
  // ordinary dock notices deliberately remain visible below Working during a run.
341
- if (promptAccepted && this.#dockNotice?.kind === "new-session")
342
- this.#dockNotice = undefined;
290
+ if (promptAccepted && this.#dockChrome.notice?.kind === "new-session")
291
+ this.#dockChrome.forgetNotice();
343
292
  if (promptAccepted && this.#imageSubmissionsSending > 0) {
344
293
  this.#imageSubmissionsSending = 0;
345
294
  this.#syncWorkingOverride();
346
295
  }
347
296
  this.#status.update(view);
348
- this.#footer.update(this.#viewWithExtensionStatuses(view));
297
+ this.#footer.update(this.#extensions.withStatuses(view));
349
298
  this.#queued.update(view.editor.queuedSubmissions);
350
- this.#syncTranscript(view.transcript);
299
+ this.#document.sync(view.transcript);
351
300
  this.editor.setSubmitEnabled(view.lifecycle !== "stopping" && view.lifecycle !== "stopped" && view.lifecycle !== "failed");
352
301
  this.editor.setThinkingLevel(view.thinkingLevel);
353
302
  // Performance: semantic updates already invalidate affected transcript blocks. Chrome is a separate authority.
@@ -361,24 +310,7 @@ export class OwnedUiSessionShellRoot {
361
310
  * walk of the whole transcript.
362
311
  */
363
312
  applyTranscriptBlock(block) {
364
- const current = this.#blocksById.get(block.id);
365
- // Invariant: a full-view/final presentation may preempt queued partials. Never revive an older revision.
366
- if (current !== undefined && !acceptsTranscriptUpdate(current, block))
367
- return;
368
- this.#blocksById.set(block.id, block);
369
- const component = this.#transcript.get(block.id);
370
- if (component === undefined) {
371
- const created = this.#mountTranscript(block);
372
- this.#transcript.set(block.id, created);
373
- this.#transcriptOrder.push(block.id);
374
- }
375
- else if (component.revision !== block.revision) {
376
- component.update(block);
377
- }
378
- // Performance: one chunk touches one block, and the updated component tracks its own dirtiness.
379
- // Invalidating the whole shell here would re-wrap the entire transcript per chunk.
380
- this.#renderedRows.delete(block.id);
381
- this.#documentLayouts.clear();
313
+ this.#document.apply(block);
382
314
  }
383
315
  #mountTranscript(block) {
384
316
  // Invariant: the notice answers the reader's last command, so only their next prompt or
@@ -386,129 +318,18 @@ export class OwnedUiSessionShellRoot {
386
318
  if (block.kind === "user" || block.kind === "bash")
387
319
  this.dismissNotice();
388
320
  const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer, this.#outputPad, !this.#thinkingVisible, this.#mermaidRenderingMode, this.#showImages, this.#imageWidthCells, this.#imageAssets, { ...this.#componentRuntime, changed: () => {
389
- if (this.#transcript.get(block.id) !== created)
321
+ if (this.#document.component(block.id) !== created)
390
322
  return;
391
- this.#renderedRows.delete(block.id);
392
- this.#documentLayouts.clear();
393
- this.#visibleViewportSnapshot = undefined;
394
- this.#dockInputCandidate = false;
395
- this.#dockInputRevision = undefined;
323
+ this.#document.blockChanged(block.id);
324
+ this.#layout.invalidate();
325
+ this.#input.clearDockInputCandidate();
396
326
  this.#componentRuntime.requestRender();
397
327
  } });
398
328
  created.setExpanded(this.#toolsExpanded);
399
329
  return created;
400
330
  }
401
331
  render(width) {
402
- if (!this.#customViewport) {
403
- this.#viewportController.setEditorPointerFrame(undefined);
404
- return [...this.#renderDocument(width), ...this.#renderDock(width)];
405
- }
406
- const height = Math.max(0, this.#componentRuntime.getRows());
407
- const dock = this.#renderDockLayout(width);
408
- const copyAcknowledgement = this.#renderCopyAcknowledgement(width);
409
- // Invariant: auto/always own one stable final-column gutter. Every scrollable
410
- // surface ends before it; the dock remains full-width and hidden mode returns it.
411
- const documentWidth = this.#viewportController.config.scrollbarAppearance === "hidden"
412
- ? width
413
- : Math.max(1, width - 1);
414
- const document = this.#renderDocumentLayout(documentWidth);
415
- const steeringRows = this.#renderQueued(documentWidth);
416
- const releaseBanner = this.#renderReleaseBanner(documentWidth);
417
- // Rationale: the release notice rides the bottom-aligned tail directly above Working, so it
418
- // never displaces transcript content and scrolls away with the live status.
419
- // Invariant: when idle the banner's last row sits on the line Working would occupy.
420
- const statusRows = [...releaseBanner?.rows ?? [], ...this.#renderStatus(documentWidth)];
421
- const transientSignature = transientRowsSignature(steeringRows, statusRows);
422
- const snapshot = this.#visibleViewportSnapshot;
423
- // Invariant: paint-only feedback may cover either viewport or dock cells, so
424
- // ordinary dock-only reuse cannot safely omit its overlay composition.
425
- const dockInputCandidate = this.#dockInputCandidate && copyAcknowledgement === undefined;
426
- // Concurrency: input may change after a keyboard receipt but before its paint.
427
- // Capture only the live inputs this composition will actually represent.
428
- const inputSurface = this.#inputSurface;
429
- const dockRows = dock.rows;
430
- const selectableDocumentRowCount = document.rows.length;
431
- const dockStartRow = height - dockRows.length + 1;
432
- const editorOffset = dock.editorOffset;
433
- this.#viewportController.setInputSurfaceFrame(this.usesDefaultInputSurface() ? undefined : {
434
- component: this.#inputSurface,
435
- columnStart: 1,
436
- columnEnd: width,
437
- rowStart: Math.max(1, dockStartRow + editorOffset),
438
- rowEnd: Math.min(height, dockStartRow + editorOffset + dock.inputRows - 1),
439
- });
440
- this.#viewportController.setEditorPointerFrame(this.usesDefaultInputSurface()
441
- ? {
442
- rowStart: dockStartRow + editorOffset,
443
- rowEnd: dockStartRow + editorOffset + dock.inputRows - 1,
444
- }
445
- : undefined);
446
- const viewportRevision = this.#viewportController.presentationRevision;
447
- const selectionRevision = this.#viewportController.selectionRevision;
448
- const pointerPosition = this.#viewportController.pointerPosition;
449
- this.#dockInputCandidate = false;
450
- let frame = dockInputCandidate
451
- && snapshot !== undefined
452
- && snapshot.width === width
453
- && snapshot.height === height
454
- && snapshot.documentRows === document.rows
455
- && snapshot.transientSignature === transientSignature
456
- && snapshot.promptAnchors === document.promptAnchors
457
- && snapshot.dockLength === dockRows.length
458
- && snapshot.inputSurface === inputSurface
459
- && snapshot.viewportRevision === viewportRevision
460
- && snapshot.selectionRevision === selectionRevision
461
- ? this.#viewportController.composeDockOnly(dockRows, width, height)
462
- : null;
463
- if (frame === null) {
464
- frame = this.#viewportController.compose({
465
- // Performance: do not copy the complete document on unchanged dock-only input.
466
- // Steering and Working retain their one non-persistent viewport-tail ownership.
467
- documentRows: [...document.rows, ...steeringRows, ...statusRows],
468
- ...(this.#viewportController.transcriptPointerSelecting
469
- ? { paintDocumentRow: heldNativeHyperlinkStyle }
470
- : {}),
471
- // Invariant: this boundary tracks persistent transcript rows for streaming and
472
- // geometry; transient Steering and Working rows remain complete-frame selectable.
473
- selectableDocumentRowCount,
474
- ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
475
- bottomAlignedTailRowCount: steeringRows.length + statusRows.length,
476
- dockRows,
477
- promptAnchors: document.promptAnchors,
478
- width,
479
- height,
480
- // Invariant: the control belongs immediately above the complete dock and
481
- // floats over transient viewport content rather than consuming a dock row.
482
- bottomControlRow: Math.max(0, height - Math.min(height, dockRows.length) - 1),
483
- ...(copyAcknowledgement === undefined || height <= 0 ? {} : {
484
- frameOverlay: {
485
- row: Math.max(1, Math.min(height, dockStartRow + dock.inputOffset - 1)),
486
- text: copyAcknowledgement,
487
- },
488
- }),
489
- theme: this.#viewportTheme,
490
- });
491
- this.#fullViewportCompositions += 1;
492
- }
493
- else
494
- this.#dockOnlyViewportCompositions += 1;
495
- this.#locateReleaseClose(frame, releaseBanner, document.rows.length + steeringRows.length + statusRows.length, statusRows.length);
496
- this.#visibleViewportSnapshot = {
497
- width,
498
- height,
499
- documentRows: document.rows,
500
- transientSignature,
501
- promptAnchors: document.promptAnchors,
502
- dockLength: dockRows.length,
503
- inputSurface,
504
- // Invariant: a callback during composition cannot bless older rows with its
505
- // newer revision. Its ordinary pending render will publish the newer state.
506
- viewportRevision,
507
- selectionRevision: frame.descriptor.selectionRevision,
508
- pointerPosition,
509
- };
510
- this.#onViewportFrame?.(frame);
511
- return frame.rows;
332
+ return this.#layout.render(width);
512
333
  }
513
334
  viewportFrameDescriptor() {
514
335
  return this.#viewportController.frame?.descriptor ?? null;
@@ -516,10 +337,10 @@ export class OwnedUiSessionShellRoot {
516
337
  /** Read-only, payload-free evidence; observing a pending frame must never render or repair it. */
517
338
  viewportPresentationEvidence() {
518
339
  const frame = this.#viewportController.frame;
519
- const snapshot = this.#visibleViewportSnapshot;
340
+ const snapshot = this.#layout.composed();
520
341
  return {
521
- candidate: this.#dockInputCandidate,
522
- candidateRevision: this.#dockInputCandidate ? this.#dockInputRevision ?? null : null,
342
+ candidate: this.#input.dockInputCandidate,
343
+ candidateRevision: this.#input.dockInputCandidate ? this.#input.dockInputRevision ?? null : null,
523
344
  currentRevision: this.#viewportController.presentationRevision,
524
345
  composedRevision: snapshot?.viewportRevision ?? null,
525
346
  currentPointer: this.#viewportController.pointerPosition ?? null,
@@ -540,27 +361,16 @@ export class OwnedUiSessionShellRoot {
540
361
  return this.#viewportController.hasSelection || this.editor.hasSelection();
541
362
  }
542
363
  setViewportConfig(config) {
543
- if (this.#viewportController.setConfig(config))
544
- this.#documentLayouts.clear();
364
+ if (this.#viewportController.setConfig(config)) {
365
+ this.#document.invalidate("viewport-config");
366
+ this.#layout.invalidate();
367
+ }
545
368
  }
546
369
  setFullscreenCopyOnSelect(enabled) {
547
370
  this.#viewportController.setCopyOnSelect(enabled);
548
371
  }
549
372
  showCopyAcknowledgement(message) {
550
- this.#clearCopyAcknowledgement(false);
551
- this.#copyAcknowledgement = message;
552
- const timer = setTimeout(() => {
553
- if (this.#copyAcknowledgementTimer !== timer)
554
- return;
555
- this.#copyAcknowledgementTimer = undefined;
556
- this.#copyAcknowledgement = undefined;
557
- this.#invalidateChrome();
558
- this.#componentRuntime.requestRender();
559
- }, COPY_ACKNOWLEDGEMENT_DURATION_MS);
560
- timer.unref?.();
561
- this.#copyAcknowledgementTimer = timer;
562
- this.#invalidateChrome();
563
- this.#componentRuntime.requestRender();
373
+ this.#dockChrome.showCopyAcknowledgement(message);
564
374
  }
565
375
  resumeViewportFollowing() {
566
376
  this.#viewportController.resumeFollowing();
@@ -570,13 +380,7 @@ export class OwnedUiSessionShellRoot {
570
380
  }
571
381
  /** A new session binding owns new mounts even when it reuses semantic invocation ids. */
572
382
  resetTranscript() {
573
- for (const component of this.#transcript.values())
574
- component.dispose?.();
575
- this.#transcript.clear();
576
- this.#blocksById.clear();
577
- this.#transcriptOrder = [];
578
- this.#renderedRows.clear();
579
- this.#documentLayouts.clear();
383
+ this.#document.reset();
580
384
  }
581
385
  resetViewport() {
582
386
  this.#viewportController.reset();
@@ -594,18 +398,17 @@ export class OwnedUiSessionShellRoot {
594
398
  handleViewportPreInput(data, allowWheel = true, now = Date.now(), editorActive = this.usesDefaultInputSurface()) {
595
399
  return this.#viewportController.handlePreInput(data, allowWheel, now, editorActive);
596
400
  }
597
- #renderDock(width) {
598
- return this.#renderDockLayout(width).rows;
599
- }
600
401
  #renderDockLayout(width) {
601
402
  const queued = this.#customViewport ? [] : this.#renderQueued(width);
602
403
  const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
603
- const transientRows = [...queued, ...statusRows, ...this.#renderDockNotice(width)];
604
- const aboveWidgets = this.#renderWidgets("aboveEditor", width);
605
- const input = this.#inputSurface.render(width);
606
- // Invariant: pointer rows describe the body, not the autocomplete block now preceding it.
404
+ const noticeRows = this.#dockChrome.renderNotice(width, this.#outputPad);
405
+ const transientRows = [...queued, ...statusRows, ...noticeRows];
406
+ const messageStart = queued.length;
407
+ const copyableMessageRows = Array.from({ length: statusRows.length + noticeRows.length }, (_value, index) => ({ row: messageStart + index }));
408
+ const aboveWidgets = this.#extensions.renderWidgets("aboveEditor", width);
409
+ const input = this.#input.surface.render(width);
607
410
  const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
608
- const belowWidgets = this.#renderWidgets("belowEditor", width);
411
+ const belowWidgets = this.#extensions.renderWidgets("belowEditor", width);
609
412
  const footer = this.#renderFooter(width);
610
413
  const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
611
414
  const inputOffset = transientRows.length + aboveWidgets.length;
@@ -614,68 +417,12 @@ export class OwnedUiSessionShellRoot {
614
417
  inputOffset,
615
418
  editorOffset: inputOffset + (body?.rowOffset ?? 0),
616
419
  inputRows: body?.rowCount ?? input.length,
420
+ footerRows: footer.length,
421
+ copyableMessageRows,
617
422
  };
618
423
  }
619
- #renderCopyAcknowledgement(width) {
620
- if (this.#copyAcknowledgement === undefined || width <= 0)
621
- return undefined;
622
- return piTheme().fg("accent", piShellTruncateToWidth(this.#copyAcknowledgement, width));
623
- }
624
- #clearCopyAcknowledgement(requestRender) {
625
- if (this.#copyAcknowledgementTimer !== undefined)
626
- clearTimeout(this.#copyAcknowledgementTimer);
627
- this.#copyAcknowledgementTimer = undefined;
628
- if (this.#copyAcknowledgement === undefined)
629
- return;
630
- this.#copyAcknowledgement = undefined;
631
- this.#invalidateChrome();
632
- if (requestRender)
633
- this.#componentRuntime.requestRender();
634
- }
635
- #renderReleaseBanner(width) {
636
- if (this.#releaseNoticeDismissed)
637
- return undefined;
638
- const diagnostic = this.#view.diagnostics.findLast(item => item.code === "release-update");
639
- const release = diagnostic === undefined ? null : parseReleaseUpdateDiagnostic(diagnostic.message);
640
- if (release === null)
641
- return undefined;
642
- return renderPiShellReleaseUpdateBanner(release, width, this.#viewportController.tailControlHovered(this.#releaseCloseTarget));
643
- }
644
- #locateReleaseClose(frame, banner, tailEnd, tailLength) {
645
- // Invariant: the tail is the document suffix, so the control's document row is fixed relative to its
646
- // end; alignment gap rows precede it and the viewport scroll decides whether it is on screen.
647
- const bannerStart = tailEnd + frame.descriptor.transientAlignmentGapRows - tailLength - frame.scrollTop + 1;
648
- const rowStart = banner === undefined ? 0 : Math.max(1, bannerStart + banner.close.rowStart);
649
- const rowEnd = banner === undefined ? 0 : Math.min(frame.hits.viewportHeight, bannerStart + banner.close.rowEnd);
650
- this.#releaseCloseTarget = banner !== undefined && rowStart <= rowEnd
651
- ? { rowStart, rowEnd, columnStart: banner.close.columnStart, columnEnd: banner.close.columnEnd }
652
- : undefined;
653
- this.#viewportController.setTailControl(this.#releaseCloseTarget === undefined ? undefined : {
654
- ...this.#releaseCloseTarget,
655
- activate: () => {
656
- this.#releaseNoticeDismissed = true;
657
- this.#releaseCloseTarget = undefined;
658
- },
659
- });
660
- }
661
- #renderDockNotice(width) {
662
- const notice = this.#dockNotice;
663
- if (notice === undefined)
664
- return [];
665
- if (notice.kind === "status") {
666
- // Compatibility: Pi pads its status text by one cell regardless of the output pad setting.
667
- return ["", ...renderPiShellStatusText(notice.message, width, PINNED_PI_LAYOUT.outputPad)];
668
- }
669
- return renderPiShellCommandMessage({
670
- kind: notice.kind === "new-session" ? "accent" : notice.kind,
671
- message: notice.message,
672
- }, width, this.#outputPad);
673
- }
674
424
  dismissNotice() {
675
- if (this.#dockNotice === undefined)
676
- return;
677
- this.#dockNotice = undefined;
678
- this.#invalidateChrome();
425
+ this.#dockChrome.dismissNotice();
679
426
  }
680
427
  #renderQueued(width) {
681
428
  return this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
@@ -685,284 +432,30 @@ export class OwnedUiSessionShellRoot {
685
432
  return {
686
433
  document: layoutPort(width => this.#renderDocument(width), () => this.invalidate()),
687
434
  queued: layoutPort(width => this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width), () => this.#queued.invalidate()),
688
- aboveWidgets: layoutPort(width => this.#renderWidgets("aboveEditor", width), () => this.#invalidateExtensions()),
435
+ aboveWidgets: layoutPort(width => this.#extensions.renderWidgets("aboveEditor", width), () => this.#extensions.invalidate()),
689
436
  status: layoutPort(width => this.#renderStatus(width), () => this.#status.invalidate()),
690
- editor: layoutPort(width => this.#inputSurface.render(width), () => this.#inputSurface.invalidate(), data => this.#inputSurface.handleInput?.(data)),
691
- belowWidgets: layoutPort(width => this.#renderWidgets("belowEditor", width), () => this.#invalidateExtensions()),
692
- footer: layoutPort(width => this.#renderFooter(width), () => (this.#extensionFooter ?? this.#footer).invalidate()),
437
+ editor: layoutPort(width => this.#input.surface.render(width), () => this.#input.surface.invalidate(), data => this.#input.surface.handleInput?.(data)),
438
+ belowWidgets: layoutPort(width => this.#extensions.renderWidgets("belowEditor", width), () => this.#extensions.invalidate()),
439
+ footer: layoutPort(width => this.#renderFooter(width), () => (this.#extensions.footer ?? this.#footer).invalidate()),
693
440
  };
694
441
  }
695
442
  #renderDocument(width) {
696
- return this.#renderDocumentLayout(width).rows;
697
- }
698
- #renderDocumentLayout(width) {
699
- // Performance: extension headers may animate independently of transcript revisions, so
700
- // only the stable built-in document participates in this frame cache.
701
- const cached = this.#extensionHeader === null ? this.#documentLayouts.get(width) : undefined;
702
- if (cached !== undefined)
703
- return cached;
704
- const diagnostics = this.#view.diagnostics;
705
- const startupRows = diagnostics
706
- // Compatibility: the pinned route keeps trust failures with Pi-style startup diagnostics;
707
- // bare A1 presents them in its prompt-adjacent notice dock instead.
708
- .filter(diagnostic => diagnostic.code === "engine-startup"
709
- || !this.#customViewport && diagnostic.code === "project-trust")
710
- .flatMap(diagnostic => renderPiShellStartupDiagnostic(diagnostic, width));
711
- const resourceRows = this.#customViewport ? [] : [...this.resources.render(width)];
712
- if (resourceRows.at(-1) === "")
713
- resourceRows.pop();
714
- const headerRows = this.#extensionHeader !== null
715
- ? this.#extensionHeader.render(width)
716
- : this.#customViewport ? [] : this.header.render(width);
717
- const rows = [
718
- ...startupRows,
719
- ...headerRows,
720
- ...resourceRows,
721
- ];
722
- const promptAnchors = [];
723
- // Rationale: a block that is still streaming re-renders every row it owns, so the frame
724
- // reports where its rows begin. Damage-aware painting uses that to tell a live block's
725
- // own reflow apart from a repaint that reaches settled rows.
726
- let liveTailStartRow;
727
- for (let index = 0; index < this.#transcriptOrder.length; index += 1) {
728
- const id = this.#transcriptOrder[index];
729
- const block = this.#blocksById.get(id);
730
- if (!this.#thinkingVisible && block?.kind === "thinking")
731
- continue;
732
- const promptLike = block !== undefined
733
- && (block.kind === "user" || this.#customViewport && isPiPromptStyleCompaction(block));
734
- // Invariant: the custom viewport has already removed its rail gutter from this width.
735
- // Prompt-like and ordinary blocks therefore share one right content edge.
736
- const blockWidth = width;
737
- const blockRows = this.#blockRows(id, block, blockWidth);
738
- if (promptLike) {
739
- // Compatibility: the first natural prompt gets one breathing row at the document top.
740
- // Once scrolling advances, the prompt itself reaches row zero and then
741
- // becomes sticky there, so the spacer is never pinned with it.
742
- if (this.#customViewport && index === 0)
743
- rows.push("");
744
- else if (index > 0)
745
- rows.push("");
746
- }
747
- const firstRow = rows.length;
748
- if (liveTailStartRow === undefined && block !== undefined && block.status !== "finalized") {
749
- liveTailStartRow = firstRow;
750
- }
751
- rows.push(...blockRows);
752
- if (promptLike && blockRows[0] !== undefined) {
753
- promptAnchors.push({
754
- id: block.id,
755
- firstRow,
756
- lastRow: Math.max(firstRow, rows.length - 1),
757
- sourceRow: pinnedPromptSourceRow(block, blockRows[0], blockWidth),
758
- prominentSourceRow: blockRows[0],
759
- });
760
- }
761
- }
762
- // Invariant: the A1 release notice precedes the extension-package notice, matching pinned Pi's order.
763
- // Bare A1 docks it in the viewport tail instead of the document.
764
- rows.push(...diagnostics
765
- .filter(diagnostic => !this.#customViewport && diagnostic.code === "release-update")
766
- .flatMap(diagnostic => {
767
- const release = parseReleaseUpdateDiagnostic(diagnostic.message);
768
- return release === null ? [] : renderPiShellReleaseUpdateNotice(release, width);
769
- }));
770
- rows.push(...diagnostics
771
- .filter(diagnostic => diagnostic.code === "package-updates")
772
- .flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
773
- rows.push(...diagnostics
774
- .filter(diagnostic => diagnostic.code === "changelog-collapsed" || diagnostic.code === "changelog-expanded")
775
- // Rationale: bare A1 translates either startup diagnostic into transient dock chrome;
776
- // the pinned layout retains Pi's collapsed hint or expanded document.
777
- .flatMap(diagnostic => this.#customViewport
778
- ? []
779
- : diagnostic.code === "changelog-collapsed"
780
- ? createPiShellCollapsedChangelog().render(width)
781
- : createPiShellChangelog(diagnostic.message).render(width)));
782
- rows.push(...diagnostics
783
- .filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "project-trust"
784
- && diagnostic.code !== "release-update"
785
- && diagnostic.code !== "package-updates" && diagnostic.code !== "changelog-collapsed"
786
- && diagnostic.code !== "changelog-expanded")
787
- .slice(-3)
788
- .flatMap(diagnostic => renderPiShellTranscriptBlock({
789
- id: `diagnostic-${diagnostic.sequence}`,
790
- kind: diagnostic.severity === "error" ? "error" : "system",
791
- status: "finalized",
792
- revision: diagnostic.sequence,
793
- title: diagnostic.code,
794
- text: diagnostic.message,
795
- payload: {},
796
- }, width, this.#cwd)));
797
- const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors), liveTailStartRow };
798
- if (this.#extensionHeader === null) {
799
- this.#documentLayouts.set(width, layout);
800
- // Performance: the custom viewport commonly probes full width and reserved-rail width.
801
- while (this.#documentLayouts.size > 2)
802
- this.#documentLayouts.delete(this.#documentLayouts.keys().next().value);
803
- }
804
- return layout;
805
- }
806
- // Performance: finalized blocks cache by revision and width; live blocks always render.
807
- #blockRows(id, block, width) {
808
- const component = this.#transcript.get(id);
809
- if (component === undefined)
810
- return [];
811
- const render = () => {
812
- this.#transcriptBlockRenders += 1;
813
- const rows = component.render(width);
814
- if (!this.#customViewport || block?.kind === "user")
815
- return rows;
816
- return rows.map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor));
817
- };
818
- if (block === undefined || block.status !== "finalized")
819
- return render();
820
- let byWidth = this.#renderedRows.get(id);
821
- const cached = byWidth?.get(width);
822
- const presentationRevision = component.presentationRevision ?? 0;
823
- if (cached?.revision === block.revision && cached.presentationRevision === presentationRevision)
824
- return cached.rows;
825
- const rows = render();
826
- if (byWidth === undefined) {
827
- byWidth = new Map();
828
- this.#renderedRows.set(id, byWidth);
829
- }
830
- byWidth.set(width, { revision: block.revision, presentationRevision, rows });
831
- // Invariant: bare A1 probes full width before reserving the overflowing rail column;
832
- // retain both widths without turning resize history into an unbounded cache.
833
- while (byWidth.size > 2)
834
- byWidth.delete(byWidth.keys().next().value);
835
- return rows;
443
+ return this.#document.layout(width).rows;
836
444
  }
837
445
  transcriptComponent(id) {
838
- return this.#transcript.get(id);
446
+ return this.#document.component(id);
839
447
  }
840
448
  exitTranscript(width) {
841
- const rows = [];
842
- for (const id of this.#transcriptOrder) {
843
- const block = this.#blocksById.get(id);
844
- if (block === undefined || (!this.#thinkingVisible && block.kind === "thinking"))
845
- continue;
846
- if (rows.length > 0 && (block.kind === "user" || this.#customViewport && isPiPromptStyleCompaction(block)))
847
- rows.push("");
848
- rows.push(...this.#blockRows(id, block, width).map(sanitizeExitTranscriptRow));
849
- }
850
- while (rows.at(-1) === "")
851
- rows.pop();
852
- return rows.join("\n");
449
+ return this.#document.exitTranscript(width);
853
450
  }
854
451
  appendWorkflowStatus(message) {
855
- // Rationale: bare A1 acknowledges commands in one dock notice above the editor, where the
856
- // reader's eye already is, instead of a transcript row that opens an empty session at the
857
- // top-left or sinks into a long feed. The pinned route keeps Pi's chat placement.
858
- if (this.#customViewport) {
859
- this.#dockNotice = { kind: "status", message };
860
- this.#invalidateChrome();
861
- return;
862
- }
863
- const previousId = this.#lastWorkflowStatusId;
864
- if (previousId !== undefined
865
- && this.#transcriptOrder.at(-1) === previousId
866
- && this.#workflowStatusAnchors.get(previousId) === this.#view.transcript.length) {
867
- this.#workflowStatusMessages.set(previousId, message);
868
- this.#documentLayouts.clear();
869
- this.invalidate();
870
- return;
871
- }
872
- const id = this.#appendAnchoredWorkflowComponent(width => [
873
- "",
874
- ...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width),
875
- ]);
876
- this.#workflowStatusMessages.set(id, message);
877
- this.#lastWorkflowStatusId = id;
452
+ this.#workflowOutput.appendStatus(message);
878
453
  }
879
454
  appendWorkflowMessage(message, errorCode) {
880
- if (message.kind === "status") {
881
- this.appendWorkflowStatus(message.message);
882
- return;
883
- }
884
- if (this.#customViewport && (message.kind === "warning" || message.kind === "error")) {
885
- this.#dockNotice = {
886
- kind: errorCode === "image-count" ? "warning" : message.kind,
887
- message: message.message,
888
- ...(errorCode === undefined ? {} : { errorCode }),
889
- };
890
- this.#invalidateChrome();
891
- return;
892
- }
893
- this.#lastWorkflowStatusId = undefined;
894
- const presentation = { kind: message.kind, message: message.message };
895
- this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
455
+ this.#workflowOutput.appendMessage(message, errorCode);
896
456
  }
897
457
  appendWorkflowResult(result, errorCode) {
898
- if (result.messages !== undefined) {
899
- for (const message of result.messages)
900
- this.appendWorkflowMessage(message, errorCode);
901
- return;
902
- }
903
- if (result.messageKind === "silent" || (result.outcome === "cancelled" && result.messageKind === undefined))
904
- return;
905
- if (result.command === "reload" && result.outcome === "completed") {
906
- this.appendWorkflowStatus(result.message);
907
- return;
908
- }
909
- if (result.command === "session" && result.outcome === "completed" && result.presentation?.kind === "session-info") {
910
- this.#lastWorkflowStatusId = undefined;
911
- const sessionInfo = createPiShellSessionInfo(result.presentation);
912
- this.#appendAnchoredWorkflowComponent(width => sessionInfo.render(width), () => sessionInfo.dispose?.());
913
- return;
914
- }
915
- if (result.command === "hotkeys" && result.outcome === "completed") {
916
- this.#lastWorkflowStatusId = undefined;
917
- const presentation = this.hotkeysPresentation();
918
- const hotkeys = createPiShellHotkeys(presentation.bindings, presentation.getShortcuts, presentation.profile);
919
- this.#appendAnchoredWorkflowComponent(width => hotkeys.render(width), () => hotkeys.dispose?.());
920
- return;
921
- }
922
- if (result.command === "changelog" && result.outcome === "completed") {
923
- this.#lastWorkflowStatusId = undefined;
924
- const changelog = createPiShellChangelog(result.detail ?? "No changelog entries found.");
925
- this.#appendAnchoredWorkflowComponent(width => changelog.render(width), () => changelog.dispose?.());
926
- return;
927
- }
928
- if (result.outcome === "failed") {
929
- this.appendWorkflowMessage({
930
- kind: result.messageKind === "warning" ? "warning" : "error",
931
- message: result.message,
932
- }, errorCode);
933
- return;
934
- }
935
- if (result.outcome === "completed" && (result.command === "quit" || result.command === "compact"))
936
- return;
937
- if (result.outcome === "completed" && result.command === "new" && this.#customViewport) {
938
- // Rationale: bare A1 keeps the replacement session visually empty while preserving Pi's accent notice shape.
939
- this.#dockNotice = { kind: "new-session", message: result.message };
940
- this.#invalidateChrome();
941
- return;
942
- }
943
- if (result.outcome === "completed" && (result.command === "new" || result.command === "name" || result.command === "debug")) {
944
- this.#lastWorkflowStatusId = undefined;
945
- const presentation = { kind: result.command, message: result.message, ...(result.detail === undefined ? {} : { detail: result.detail }) };
946
- this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
947
- return;
948
- }
949
- if (result.command === "arminsayshi" && result.outcome === "completed") {
950
- this.#lastWorkflowStatusId = undefined;
951
- const armin = createPiShellArmin(this.#componentRuntime);
952
- this.#appendAnchoredWorkflowComponent(width => ["", ...armin.render(width)], () => armin.dispose?.());
953
- return;
954
- }
955
- if (result.command === "dementedelves" && result.outcome === "completed") {
956
- this.#lastWorkflowStatusId = undefined;
957
- const announcement = createPiShellEarendilAnnouncement();
958
- this.#appendAnchoredWorkflowComponent(width => ["", ...announcement.render(width)], () => announcement.dispose?.());
959
- return;
960
- }
961
- // Rationale: the pinned engine links both share URLs; the workflow result carries the plain URLs.
962
- const message = result.command === "share" && result.detail
963
- ? `${linkShareUrl(result.message)}\nGist: ${piShellHyperlink(result.detail)}`
964
- : result.message;
965
- this.appendWorkflowStatus(message);
458
+ this.#workflowOutput.appendResult(result, errorCode);
966
459
  }
967
460
  toggleThinkingVisibility() {
968
461
  this.setHideThinkingBlock(this.#thinkingVisible);
@@ -974,39 +467,25 @@ export class OwnedUiSessionShellRoot {
974
467
  this.#setToolsExpanded(expanded);
975
468
  }
976
469
  setExtensionWidget(key, component, placement) {
977
- const previous = this.#extensionWidgets.get(key)?.component;
978
- if (component === null)
979
- this.#extensionWidgets.delete(key);
980
- else
981
- this.#extensionWidgets.set(key, { component, placement });
982
- if (previous !== undefined && previous !== component)
983
- previous.dispose?.();
470
+ this.#extensions.placeWidget(key, component, placement);
984
471
  this.invalidate();
985
472
  }
986
473
  setExtensionHeader(component) {
987
- if (this.#extensionHeader !== component)
988
- this.#extensionHeader?.dispose?.();
989
- this.#extensionHeader = component;
990
- this.#documentLayouts.clear();
474
+ this.#extensions.replaceHeader(component);
475
+ this.#document.invalidate("extension-header");
991
476
  this.invalidate();
992
477
  }
993
478
  setExtensionFooter(component) {
994
- if (this.#extensionFooter !== component)
995
- this.#extensionFooter?.dispose?.();
996
- this.#extensionFooter = component;
479
+ this.#extensions.replaceFooter(component);
997
480
  this.invalidate();
998
481
  }
999
482
  setExtensionStatus(key, text) {
1000
- if (text === undefined)
1001
- this.#extensionStatuses.delete(key);
1002
- else
1003
- this.#extensionStatuses.set(key, text);
1004
- this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
483
+ this.#extensions.setStatus(key, text);
484
+ this.#footer.update(this.#extensions.withStatuses(this.#view));
1005
485
  this.invalidate();
1006
486
  }
1007
- setExtensionWorking(message, visible = this.#extensionWorkingVisible) {
1008
- this.#extensionWorkingMessage = message;
1009
- this.#extensionWorkingVisible = visible;
487
+ setExtensionWorking(message, visible = this.#extensions.working.visible) {
488
+ this.#extensions.setWorking(message, visible);
1010
489
  this.#syncWorkingOverride();
1011
490
  this.invalidate();
1012
491
  }
@@ -1028,7 +507,7 @@ export class OwnedUiSessionShellRoot {
1028
507
  const sending = this.#imageSubmissionsSending > 0;
1029
508
  this.#status.setWorkingOverride(sending
1030
509
  ? "Sending…"
1031
- : this.#extensionWorkingVisible ? this.#extensionWorkingMessage : undefined, sending);
510
+ : this.#extensions.working.visible ? this.#extensions.working.message : undefined, sending);
1032
511
  }
1033
512
  hotkeysPresentation() {
1034
513
  return {
@@ -1038,34 +517,17 @@ export class OwnedUiSessionShellRoot {
1038
517
  };
1039
518
  }
1040
519
  resetWorkflowPresentation() {
1041
- this.#dockNotice = undefined;
1042
- this.#clearCopyAcknowledgement(false);
1043
- for (const id of this.#workflowStatusAnchors.keys()) {
1044
- this.#transcript.get(id)?.dispose?.();
1045
- this.#transcript.delete(id);
1046
- }
1047
- this.#workflowStatusAnchors.clear();
1048
- this.#workflowStatusMessages.clear();
1049
- this.#transcriptOrder = this.#transcriptOrder.filter(id => !id.startsWith("workflow-status-"));
1050
- this.#lastWorkflowStatusId = undefined;
1051
- this.#documentLayouts.clear();
520
+ this.#dockChrome.forgetNotice();
521
+ this.#dockChrome.clearCopyAcknowledgement(false);
522
+ this.#workflowOutput.reset();
1052
523
  this.invalidate();
1053
524
  }
1054
525
  resetExtensionUi() {
1055
- this.#extensionHeader?.dispose?.();
1056
- this.#extensionFooter?.dispose?.();
1057
- for (const { component } of this.#extensionWidgets.values())
1058
- component.dispose?.();
1059
- this.#extensionHeader = null;
1060
- this.#extensionFooter = null;
1061
- this.#extensionWidgets.clear();
1062
- this.#extensionStatuses.clear();
1063
- this.#extensionWorkingMessage = undefined;
526
+ this.#extensions.reset();
1064
527
  this.#syncWorkingOverride();
1065
- this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
528
+ this.#footer.update(this.#extensions.withStatuses(this.#view));
1066
529
  // Invariant: an extension renderer may have drawn transcript blocks that are now unrendered by it.
1067
- this.#renderedRows.clear();
1068
- this.#documentLayouts.clear();
530
+ this.#document.invalidate("extension-renderers");
1069
531
  this.invalidate();
1070
532
  }
1071
533
  addExtensionNotification(message, type) {
@@ -1078,263 +540,97 @@ export class OwnedUiSessionShellRoot {
1078
540
  extensionFooterData() {
1079
541
  return {
1080
542
  getGitBranch: () => this.#view.status.footer?.branch ?? null,
1081
- getExtensionStatuses: () => new Map(this.#extensionStatuses),
543
+ getExtensionStatuses: () => this.#extensions.statuses(),
1082
544
  getAvailableProviderCount: () => this.#view.status.footer?.availableProviderCount ?? 1,
1083
545
  onBranchChange: () => () => { },
1084
546
  };
1085
547
  }
1086
548
  usesDefaultInputSurface() {
1087
- return this.#inputSurface === this.editor;
549
+ return this.#input.usesEditor;
1088
550
  }
1089
551
  inputCoordinationSurface() {
1090
- return this.#inputSurfaceCoordination;
552
+ return this.#input.coordination;
1091
553
  }
1092
554
  viewportCompositionEvidence() {
1093
- return { full: this.#fullViewportCompositions, dockOnly: this.#dockOnlyViewportCompositions };
555
+ return this.#layout.compositions();
1094
556
  }
1095
557
  transcriptRenderCount() {
1096
- return this.#transcriptBlockRenders;
558
+ return this.#document.renderCount;
1097
559
  }
1098
560
  setFooterLevel(visible) { this.#footerLevel = visible; }
1099
561
  setInputSurface(component, disposePrevious = true, coordination = "owned") {
1100
- const next = component ?? this.editor;
1101
- const nextCoordination = next === this.editor ? "editor" : coordination;
1102
- if (next === this.#inputSurface) {
1103
- this.#inputSurfaceCoordination = nextCoordination;
1104
- return;
1105
- }
1106
- this.#dockInputCandidate = false;
1107
- this.#viewportController.invalidateInputSurface();
1108
- this.#inputSurface.setFocused?.(false);
1109
- if (disposePrevious && this.#inputSurface !== this.editor)
1110
- this.#inputSurface.dispose?.();
1111
- this.#inputSurface = next;
1112
- this.#inputSurfaceCoordination = nextCoordination;
1113
- this.#onInputSurfaceChanged?.();
1114
- this.#inputSurface.setFocused?.(true);
1115
- this.invalidate();
562
+ if (this.#input.set(component, disposePrevious, coordination))
563
+ this.invalidate();
1116
564
  }
1117
565
  handleInput(data) {
1118
- this.#dockInputCandidate = this.#customViewport && this.#dockInputReuseEnabled
1119
- && classifyPiTuiInput(data, this.#inputSurfaceCoordination) === "safe";
1120
- this.#dockInputRevision = this.#dockInputCandidate ? this.#viewportController.presentationRevision : undefined;
1121
- try {
1122
- this.#inputSurface.handleInput?.(data);
1123
- }
1124
- catch (error) {
1125
- this.#dockInputCandidate = false;
1126
- throw error;
1127
- }
566
+ this.#input.handleInput(data);
1128
567
  }
1129
568
  invalidate() {
1130
- this.#renderedRows.clear();
1131
- this.#documentLayouts.clear();
1132
- for (const component of this.#transcript.values())
1133
- component.invalidate();
569
+ this.#document.invalidate("repaint");
1134
570
  this.#invalidateChrome();
1135
571
  }
1136
572
  #invalidateChrome() {
1137
573
  this.header.invalidate();
1138
574
  this.resources.invalidate();
1139
575
  this.editor.invalidate();
1140
- if (this.#inputSurface !== this.editor)
1141
- this.#inputSurface.invalidate();
1142
- this.#invalidateExtensions();
576
+ if (!this.#input.usesEditor)
577
+ this.#input.surface.invalidate();
578
+ this.#extensions.invalidate();
1143
579
  this.#status.invalidate();
1144
580
  this.#footer.invalidate();
1145
581
  this.#queued.invalidate();
1146
582
  }
1147
583
  setFocused(focused) {
1148
- this.#inputSurface.setFocused?.(focused);
584
+ this.#input.surface.setFocused?.(focused);
1149
585
  if (focused)
1150
586
  this.#onPromptSuggestionEligibilityChanged?.();
1151
587
  }
1152
588
  dispose() {
1153
- this.#clearCopyAcknowledgement(false);
589
+ this.#dockChrome.clearCopyAcknowledgement(false);
1154
590
  this.clearViewportPointerState();
1155
591
  this.#themeUnsubscribe();
1156
592
  this.header.dispose?.();
1157
593
  this.resources.dispose?.();
1158
- for (const component of this.#transcript.values())
1159
- component.dispose?.();
1160
- this.#transcript.clear();
1161
- this.#renderedRows.clear();
1162
- this.#documentLayouts.clear();
1163
- this.#visibleViewportSnapshot = undefined;
1164
- this.#dockInputCandidate = false;
1165
- this.#dockInputRevision = undefined;
1166
- if (this.#inputSurface !== this.editor)
1167
- this.#inputSurface.dispose?.();
1168
- this.#extensionHeader?.dispose?.();
1169
- this.#extensionFooter?.dispose?.();
1170
- for (const { component } of this.#extensionWidgets.values())
1171
- component.dispose?.();
1172
- this.#extensionWidgets.clear();
594
+ this.#document.dispose();
595
+ this.#layout.invalidate();
596
+ this.#input.dispose();
597
+ this.#extensions.dispose();
1173
598
  this.editor.dispose?.();
1174
599
  this.#status.dispose?.();
1175
600
  this.#footer.dispose?.();
1176
601
  this.#queued.dispose?.();
1177
602
  }
1178
603
  #invalidateTranscriptPresentation() {
1179
- this.#renderedRows.clear();
1180
- this.#documentLayouts.clear();
1181
- this.invalidate();
1182
- }
1183
- #syncTranscript(blocks) {
1184
- const previousIds = this.#transcriptOrder.filter(id => !id.startsWith("workflow-status-"));
1185
- const transcriptChanged = previousIds.length !== blocks.length || blocks.some((block, index) => {
1186
- const previous = this.#blocksById.get(block.id);
1187
- return previousIds[index] !== block.id || previous?.revision !== block.revision;
1188
- });
1189
- if (transcriptChanged)
1190
- this.#documentLayouts.clear();
1191
- this.#blocksById.clear();
1192
- for (const block of blocks)
1193
- this.#blocksById.set(block.id, block);
1194
- const nextIds = new Set(blocks.map(block => block.id));
1195
- for (const [id, component] of this.#transcript) {
1196
- if (id.startsWith("workflow-status-") || nextIds.has(id))
1197
- continue;
1198
- component.dispose?.();
1199
- this.#transcript.delete(id);
1200
- this.#renderedRows.delete(id);
1201
- }
1202
- for (const block of blocks) {
1203
- const component = this.#transcript.get(block.id);
1204
- if (component === undefined) {
1205
- const created = this.#mountTranscript(block);
1206
- this.#transcript.set(block.id, created);
1207
- }
1208
- else if (component.revision !== block.revision) {
1209
- component.update(block);
1210
- }
1211
- }
1212
- const statusIds = [...this.#workflowStatusAnchors.keys()];
1213
- const order = [];
1214
- for (let index = 0; index <= blocks.length; index += 1) {
1215
- for (const statusId of statusIds) {
1216
- if (this.#workflowStatusAnchors.get(statusId) === index)
1217
- order.push(statusId);
1218
- }
1219
- const block = blocks[index];
1220
- if (block !== undefined)
1221
- order.push(block.id);
1222
- }
1223
- const placed = new Set(order);
1224
- for (const statusId of statusIds) {
1225
- if (!placed.has(statusId))
1226
- order.push(statusId);
1227
- }
1228
- this.#transcriptOrder = order;
1229
- }
1230
- #appendAnchoredWorkflowComponent(render, dispose) {
1231
- // Invariant: transcript-bound workflow output supersedes a pending acknowledgement.
1232
- this.dismissNotice();
1233
- this.#workflowTranscriptSequence += 1;
1234
- const id = `workflow-status-${this.#workflowTranscriptSequence}`;
1235
- const component = {
1236
- id,
1237
- revision: 1,
1238
- render,
1239
- handleInput() { },
1240
- invalidate() { },
1241
- update() { },
1242
- setExpanded() { },
1243
- setOutputPad() { },
1244
- setHideThinkingBlock() { },
1245
- setMermaidRenderingMode() { },
1246
- setImagePresentation() { },
1247
- ...(dispose === undefined ? {} : { dispose }),
1248
- };
1249
- this.#transcript.set(id, component);
1250
- this.#workflowStatusAnchors.set(id, this.#view.transcript.length);
1251
- this.#transcriptOrder.push(id);
1252
- this.#documentLayouts.clear();
604
+ this.#document.invalidate("block-presentation");
1253
605
  this.invalidate();
1254
- return id;
1255
- }
1256
- #renderWidgets(placement, width) {
1257
- const rows = [...this.#extensionWidgets.values()]
1258
- .filter(widget => widget.placement === placement)
1259
- .flatMap(widget => widget.component.render(width));
1260
- return placement === "aboveEditor" ? ["", ...rows] : rows;
1261
606
  }
1262
607
  #renderStatus(width) {
1263
608
  return this.#customViewport ? this.#status.renderLive(width) : this.#status.render(width);
1264
609
  }
1265
610
  #renderFooter(width) {
1266
- return (this.#extensionFooter ?? this.#footer).render(width);
1267
- }
1268
- #viewWithExtensionStatuses(view) {
1269
- const footer = view.status.footer;
1270
- const statuses = new Map(footer?.extensionStatuses ?? []);
1271
- for (const [key, text] of this.#extensionStatuses)
1272
- statuses.set(key, text);
1273
- return {
1274
- ...view,
1275
- status: {
1276
- ...view.status,
1277
- footer: {
1278
- ...footer,
1279
- branch: footer?.branch ?? null,
1280
- sessionName: footer?.sessionName ?? null,
1281
- availableProviderCount: footer?.availableProviderCount ?? 1,
1282
- extensionStatuses: [...statuses],
1283
- },
1284
- },
1285
- };
1286
- }
1287
- #invalidateExtensions() {
1288
- this.#extensionHeader?.invalidate();
1289
- this.#extensionFooter?.invalidate();
1290
- for (const { component } of this.#extensionWidgets.values())
1291
- component.invalidate();
611
+ const footer = this.#extensions.footer ?? this.#footer;
612
+ if (!this.#customViewport)
613
+ return footer.render(width);
614
+ width = Math.max(0, width - 2);
615
+ return footer.render(width).map(row => ` ${piShellTruncateToWidth(row, width)}`);
1292
616
  }
1293
617
  #setToolsExpanded(expanded) {
1294
618
  this.#toolsExpanded = expanded;
1295
619
  this.header.setExpanded(expanded);
1296
620
  this.resources.setExpanded(expanded);
1297
- for (const component of this.#transcript.values())
621
+ for (const component of this.#document.components())
1298
622
  component.setExpanded(expanded);
1299
623
  // Invariant: expansion changes what a block draws without changing its revision.
1300
- this.#renderedRows.clear();
1301
- this.#documentLayouts.clear();
624
+ this.#document.invalidate("tools-expanded");
1302
625
  this.invalidate();
1303
626
  }
1304
627
  }
1305
- const COPY_ACKNOWLEDGEMENT_DURATION_MS = 1_000;
1306
628
  const SCROLLBAR_CELL_RESET = "\u001b[22;23;24;25;27;28;29;39;54;55m";
1307
- const KITTY_IMAGE_CONTROL = /\u001b_G[\s\S]*?\u001b\\/g;
1308
- const ITERM_IMAGE_CONTROL = /\u001b]1337;File=[^\u0007]*(?:\u0007|\u001b\\)/g;
1309
- /** Keep semantic SGR/OSC styling while excluding non-replayable image payloads. */
1310
- function sanitizeExitTranscriptRow(row) {
1311
- return row.replace(KITTY_IMAGE_CONTROL, "").replace(ITERM_IMAGE_CONTROL, "");
1312
- }
1313
629
  const TERMINAL_BACKGROUND = /\u001b\[(?:4[0-9]|10[0-7]|48(?:[;:][0-9;:]*)?)m/g;
1314
- /** Web URLs use link blue; file targets retain A1's cyan interactive accent. */
1315
- function nativeTranscriptLinkColor(text, target) {
1316
- return piTheme().fg(/^file:/iu.test(target) ? "accent" : "mdLink", text);
1317
- }
1318
630
  /** Repaint a source prompt with viewport chrome while preserving text, links, and foreground roles. */
1319
631
  function withoutTerminalBackground(text) {
1320
632
  return text.replace(TERMINAL_BACKGROUND, "");
1321
633
  }
1322
- /** Preserve the stable metadata-grey foreground for every pinned timestamp state. */
1323
- function pinnedPromptSourceRow(block, sourceRow, width) {
1324
- if (typeof block.payload !== "object" || block.payload === null)
1325
- return sourceRow;
1326
- const value = block.payload.timestamp;
1327
- if (typeof value !== "number" || !Number.isFinite(value))
1328
- return sourceRow;
1329
- const timestamp = formatSubmittedPromptTime(value);
1330
- if (timestamp === null || submittedPromptLayout(width, value).timestamp === null)
1331
- return sourceRow;
1332
- const rowWidth = displayWidth(sourceRow);
1333
- const timestampWidth = displayWidth(timestamp);
1334
- if (rowWidth < timestampWidth)
1335
- return sourceRow;
1336
- return overlaySpan(sourceRow, rowWidth - timestampWidth, rowWidth, paintPiSubmittedPromptTimestamp(timestamp));
1337
- }
1338
634
  /**
1339
635
  * The pinned-profile screen: a followed transcript scroll above the dock. The runtime mounts one such layout,
1340
636
  * so its single `transcript` scroll view is shared by whichever session the parts currently resolve to.
@@ -1544,20 +840,13 @@ function isPackageExtensionSource(sourceInfo) {
1544
840
  const source = sourceInfo?.source ?? "";
1545
841
  return source.startsWith("npm:") || source.startsWith("git:");
1546
842
  }
1547
- function transientRowsSignature(steeringRows, statusRows) {
1548
- return `${steeringRows.length}\u0000${steeringRows.join("\u0000")}\u0001${statusRows.length}\u0000${statusRows.join("\u0000")}`;
1549
- }
1550
- /** The release, command, and optional changelog carried by the adapter's `release-update` diagnostic. */
1551
- function parseReleaseUpdateDiagnostic(message) {
1552
- const [instruction, changelog] = message.split("\n");
1553
- const match = /^New version (\S+) is available\. Run (.+)$/.exec(instruction ?? "");
1554
- if (match === null)
1555
- return null;
1556
- const url = changelog === undefined ? null : /^Changelog: (\S+)$/.exec(changelog)?.[1] ?? null;
1557
- return { version: match[1], command: match[2], changelogUrl: url };
1558
- }
1559
- /** "Share URL: <url>" with the URL made clickable; other share wordings pass through unchanged. */
1560
- function linkShareUrl(message) {
1561
- const match = /^Share URL: (\S+)$/.exec(message);
1562
- return match === null ? message : `Share URL: ${piShellHyperlink(match[1])}`;
1563
- }
843
+ // Performance: stable painter identities let the neutral viewport retain row-level
844
+ // selection variants while each callback still resolves the live Pi theme.
845
+ const VIEWPORT_THEME = {
846
+ track: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("dim", text)}`,
847
+ thumb: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("accent", text)}`,
848
+ sticky: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", withoutTerminalBackground(text))),
849
+ quietSticky: text => `\u001b[2m${text.replace(/\u001b\[(?:0|22)m/g, "$&\u001b[2m")}\u001b[22m`,
850
+ bottomControl: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", text)),
851
+ selection: paintTextSelection,
852
+ };