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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (423) hide show
  1. package/README.md +41 -3
  2. package/bin/automatic-update.js +7 -0
  3. package/bin/cli.js +4 -0
  4. package/bin/guardian.js +5 -0
  5. package/bin/tab-launch-context.d.ts +27 -0
  6. package/bin/tab-launch-context.js +94 -0
  7. package/bin/ui.js +107 -59
  8. package/dist/app/session-shell/clipboard-diagnostics.js +1 -1
  9. package/dist/app/session-shell/dialog-coordinator.d.ts +42 -0
  10. package/dist/app/session-shell/dialog-coordinator.js +133 -0
  11. package/dist/app/session-shell/dock-chrome.d.ts +34 -0
  12. package/dist/app/session-shell/dock-chrome.js +109 -0
  13. package/dist/app/session-shell/exit-notice.d.ts +1 -1
  14. package/dist/app/session-shell/exit-notice.js +1 -1
  15. package/dist/app/session-shell/extension-surface.d.ts +30 -0
  16. package/dist/app/session-shell/extension-surface.js +105 -0
  17. package/dist/app/session-shell/helper-pool.d.ts +3 -5
  18. package/dist/app/session-shell/helper-pool.js +3 -5
  19. package/dist/app/session-shell/input-surface-switch.d.ts +36 -0
  20. package/dist/app/session-shell/input-surface-switch.js +85 -0
  21. package/dist/app/session-shell/login-interaction.d.ts +27 -0
  22. package/dist/app/session-shell/login-interaction.js +91 -0
  23. package/dist/app/session-shell/prompt-submission.d.ts +48 -0
  24. package/dist/app/session-shell/prompt-submission.js +276 -0
  25. package/dist/app/session-shell/resident-tab-coordinator.d.ts +38 -0
  26. package/dist/app/session-shell/resident-tab-coordinator.js +77 -0
  27. package/dist/app/session-shell/response-copy-helper.js +21 -6
  28. package/dist/app/session-shell/response-copy-protocol.d.ts +1 -1
  29. package/dist/app/session-shell/response-copy-transport.d.ts +16 -6
  30. package/dist/app/session-shell/response-copy-transport.js +69 -17
  31. package/dist/app/session-shell/session-concern.d.ts +22 -0
  32. package/dist/app/session-shell/session-concern.js +29 -0
  33. package/dist/app/session-shell/session-lifecycle.d.ts +29 -0
  34. package/dist/app/session-shell/session-lifecycle.js +49 -0
  35. package/dist/app/session-shell/session-shell-root.d.ts +42 -6
  36. package/dist/app/session-shell/session-shell-root.js +196 -908
  37. package/dist/app/session-shell/session-shell.d.ts +3 -8
  38. package/dist/app/session-shell/session-shell.js +324 -835
  39. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -2
  40. package/dist/app/session-shell/session-viewport-controller.js +29 -15
  41. package/dist/app/session-shell/settings-bindings.d.ts +17 -0
  42. package/dist/app/session-shell/settings-bindings.js +91 -0
  43. package/dist/app/session-shell/shell-editor-factory.d.ts +27 -0
  44. package/dist/app/session-shell/shell-editor-factory.js +102 -0
  45. package/dist/app/session-shell/shell-host.d.ts +6 -0
  46. package/dist/app/session-shell/shell-host.js +34 -7
  47. package/dist/app/session-shell/shell-layout.d.ts +64 -0
  48. package/dist/app/session-shell/shell-layout.js +18 -0
  49. package/dist/app/session-shell/suggestion-surface.d.ts +25 -0
  50. package/dist/app/session-shell/suggestion-surface.js +63 -0
  51. package/dist/app/session-shell/transcript-document.d.ts +78 -0
  52. package/dist/app/session-shell/transcript-document.js +365 -0
  53. package/dist/app/session-shell/viewport-layout.d.ts +31 -0
  54. package/dist/app/session-shell/viewport-layout.js +160 -0
  55. package/dist/app/session-shell/workflow-output.d.ts +33 -0
  56. package/dist/app/session-shell/workflow-output.js +174 -0
  57. package/dist/app/session-shell/workflow-router.d.ts +25 -0
  58. package/dist/app/session-shell/workflow-router.js +39 -0
  59. package/dist/cli/dispatch.d.ts +11 -1
  60. package/dist/cli/dispatch.js +79 -5
  61. package/dist/cli/tabs-request.d.ts +7 -0
  62. package/dist/cli/tabs-request.js +1 -0
  63. package/dist/cli/tabs.d.ts +14 -0
  64. package/dist/cli/tabs.js +151 -0
  65. package/dist/cli/version-stats.js +38 -3
  66. package/dist/composition/owned-settings.d.ts +12 -0
  67. package/dist/composition/owned-settings.js +36 -0
  68. package/dist/composition/owned-ui.d.ts +27 -28
  69. package/dist/composition/owned-ui.js +205 -153
  70. package/dist/composition/resident-tab-bridge.d.ts +25 -0
  71. package/dist/composition/resident-tab-bridge.js +180 -0
  72. package/dist/composition/session-backend.d.ts +19 -0
  73. package/dist/composition/session-backend.js +1 -0
  74. package/dist/composition/session-runtime-registry.d.ts +20 -0
  75. package/dist/composition/session-runtime-registry.js +85 -0
  76. package/dist/composition/settings-route-host.d.ts +16 -1
  77. package/dist/composition/settings-route-host.js +29 -10
  78. package/dist/composition/settings-search-input.d.ts +9 -0
  79. package/dist/composition/settings-search-input.js +58 -0
  80. package/dist/composition/startup-route.d.ts +32 -0
  81. package/dist/composition/startup-route.js +72 -0
  82. package/dist/composition/utility-tab.d.ts +28 -0
  83. package/dist/composition/utility-tab.js +242 -0
  84. package/dist/contracts/owned-ui/index.d.ts +1 -1
  85. package/dist/contracts/owned-ui/model.d.ts +3 -0
  86. package/dist/contracts/owned-ui/session-backend.d.ts +3 -0
  87. package/dist/contracts/owned-ui/session-workflows.d.ts +11 -4
  88. package/dist/contracts/presentation/index.d.ts +11 -0
  89. package/dist/contracts/presentation/index.js +12 -0
  90. package/dist/features/launch/development-launch.js +6 -0
  91. package/dist/features/launch/index.d.ts +1 -1
  92. package/dist/features/launch/intent.d.ts +3 -1
  93. package/dist/features/launch/intent.js +7 -4
  94. package/dist/features/launch/profile-paths.js +4 -0
  95. package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
  96. package/dist/features/owned-ui/plus-menu-dialog.js +76 -0
  97. package/dist/features/owned-ui/project-trust-dialog.d.ts +1 -1
  98. package/dist/features/owned-ui/project-trust-dialog.js +6 -8
  99. package/dist/features/owned-ui/project-trust-prompt.d.ts +1 -0
  100. package/dist/features/owned-ui/project-trust-prompt.js +7 -3
  101. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -0
  102. package/dist/features/owned-ui/reference-screen-app.js +12 -2
  103. package/dist/features/owned-ui/resources/release-notes.json +4 -0
  104. package/dist/features/owned-ui/settings-app.d.ts +12 -2
  105. package/dist/features/owned-ui/settings-app.js +119 -84
  106. package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
  107. package/dist/features/owned-ui/settings-search-input.js +1 -0
  108. package/dist/features/prompt-history/index.d.ts +1 -1
  109. package/dist/features/prompt-history/paths.d.ts +2 -1
  110. package/dist/features/prompt-history/paths.js +10 -2
  111. package/dist/features/prompt-history/service.d.ts +5 -0
  112. package/dist/features/prompt-history/service.js +1 -1
  113. package/dist/features/resident-tab/auto-name.d.ts +11 -0
  114. package/dist/features/resident-tab/auto-name.js +44 -0
  115. package/dist/features/resident-tab/index.d.ts +5 -0
  116. package/dist/features/resident-tab/index.js +7 -0
  117. package/dist/features/resident-tab/shortcut-bindings-file.d.ts +8 -0
  118. package/dist/features/resident-tab/shortcut-bindings-file.js +40 -0
  119. package/dist/features/resident-tab/shortcut-declarations.d.ts +13 -0
  120. package/dist/features/resident-tab/shortcut-declarations.js +26 -0
  121. package/dist/features/resident-tab/shortcut-projection.d.ts +29 -0
  122. package/dist/features/resident-tab/shortcut-projection.js +64 -0
  123. package/dist/features/resident-tab/status-deriver.d.ts +10 -0
  124. package/dist/features/resident-tab/status-deriver.js +12 -0
  125. package/dist/foundation/launch-context/index.d.ts +12 -0
  126. package/dist/foundation/launch-context/index.js +26 -0
  127. package/dist/foundation/launch-guardian/exit-watcher.d.ts +14 -0
  128. package/dist/foundation/launch-guardian/exit-watcher.js +94 -0
  129. package/dist/foundation/launch-guardian/main.d.ts +7 -1
  130. package/dist/foundation/launch-guardian/main.js +60 -7
  131. package/dist/foundation/lifecycle/session-repository-context.js +16 -2
  132. package/dist/foundation/process-containment/{native-guardian-containment.d.ts → addon-containment.d.ts} +7 -5
  133. package/dist/foundation/process-containment/addon-containment.js +139 -0
  134. package/dist/foundation/process-containment/addon-process-inspector.d.ts +11 -0
  135. package/dist/foundation/process-containment/addon-process-inspector.js +36 -0
  136. package/dist/foundation/process-containment/addon.d.ts +42 -0
  137. package/dist/foundation/process-containment/addon.js +57 -0
  138. package/dist/foundation/process-containment/artifact.d.ts +1 -1
  139. package/dist/foundation/process-containment/artifact.js +6 -5
  140. package/dist/foundation/process-containment/contracts.d.ts +2 -0
  141. package/dist/foundation/process-containment/index.d.ts +4 -4
  142. package/dist/foundation/process-containment/index.js +3 -3
  143. package/dist/foundation/process-containment/native-process-inspector.d.ts +2 -1
  144. package/dist/foundation/process-containment/native-process-inspector.js +10 -10
  145. package/dist/foundation/release/automatic-update.d.ts +37 -0
  146. package/dist/foundation/release/automatic-update.js +265 -0
  147. package/dist/foundation/release/bootstrap.d.ts +7 -9
  148. package/dist/foundation/release/bootstrap.js +107 -24
  149. package/dist/foundation/release/cohort-state.d.ts +1 -1
  150. package/dist/foundation/release/cohort-state.js +6 -3
  151. package/dist/foundation/release/dependency-certification.js +54 -19
  152. package/dist/foundation/release/early-accent.d.ts +12 -0
  153. package/dist/foundation/release/early-accent.js +59 -0
  154. package/dist/foundation/release/index.d.ts +2 -2
  155. package/dist/foundation/release/index.js +2 -2
  156. package/dist/foundation/release/latest-release.d.ts +24 -2
  157. package/dist/foundation/release/latest-release.js +51 -1
  158. package/dist/foundation/release/progress-palette.generated.d.ts +137 -3
  159. package/dist/foundation/release/progress-palette.generated.js +137 -3
  160. package/dist/foundation/release/release-gc.d.ts +3 -0
  161. package/dist/foundation/release/release-gc.js +59 -8
  162. package/dist/foundation/release/release-store.d.ts +12 -0
  163. package/dist/foundation/release/release-store.js +124 -2
  164. package/dist/foundation/release/update-execution-lease.d.ts +48 -0
  165. package/dist/foundation/release/update-execution-lease.js +161 -0
  166. package/dist/foundation/release/update.d.ts +12 -7
  167. package/dist/foundation/release/update.js +281 -196
  168. package/dist/foundation/resident-tabs/bounded-host-command.d.ts +18 -0
  169. package/dist/foundation/resident-tabs/bounded-host-command.js +39 -0
  170. package/dist/foundation/resident-tabs/bridge-client.d.ts +70 -0
  171. package/dist/foundation/resident-tabs/bridge-client.js +321 -0
  172. package/dist/foundation/resident-tabs/doctor.d.ts +28 -0
  173. package/dist/foundation/resident-tabs/doctor.js +166 -0
  174. package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
  175. package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
  176. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +36 -0
  177. package/dist/foundation/resident-tabs/ensure-terminal-host.js +151 -0
  178. package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
  179. package/dist/foundation/resident-tabs/fault-injection.js +26 -0
  180. package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
  181. package/dist/foundation/resident-tabs/fixture-control.js +35 -0
  182. package/dist/foundation/resident-tabs/host-entry.d.ts +1 -0
  183. package/dist/foundation/resident-tabs/host-entry.js +17 -0
  184. package/dist/foundation/resident-tabs/index.d.ts +12 -0
  185. package/dist/foundation/resident-tabs/index.js +6 -0
  186. package/dist/foundation/resident-tabs/maintenance.d.ts +39 -0
  187. package/dist/foundation/resident-tabs/maintenance.js +75 -0
  188. package/dist/foundation/resident-tabs/progress-observation-schema.d.ts +34 -0
  189. package/dist/foundation/resident-tabs/progress-observation-schema.js +1 -0
  190. package/dist/foundation/resident-tabs/progress-observation.d.ts +4 -0
  191. package/dist/foundation/resident-tabs/progress-observation.js +150 -0
  192. package/dist/foundation/resident-tabs/prompt-journal.d.ts +63 -0
  193. package/dist/foundation/resident-tabs/prompt-journal.js +213 -0
  194. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  195. package/dist/foundation/resident-tabs/protocol.js +195 -0
  196. package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
  197. package/dist/foundation/resident-tabs/recovery.js +205 -0
  198. package/dist/foundation/resident-tabs/release-holds.d.ts +28 -0
  199. package/dist/foundation/resident-tabs/release-holds.js +75 -0
  200. package/dist/foundation/resident-tabs/routes.d.ts +5 -0
  201. package/dist/foundation/resident-tabs/routes.js +5 -0
  202. package/dist/foundation/resident-tabs/routing.d.ts +11 -0
  203. package/dist/foundation/resident-tabs/routing.js +10 -0
  204. package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
  205. package/dist/foundation/resident-tabs/session-sync.js +37 -0
  206. package/dist/foundation/resident-tabs/settings.d.ts +12 -0
  207. package/dist/foundation/resident-tabs/settings.js +58 -0
  208. package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +14 -0
  209. package/dist/foundation/resident-tabs/terminal-host-artifact.js +78 -0
  210. package/dist/foundation/resident-tabs/zip-archive.d.ts +9 -0
  211. package/dist/foundation/resident-tabs/zip-archive.js +56 -0
  212. package/dist/foundation/session-writer-lock/index.d.ts +30 -0
  213. package/dist/foundation/session-writer-lock/index.js +203 -0
  214. package/dist/foundation/startup/startup-descriptor.js +2 -2
  215. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  216. package/dist/foundation/terminal-cleanup/index.js +1 -1
  217. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +6 -0
  218. package/dist/foundation/terminal-cleanup/terminal-reset.js +6 -0
  219. package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
  220. package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
  221. package/dist/integrations/pi/components/modal-frame.js +48 -9
  222. package/dist/integrations/pi/components/models-dialog.js +4 -52
  223. package/dist/integrations/pi/components/owned-editor-ux.d.ts +6 -1
  224. package/dist/integrations/pi/components/owned-editor-ux.js +90 -6
  225. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
  226. package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
  227. package/dist/integrations/pi/components/share-operation-dialog.d.ts +2 -0
  228. package/dist/integrations/pi/components/share-operation-dialog.js +2 -1
  229. package/dist/integrations/pi/components/shell-editor-autocomplete.js +58 -36
  230. package/dist/integrations/pi/components/shell-extension-ui.js +79 -60
  231. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  232. package/dist/integrations/pi/components/shell-footer-status.js +21 -5
  233. package/dist/integrations/pi/components/shell-hotkey-sections.js +1 -1
  234. package/dist/integrations/pi/components/shell-presenters-info.d.ts +5 -2
  235. package/dist/integrations/pi/components/shell-presenters-info.js +18 -4
  236. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
  237. package/dist/integrations/pi/components/shell-selectors-dialogs.js +5 -1
  238. package/dist/integrations/pi/components/shell-shared-facade.d.ts +12 -1
  239. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  240. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  241. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
  242. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  243. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  244. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  245. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  246. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  247. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  248. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  249. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  250. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +8 -6
  251. package/dist/integrations/pi/components/upstream/components/owned-editor.js +16 -9
  252. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  253. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +4 -4
  254. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
  255. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  256. package/dist/integrations/pi/components/upstream/components/session-footer.js +31 -25
  257. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  258. package/dist/integrations/pi/components/upstream/components/session-selector.js +8 -7
  259. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  260. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  261. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
  262. package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
  263. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  264. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  265. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +8 -7
  266. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +10 -9
  267. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +2 -6
  268. package/dist/integrations/pi/components/upstream/components/tool-execution.js +103 -101
  269. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +9 -9
  270. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -21
  271. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  272. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  273. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
  274. package/dist/integrations/pi/components/upstream/history/editor-core.js +7 -5
  275. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  276. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  277. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  278. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  279. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  280. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  281. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  282. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  283. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  284. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  285. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +5 -0
  286. package/dist/integrations/pi/components/upstream/theme/theme.js +29 -2
  287. package/dist/integrations/pi/engine/adapter.d.ts +5 -77
  288. package/dist/integrations/pi/engine/adapter.js +95 -394
  289. package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
  290. package/dist/integrations/pi/engine/command-dispatch.js +57 -0
  291. package/dist/integrations/pi/engine/host.js +2 -2
  292. package/dist/integrations/pi/engine/offline-settings-port.d.ts +23 -0
  293. package/dist/integrations/pi/engine/offline-settings-port.js +53 -0
  294. package/dist/integrations/pi/engine/pi-settings-metadata.json +14 -2
  295. package/dist/integrations/pi/engine/ports.d.ts +56 -0
  296. package/dist/integrations/pi/engine/ports.js +93 -0
  297. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
  298. package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
  299. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  300. package/dist/integrations/pi/engine/runtime-integration.js +102 -29
  301. package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
  302. package/dist/integrations/pi/engine/session-runtime.js +13 -5
  303. package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
  304. package/dist/integrations/pi/engine/session-selection.js +45 -9
  305. package/dist/integrations/pi/engine/settings-bridge.d.ts +5 -0
  306. package/dist/integrations/pi/engine/settings-bridge.js +15 -0
  307. package/dist/integrations/pi/engine/settings-port.js +11 -15
  308. package/dist/integrations/pi/engine/view-state.d.ts +42 -0
  309. package/dist/integrations/pi/engine/view-state.js +126 -0
  310. package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
  311. package/dist/integrations/pi/engine/workflow-runner.js +73 -4
  312. package/dist/integrations/pi/engine/workflow-support.d.ts +12 -0
  313. package/dist/integrations/pi/engine/workflow-support.js +36 -0
  314. package/dist/integrations/pi/engine/workflows.d.ts +3 -1
  315. package/dist/integrations/pi/engine/workflows.js +1 -0
  316. package/dist/integrations/pi/startup-public.js +257 -310
  317. package/dist/integrations/pi/startup-public.manifest.json +564 -440
  318. package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
  319. package/dist/integrations/pi/tui-runtime/adapter.js +32 -1
  320. package/dist/integrations/pi/tui-runtime/contracts.d.ts +5 -0
  321. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
  322. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +31 -2
  323. package/dist/native/darwin-arm64/manifest.json +10 -7
  324. package/dist/native/darwin-arm64/process-guardian.node +0 -0
  325. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
  326. package/dist/native/darwin-arm64/terminal-host.manifest.json +46 -0
  327. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  328. package/dist/native/darwin-x64/manifest.json +23 -0
  329. package/dist/native/darwin-x64/process-guardian.node +0 -0
  330. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
  331. package/dist/native/darwin-x64/terminal-host.manifest.json +46 -0
  332. package/dist/native/darwin-x64/terminal-host.node +0 -0
  333. package/dist/native/linux-x64/manifest.json +10 -7
  334. package/dist/native/linux-x64/process-guardian.node +0 -0
  335. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
  336. package/dist/native/linux-x64/terminal-host.manifest.json +46 -0
  337. package/dist/native/linux-x64/terminal-host.node +0 -0
  338. package/dist/native/win32-x64/manifest.json +10 -7
  339. package/dist/native/win32-x64/process-guardian.node +0 -0
  340. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
  341. package/dist/native/win32-x64/terminal-host.manifest.json +46 -0
  342. package/dist/native/win32-x64/terminal-host.node +0 -0
  343. package/dist/product-identity.d.ts +3 -3
  344. package/dist/product-identity.js +4 -4
  345. package/dist/product-identity.json +16 -2
  346. package/dist/runtime-payload-inventory.json +6330 -6677
  347. package/dist/ui/apps/host.d.ts +2 -0
  348. package/dist/ui/apps/host.js +7 -1
  349. package/dist/ui/components/dialog-panel.js +35 -30
  350. package/dist/ui/components/index.d.ts +2 -2
  351. package/dist/ui/components/index.js +1 -1
  352. package/dist/ui/components/line-input.d.ts +4 -1
  353. package/dist/ui/components/line-input.js +3 -1
  354. package/dist/ui/components/prompt-input.d.ts +11 -8
  355. package/dist/ui/components/prompt-input.js +15 -13
  356. package/dist/ui/components/selection-copy.d.ts +2 -0
  357. package/dist/ui/components/selection-copy.js +13 -0
  358. package/dist/ui/components/spans.js +2 -2
  359. package/dist/ui/components/submitted-prompt.d.ts +9 -6
  360. package/dist/ui/components/submitted-prompt.js +20 -12
  361. package/dist/ui/components/surface.js +4 -2
  362. package/dist/ui/components/text.d.ts +3 -6
  363. package/dist/ui/components/text.js +6 -8
  364. package/dist/ui/components/transcript-viewport.d.ts +9 -2
  365. package/dist/ui/components/transcript-viewport.js +176 -55
  366. package/dist/ui/components/value-menu.d.ts +6 -3
  367. package/dist/ui/components/value-menu.js +6 -5
  368. package/dist/ui/settings/declarations.d.ts +90 -16
  369. package/dist/ui/settings/declarations.js +67 -14
  370. package/dist/ui/settings/early-accent.d.ts +81 -0
  371. package/dist/ui/settings/early-accent.js +50 -0
  372. package/dist/ui/settings/index.d.ts +4 -2
  373. package/dist/ui/settings/index.js +2 -1
  374. package/dist/ui/settings/manager.d.ts +16 -3
  375. package/dist/ui/settings/manager.js +122 -33
  376. package/dist/ui/settings/migrations.d.ts +6 -0
  377. package/dist/ui/settings/migrations.js +73 -0
  378. package/dist/ui/settings/profile-document.d.ts +26 -0
  379. package/dist/ui/settings/profile-document.js +164 -0
  380. package/dist/ui/settings/progress-palette.generated.d.ts +139 -0
  381. package/dist/ui/settings/progress-palette.generated.js +139 -0
  382. package/dist/ui/settings/sections.d.ts +4 -0
  383. package/dist/ui/settings/sections.js +9 -3
  384. package/docs/architecture/boundaries.md +25 -10
  385. package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
  386. package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
  387. package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
  388. package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
  389. package/docs/architecture/evidence/terminal-host-spike/manual-one-pane-terminal-host.md +70 -0
  390. package/docs/architecture/evidence/terminal-host-spike/manual-two-by-two-basic-terminal-host.md +32 -0
  391. package/docs/architecture/evidence/terminal-host-spike/milestone-baseline.json +115 -0
  392. package/docs/architecture/evidence/terminal-host-spike/owned-ui-foundation-prerequisite.json +48 -0
  393. package/docs/architecture/evidence/terminal-host-spike/terminal-host-provenance.json +125 -0
  394. package/docs/architecture/evidence/terminal-host-spike/terminal-host-task-5-1.json +56 -0
  395. package/docs/architecture/evidence/terminal-host-spike/terminal-host-task-5-3.json +174 -0
  396. package/docs/architecture/evidence/terminal-host-spike/terminal-spike-acceptance-record.json +18 -0
  397. package/docs/architecture/internal-naming.md +3 -1
  398. package/docs/architecture/process-guardian-provenance.md +18 -10
  399. package/docs/architecture/project-structure.md +8 -0
  400. package/docs/architecture/resident-tabs-requirements.md +18 -0
  401. package/docs/architecture/resident-tabs.md +648 -0
  402. package/docs/architecture/response-copy-delivery.md +1 -1
  403. package/docs/architecture/terminal-host-proof-gate.md +3 -1
  404. package/docs/architecture/terminal-host-provenance.md +16 -4
  405. package/docs/architecture/terminal-host-spike-evidence.md +1 -1
  406. package/docs/architecture/tool-image-presentation.md +2 -1
  407. package/docs/architecture/toolchain.md +11 -9
  408. package/docs/ci-release-runbook.md +14 -7
  409. package/docs/features/launch-profiles.md +11 -8
  410. package/docs/features/prompt-history.md +41 -20
  411. package/docs/manual-resident-tabs-acceptance.md +51 -0
  412. package/docs/openspec-archive-automation.md +49 -22
  413. package/docs/releases/0.2.6.md +44 -0
  414. package/docs/validation.md +49 -9
  415. package/package.json +19 -6
  416. package/dist/foundation/process-containment/darwin-process-inspector.d.ts +0 -5
  417. package/dist/foundation/process-containment/darwin-process-inspector.js +0 -7
  418. package/dist/foundation/process-containment/native-guardian-containment.js +0 -152
  419. package/dist/foundation/process-containment/windows-process-inspector.d.ts +0 -19
  420. package/dist/foundation/process-containment/windows-process-inspector.js +0 -56
  421. package/dist/native/darwin-arm64/process-guardian +0 -0
  422. package/dist/native/linux-x64/process-guardian +0 -0
  423. package/dist/native/win32-x64/process-guardian.exe +0 -0
@@ -1,35 +1,31 @@
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 { backgroundSgrSpan, nativeHyperlinkStyle, } from "../../ui/components/spans.js";
6
5
  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
6
  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";
7
+ import { createPiShellTranscriptComponent, } from "../../integrations/pi/components/shell-presenters-transcript.js";
8
+ import { onPiThemeChange, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
9
+ import { piShellTruncateToWidth, } from "../../integrations/pi/components/shell-shared-facade.js";
10
+ import {} from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
16
11
  import { PromptChipStore } from "./prompt-chips.js";
17
- import { EditorHyperlinkBudget } from "./editor-hyperlink-budget.js";
18
12
  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. */
13
+ import { createShellEditor } from "./shell-editor-factory.js";
14
+ import { ExtensionSurface } from "./extension-surface.js";
15
+ import { DockChrome } from "./dock-chrome.js";
16
+ import { WorkflowOutput } from "./workflow-output.js";
17
+ import { InputSurfaceSwitch } from "./input-surface-switch.js";
18
+ import { PinnedLayout } from "./shell-layout.js";
19
+ import { ViewportLayout } from "./viewport-layout.js";
20
+ import { nativeTranscriptLinkColor, TranscriptDocument } from "./transcript-document.js";
21
+ /** Composes session state into the owned transcript, viewport, editor, and dock. */
21
22
  export class OwnedUiSessionShellRoot {
22
23
  editor;
23
24
  header;
24
25
  resources;
25
26
  #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();
27
+ #document;
31
28
  #themeUnsubscribe;
32
- #transcriptOrder = [];
33
29
  #view;
34
30
  #status;
35
31
  #footer;
@@ -37,15 +33,10 @@ export class OwnedUiSessionShellRoot {
37
33
  #extensionRenderers;
38
34
  #imageAssets;
39
35
  #promptChips;
40
- #editorHyperlinks = new EditorHyperlinkBudget();
41
36
  #pasteDiagnosticId = 0;
42
- #terminalPasteDiagnosticRequest;
43
37
  #customViewport;
44
38
  #submittedPromptComposer;
45
39
  #viewportController;
46
- #viewportTheme;
47
- #onViewportFrame;
48
- #onInputSurfaceChanged;
49
40
  #onPromptSuggestionEligibilityChanged;
50
41
  #componentRuntime;
51
42
  #toolsExpanded = false;
@@ -55,33 +46,11 @@ export class OwnedUiSessionShellRoot {
55
46
  #showImages = true;
56
47
  #imageWidthCells = 80;
57
48
  #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;
49
+ #workflowOutput;
50
+ #input;
51
+ #layout;
52
+ #extensions = new ExtensionSurface();
53
+ #dockChrome;
85
54
  #imageSubmissionsSending = 0;
86
55
  constructor(view, cwd, handlers, startup = {}, agentDir, extensionRenderers = {
87
56
  getMessageRenderer: () => undefined,
@@ -94,21 +63,13 @@ export class OwnedUiSessionShellRoot {
94
63
  ...(handlers.pasteDiagnostics === undefined ? {} : { onEvent: handlers.pasteDiagnostics }),
95
64
  ...(handlers.pastePreparation === undefined ? {} : { preparation: handlers.pastePreparation }),
96
65
  ...(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;
66
+ this.#submittedPromptComposer = this.#customViewport ? this.#createSubmittedPromptComposer() : undefined;
104
67
  this.#cwd = cwd;
68
+ this.#document = this.#createDocument(cwd);
105
69
  this.#extensionRenderers = extensionRenderers;
106
70
  this.#imageAssets = imageAssets;
107
71
  this.#componentRuntime = handlers;
108
- this.#onViewportFrame = handlers.onViewportFrame;
109
- this.#onInputSurfaceChanged = handlers.onInputSurfaceChanged;
110
72
  this.#onPromptSuggestionEligibilityChanged = handlers.onPromptSuggestionEligibilityChanged;
111
- this.#dockInputReuseEnabled = handlers.enableDockInputReuse ?? true;
112
73
  this.header = createPiShellHeader({
113
74
  ...startup,
114
75
  ...(this.#customViewport ? { getKeybindings: () => this.editor.keybindingConfig() } : {}),
@@ -119,81 +80,16 @@ export class OwnedUiSessionShellRoot {
119
80
  frame: progressStatusFrame,
120
81
  }, handlers);
121
82
  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),
83
+ this.#footer = createPiShellFooter(this.#extensions.withStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel, handlers.footerSessionName ?? true);
84
+ this.editor = createShellEditor({
85
+ handlers,
86
+ customViewport: this.#customViewport,
178
87
  cwd,
179
88
  ...(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 }),
89
+ promptChips: this.#promptChips,
90
+ editorPointerSelecting: () => this.#viewportController.editorPointerSelecting,
91
+ nextPasteDiagnosticRequest: () => --this.#pasteDiagnosticId,
92
+ toggleToolsExpanded: () => this.#setToolsExpanded(!this.#toolsExpanded),
197
93
  });
198
94
  this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned", this.#customViewport ? () => this.editor.keybindingConfig() : undefined);
199
95
  this.#viewportController = new SessionViewportController({
@@ -205,33 +101,85 @@ export class OwnedUiSessionShellRoot {
205
101
  ...(handlers.pasteDiagnostics === undefined ? {} : { pasteDiagnostics: handlers.pasteDiagnostics }),
206
102
  nextPasteDiagnosticRequest: () => --this.#pasteDiagnosticId,
207
103
  });
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
- };
104
+ this.#dockChrome = new DockChrome({
105
+ invalidateChrome: () => this.#invalidateChrome(),
106
+ requestRender: () => this.#componentRuntime.requestRender(),
107
+ viewport: this.#viewportController,
108
+ });
109
+ this.#workflowOutput = new WorkflowOutput({
110
+ customViewport: this.#customViewport,
111
+ document: this.#document,
112
+ dock: this.#dockChrome,
113
+ transcriptLength: () => this.#view.transcript.length,
114
+ outputPad: () => this.#outputPad,
115
+ hotkeysPresentation: () => this.hotkeysPresentation(),
116
+ componentRuntime: this.#componentRuntime,
117
+ invalidate: () => this.invalidate(),
118
+ });
218
119
  this.editor.setThinkingLevel(view.thinkingLevel);
219
- this.#inputSurface = this.editor;
120
+ this.#input = new InputSurfaceSwitch(this.editor, {
121
+ dockInputReuse: this.#customViewport && (handlers.enableDockInputReuse ?? true),
122
+ viewport: this.#viewportController,
123
+ ...(handlers.onInputSurfaceChanged === undefined ? {} : { onChanged: handlers.onInputSurfaceChanged }),
124
+ });
125
+ this.#layout = this.#createLayout(handlers.onViewportFrame);
220
126
  for (const block of view.transcript) {
221
127
  if (block.kind === "user")
222
128
  this.editor.addToHistory(block.text);
223
129
  }
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.
130
+ this.#document.sync(view.transcript);
131
+ // Invariant: rendered rows expire when
132
+ // their active theme changes.
227
133
  this.#themeUnsubscribe = onPiThemeChange(() => {
228
134
  this.invalidate();
229
135
  this.#componentRuntime.requestRender(true);
230
136
  });
231
137
  }
138
+ // Invariant: the document reads the shell's chrome lazily, so it may be built before the header and viewport.
139
+ #createDocument(cwd) {
140
+ return new TranscriptDocument({
141
+ customViewport: this.#customViewport,
142
+ cwd,
143
+ mount: block => this.#mountTranscript(block),
144
+ chrome: {
145
+ diagnostics: () => this.#view.diagnostics,
146
+ headerRows: width => this.#extensions.header !== null ? this.#extensions.header.render(width)
147
+ : this.#customViewport ? [] : this.header.render(width),
148
+ resourceRows: width => this.#customViewport ? [] : this.resources.render(width),
149
+ hasExtensionHeader: () => this.#extensions.header !== null,
150
+ thinkingVisible: () => this.#thinkingVisible,
151
+ timeFormat: () => this.#viewportController.config.timeFormat,
152
+ },
153
+ });
154
+ }
155
+ // Invariant: one layout per root; the render path never consults the layout mode again.
156
+ #createLayout(onFrame) {
157
+ const ports = {
158
+ viewport: this.#viewportController,
159
+ input: this.#input,
160
+ editor: this.editor,
161
+ promptChips: this.#promptChips,
162
+ dockChrome: this.#dockChrome,
163
+ theme: VIEWPORT_THEME,
164
+ ...(onFrame === undefined ? {} : { onFrame }),
165
+ rows: () => this.#componentRuntime.getRows(),
166
+ dock: width => this.#renderDockLayout(width),
167
+ document: width => this.#document.layout(width),
168
+ queued: width => this.#renderQueued(width),
169
+ liveStatus: width => this.#renderStatus(width),
170
+ diagnostics: () => this.#view.diagnostics,
171
+ };
172
+ return this.#customViewport ? new ViewportLayout(ports) : new PinnedLayout(ports);
173
+ }
174
+ #createSubmittedPromptComposer() {
175
+ return {
176
+ layout: (width, source) => submittedPromptLayout(width, source, this.#viewportController.config.timeFormat ?? "8:00 PM"),
177
+ 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)),
178
+ };
179
+ }
232
180
  setEditorPaddingX(padding) {
233
181
  this.editor.setPaddingX(padding);
234
- this.#documentLayouts.clear();
182
+ this.#document.invalidate("editor-padding");
235
183
  }
236
184
  setAutocompleteMaxVisible(maxVisible) {
237
185
  this.editor.setAutocompleteMaxVisible(maxVisible);
@@ -241,7 +189,7 @@ export class OwnedUiSessionShellRoot {
241
189
  return;
242
190
  this.#outputPad = padding;
243
191
  this.#status.setOutputPad(padding);
244
- for (const component of this.#transcript.values())
192
+ for (const component of this.#document.components())
245
193
  component.setOutputPad(padding);
246
194
  this.#invalidateTranscriptPresentation();
247
195
  }
@@ -249,7 +197,7 @@ export class OwnedUiSessionShellRoot {
249
197
  if (this.#thinkingVisible === !hidden)
250
198
  return;
251
199
  this.#thinkingVisible = !hidden;
252
- for (const component of this.#transcript.values())
200
+ for (const component of this.#document.components())
253
201
  component.setHideThinkingBlock(hidden);
254
202
  this.#invalidateTranscriptPresentation();
255
203
  }
@@ -257,7 +205,7 @@ export class OwnedUiSessionShellRoot {
257
205
  if (this.#mermaidRenderingMode === mode)
258
206
  return;
259
207
  this.#mermaidRenderingMode = mode;
260
- for (const component of this.#transcript.values())
208
+ for (const component of this.#document.components())
261
209
  component.setMermaidRenderingMode(mode);
262
210
  this.#invalidateTranscriptPresentation();
263
211
  }
@@ -266,7 +214,7 @@ export class OwnedUiSessionShellRoot {
266
214
  return;
267
215
  this.#showImages = showImages;
268
216
  this.#imageWidthCells = imageWidthCells;
269
- for (const component of this.#transcript.values())
217
+ for (const component of this.#document.components())
270
218
  component.setImagePresentation(showImages, imageWidthCells);
271
219
  this.#invalidateTranscriptPresentation();
272
220
  }
@@ -286,7 +234,7 @@ export class OwnedUiSessionShellRoot {
286
234
  return this.#promptChips.prepareSubmission(text);
287
235
  }
288
236
  reconcilePromptImageLimitNotice(text = this.editor.getText()) {
289
- if (this.#dockNotice?.errorCode !== "image-count" || !this.#promptChips.imageLimitState(text).corrected)
237
+ if (this.#dockChrome.notice?.errorCode !== "image-count" || !this.#promptChips.imageLimitState(text).corrected)
290
238
  return false;
291
239
  this.dismissNotice();
292
240
  return true;
@@ -332,22 +280,22 @@ export class OwnedUiSessionShellRoot {
332
280
  const becameReady = this.#view.lifecycle !== "ready" && view.lifecycle === "ready";
333
281
  if (view.diagnostics.length !== this.#view.diagnostics.length
334
282
  || view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
335
- this.#documentLayouts.clear();
283
+ this.#document.invalidate("diagnostics");
336
284
  }
337
285
  const promptAccepted = this.#view.lifecycle !== "busy" && view.lifecycle === "busy";
338
286
  this.#view = view;
339
287
  // Invariant: the idle new-session confirmation yields to the first truthful live status;
340
288
  // ordinary dock notices deliberately remain visible below Working during a run.
341
- if (promptAccepted && this.#dockNotice?.kind === "new-session")
342
- this.#dockNotice = undefined;
289
+ if (promptAccepted && this.#dockChrome.notice?.kind === "new-session")
290
+ this.#dockChrome.forgetNotice();
343
291
  if (promptAccepted && this.#imageSubmissionsSending > 0) {
344
292
  this.#imageSubmissionsSending = 0;
345
293
  this.#syncWorkingOverride();
346
294
  }
347
295
  this.#status.update(view);
348
- this.#footer.update(this.#viewWithExtensionStatuses(view));
296
+ this.#footer.update(this.#extensions.withStatuses(view));
349
297
  this.#queued.update(view.editor.queuedSubmissions);
350
- this.#syncTranscript(view.transcript);
298
+ this.#document.sync(view.transcript);
351
299
  this.editor.setSubmitEnabled(view.lifecycle !== "stopping" && view.lifecycle !== "stopped" && view.lifecycle !== "failed");
352
300
  this.editor.setThinkingLevel(view.thinkingLevel);
353
301
  // Performance: semantic updates already invalidate affected transcript blocks. Chrome is a separate authority.
@@ -361,24 +309,7 @@ export class OwnedUiSessionShellRoot {
361
309
  * walk of the whole transcript.
362
310
  */
363
311
  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();
312
+ this.#document.apply(block);
382
313
  }
383
314
  #mountTranscript(block) {
384
315
  // Invariant: the notice answers the reader's last command, so only their next prompt or
@@ -386,129 +317,18 @@ export class OwnedUiSessionShellRoot {
386
317
  if (block.kind === "user" || block.kind === "bash")
387
318
  this.dismissNotice();
388
319
  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)
320
+ if (this.#document.component(block.id) !== created)
390
321
  return;
391
- this.#renderedRows.delete(block.id);
392
- this.#documentLayouts.clear();
393
- this.#visibleViewportSnapshot = undefined;
394
- this.#dockInputCandidate = false;
395
- this.#dockInputRevision = undefined;
322
+ this.#document.blockChanged(block.id);
323
+ this.#layout.invalidate();
324
+ this.#input.clearDockInputCandidate();
396
325
  this.#componentRuntime.requestRender();
397
326
  } });
398
327
  created.setExpanded(this.#toolsExpanded);
399
328
  return created;
400
329
  }
401
330
  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;
331
+ return this.#layout.render(width);
512
332
  }
513
333
  viewportFrameDescriptor() {
514
334
  return this.#viewportController.frame?.descriptor ?? null;
@@ -516,10 +336,10 @@ export class OwnedUiSessionShellRoot {
516
336
  /** Read-only, payload-free evidence; observing a pending frame must never render or repair it. */
517
337
  viewportPresentationEvidence() {
518
338
  const frame = this.#viewportController.frame;
519
- const snapshot = this.#visibleViewportSnapshot;
339
+ const snapshot = this.#layout.composed();
520
340
  return {
521
- candidate: this.#dockInputCandidate,
522
- candidateRevision: this.#dockInputCandidate ? this.#dockInputRevision ?? null : null,
341
+ candidate: this.#input.dockInputCandidate,
342
+ candidateRevision: this.#input.dockInputCandidate ? this.#input.dockInputRevision ?? null : null,
523
343
  currentRevision: this.#viewportController.presentationRevision,
524
344
  composedRevision: snapshot?.viewportRevision ?? null,
525
345
  currentPointer: this.#viewportController.pointerPosition ?? null,
@@ -540,27 +360,16 @@ export class OwnedUiSessionShellRoot {
540
360
  return this.#viewportController.hasSelection || this.editor.hasSelection();
541
361
  }
542
362
  setViewportConfig(config) {
543
- if (this.#viewportController.setConfig(config))
544
- this.#documentLayouts.clear();
363
+ if (this.#viewportController.setConfig(config)) {
364
+ this.#document.invalidate("viewport-config");
365
+ this.#layout.invalidate();
366
+ }
545
367
  }
546
368
  setFullscreenCopyOnSelect(enabled) {
547
369
  this.#viewportController.setCopyOnSelect(enabled);
548
370
  }
549
371
  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();
372
+ this.#dockChrome.showCopyAcknowledgement(message);
564
373
  }
565
374
  resumeViewportFollowing() {
566
375
  this.#viewportController.resumeFollowing();
@@ -570,13 +379,7 @@ export class OwnedUiSessionShellRoot {
570
379
  }
571
380
  /** A new session binding owns new mounts even when it reuses semantic invocation ids. */
572
381
  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();
382
+ this.#document.reset();
580
383
  }
581
384
  resetViewport() {
582
385
  this.#viewportController.reset();
@@ -594,18 +397,17 @@ export class OwnedUiSessionShellRoot {
594
397
  handleViewportPreInput(data, allowWheel = true, now = Date.now(), editorActive = this.usesDefaultInputSurface()) {
595
398
  return this.#viewportController.handlePreInput(data, allowWheel, now, editorActive);
596
399
  }
597
- #renderDock(width) {
598
- return this.#renderDockLayout(width).rows;
599
- }
600
400
  #renderDockLayout(width) {
601
401
  const queued = this.#customViewport ? [] : this.#renderQueued(width);
602
402
  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.
403
+ const noticeRows = this.#dockChrome.renderNotice(width, this.#outputPad);
404
+ const transientRows = [...queued, ...statusRows, ...noticeRows];
405
+ const messageStart = queued.length;
406
+ const copyableMessageRows = Array.from({ length: statusRows.length + noticeRows.length }, (_value, index) => ({ row: messageStart + index }));
407
+ const aboveWidgets = this.#extensions.renderWidgets("aboveEditor", width);
408
+ const input = this.#input.surface.render(width);
607
409
  const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
608
- const belowWidgets = this.#renderWidgets("belowEditor", width);
410
+ const belowWidgets = this.#extensions.renderWidgets("belowEditor", width);
609
411
  const footer = this.#renderFooter(width);
610
412
  const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
611
413
  const inputOffset = transientRows.length + aboveWidgets.length;
@@ -614,68 +416,12 @@ export class OwnedUiSessionShellRoot {
614
416
  inputOffset,
615
417
  editorOffset: inputOffset + (body?.rowOffset ?? 0),
616
418
  inputRows: body?.rowCount ?? input.length,
419
+ footerRows: footer.length,
420
+ copyableMessageRows,
617
421
  };
618
422
  }
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
423
  dismissNotice() {
675
- if (this.#dockNotice === undefined)
676
- return;
677
- this.#dockNotice = undefined;
678
- this.#invalidateChrome();
424
+ this.#dockChrome.dismissNotice();
679
425
  }
680
426
  #renderQueued(width) {
681
427
  return this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
@@ -685,284 +431,30 @@ export class OwnedUiSessionShellRoot {
685
431
  return {
686
432
  document: layoutPort(width => this.#renderDocument(width), () => this.invalidate()),
687
433
  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()),
434
+ aboveWidgets: layoutPort(width => this.#extensions.renderWidgets("aboveEditor", width), () => this.#extensions.invalidate()),
689
435
  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()),
436
+ editor: layoutPort(width => this.#input.surface.render(width), () => this.#input.surface.invalidate(), data => this.#input.surface.handleInput?.(data)),
437
+ belowWidgets: layoutPort(width => this.#extensions.renderWidgets("belowEditor", width), () => this.#extensions.invalidate()),
438
+ footer: layoutPort(width => this.#renderFooter(width), () => (this.#extensions.footer ?? this.#footer).invalidate()),
693
439
  };
694
440
  }
695
441
  #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;
442
+ return this.#document.layout(width).rows;
836
443
  }
837
444
  transcriptComponent(id) {
838
- return this.#transcript.get(id);
445
+ return this.#document.component(id);
839
446
  }
840
447
  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");
448
+ return this.#document.exitTranscript(width);
853
449
  }
854
450
  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;
451
+ this.#workflowOutput.appendStatus(message);
878
452
  }
879
453
  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));
454
+ this.#workflowOutput.appendMessage(message, errorCode);
896
455
  }
897
456
  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);
457
+ this.#workflowOutput.appendResult(result, errorCode);
966
458
  }
967
459
  toggleThinkingVisibility() {
968
460
  this.setHideThinkingBlock(this.#thinkingVisible);
@@ -974,39 +466,25 @@ export class OwnedUiSessionShellRoot {
974
466
  this.#setToolsExpanded(expanded);
975
467
  }
976
468
  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?.();
469
+ this.#extensions.placeWidget(key, component, placement);
984
470
  this.invalidate();
985
471
  }
986
472
  setExtensionHeader(component) {
987
- if (this.#extensionHeader !== component)
988
- this.#extensionHeader?.dispose?.();
989
- this.#extensionHeader = component;
990
- this.#documentLayouts.clear();
473
+ this.#extensions.replaceHeader(component);
474
+ this.#document.invalidate("extension-header");
991
475
  this.invalidate();
992
476
  }
993
477
  setExtensionFooter(component) {
994
- if (this.#extensionFooter !== component)
995
- this.#extensionFooter?.dispose?.();
996
- this.#extensionFooter = component;
478
+ this.#extensions.replaceFooter(component);
997
479
  this.invalidate();
998
480
  }
999
481
  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));
482
+ this.#extensions.setStatus(key, text);
483
+ this.#footer.update(this.#extensions.withStatuses(this.#view));
1005
484
  this.invalidate();
1006
485
  }
1007
- setExtensionWorking(message, visible = this.#extensionWorkingVisible) {
1008
- this.#extensionWorkingMessage = message;
1009
- this.#extensionWorkingVisible = visible;
486
+ setExtensionWorking(message, visible = this.#extensions.working.visible) {
487
+ this.#extensions.setWorking(message, visible);
1010
488
  this.#syncWorkingOverride();
1011
489
  this.invalidate();
1012
490
  }
@@ -1028,7 +506,7 @@ export class OwnedUiSessionShellRoot {
1028
506
  const sending = this.#imageSubmissionsSending > 0;
1029
507
  this.#status.setWorkingOverride(sending
1030
508
  ? "Sending…"
1031
- : this.#extensionWorkingVisible ? this.#extensionWorkingMessage : undefined, sending);
509
+ : this.#extensions.working.visible ? this.#extensions.working.message : undefined, sending);
1032
510
  }
1033
511
  hotkeysPresentation() {
1034
512
  return {
@@ -1038,34 +516,17 @@ export class OwnedUiSessionShellRoot {
1038
516
  };
1039
517
  }
1040
518
  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();
519
+ this.#dockChrome.forgetNotice();
520
+ this.#dockChrome.clearCopyAcknowledgement(false);
521
+ this.#workflowOutput.reset();
1052
522
  this.invalidate();
1053
523
  }
1054
524
  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;
525
+ this.#extensions.reset();
1064
526
  this.#syncWorkingOverride();
1065
- this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
527
+ this.#footer.update(this.#extensions.withStatuses(this.#view));
1066
528
  // Invariant: an extension renderer may have drawn transcript blocks that are now unrendered by it.
1067
- this.#renderedRows.clear();
1068
- this.#documentLayouts.clear();
529
+ this.#document.invalidate("extension-renderers");
1069
530
  this.invalidate();
1070
531
  }
1071
532
  addExtensionNotification(message, type) {
@@ -1078,263 +539,97 @@ export class OwnedUiSessionShellRoot {
1078
539
  extensionFooterData() {
1079
540
  return {
1080
541
  getGitBranch: () => this.#view.status.footer?.branch ?? null,
1081
- getExtensionStatuses: () => new Map(this.#extensionStatuses),
542
+ getExtensionStatuses: () => this.#extensions.statuses(),
1082
543
  getAvailableProviderCount: () => this.#view.status.footer?.availableProviderCount ?? 1,
1083
544
  onBranchChange: () => () => { },
1084
545
  };
1085
546
  }
1086
547
  usesDefaultInputSurface() {
1087
- return this.#inputSurface === this.editor;
548
+ return this.#input.usesEditor;
1088
549
  }
1089
550
  inputCoordinationSurface() {
1090
- return this.#inputSurfaceCoordination;
551
+ return this.#input.coordination;
1091
552
  }
1092
553
  viewportCompositionEvidence() {
1093
- return { full: this.#fullViewportCompositions, dockOnly: this.#dockOnlyViewportCompositions };
554
+ return this.#layout.compositions();
1094
555
  }
1095
556
  transcriptRenderCount() {
1096
- return this.#transcriptBlockRenders;
557
+ return this.#document.renderCount;
1097
558
  }
1098
559
  setFooterLevel(visible) { this.#footerLevel = visible; }
1099
560
  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();
561
+ if (this.#input.set(component, disposePrevious, coordination))
562
+ this.invalidate();
1116
563
  }
1117
564
  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
- }
565
+ this.#input.handleInput(data);
1128
566
  }
1129
567
  invalidate() {
1130
- this.#renderedRows.clear();
1131
- this.#documentLayouts.clear();
1132
- for (const component of this.#transcript.values())
1133
- component.invalidate();
568
+ this.#document.invalidate("repaint");
1134
569
  this.#invalidateChrome();
1135
570
  }
1136
571
  #invalidateChrome() {
1137
572
  this.header.invalidate();
1138
573
  this.resources.invalidate();
1139
574
  this.editor.invalidate();
1140
- if (this.#inputSurface !== this.editor)
1141
- this.#inputSurface.invalidate();
1142
- this.#invalidateExtensions();
575
+ if (!this.#input.usesEditor)
576
+ this.#input.surface.invalidate();
577
+ this.#extensions.invalidate();
1143
578
  this.#status.invalidate();
1144
579
  this.#footer.invalidate();
1145
580
  this.#queued.invalidate();
1146
581
  }
1147
582
  setFocused(focused) {
1148
- this.#inputSurface.setFocused?.(focused);
583
+ this.#input.surface.setFocused?.(focused);
1149
584
  if (focused)
1150
585
  this.#onPromptSuggestionEligibilityChanged?.();
1151
586
  }
1152
587
  dispose() {
1153
- this.#clearCopyAcknowledgement(false);
588
+ this.#dockChrome.clearCopyAcknowledgement(false);
1154
589
  this.clearViewportPointerState();
1155
590
  this.#themeUnsubscribe();
1156
591
  this.header.dispose?.();
1157
592
  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();
593
+ this.#document.dispose();
594
+ this.#layout.invalidate();
595
+ this.#input.dispose();
596
+ this.#extensions.dispose();
1173
597
  this.editor.dispose?.();
1174
598
  this.#status.dispose?.();
1175
599
  this.#footer.dispose?.();
1176
600
  this.#queued.dispose?.();
1177
601
  }
1178
602
  #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();
603
+ this.#document.invalidate("block-presentation");
1253
604
  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
605
  }
1262
606
  #renderStatus(width) {
1263
607
  return this.#customViewport ? this.#status.renderLive(width) : this.#status.render(width);
1264
608
  }
1265
609
  #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();
610
+ const footer = this.#extensions.footer ?? this.#footer;
611
+ if (!this.#customViewport)
612
+ return footer.render(width);
613
+ width = Math.max(0, width - 2);
614
+ return footer.render(width).map(row => ` ${piShellTruncateToWidth(row, width)}`);
1292
615
  }
1293
616
  #setToolsExpanded(expanded) {
1294
617
  this.#toolsExpanded = expanded;
1295
618
  this.header.setExpanded(expanded);
1296
619
  this.resources.setExpanded(expanded);
1297
- for (const component of this.#transcript.values())
620
+ for (const component of this.#document.components())
1298
621
  component.setExpanded(expanded);
1299
622
  // Invariant: expansion changes what a block draws without changing its revision.
1300
- this.#renderedRows.clear();
1301
- this.#documentLayouts.clear();
623
+ this.#document.invalidate("tools-expanded");
1302
624
  this.invalidate();
1303
625
  }
1304
626
  }
1305
- const COPY_ACKNOWLEDGEMENT_DURATION_MS = 1_000;
1306
627
  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
628
  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
629
  /** Repaint a source prompt with viewport chrome while preserving text, links, and foreground roles. */
1319
630
  function withoutTerminalBackground(text) {
1320
631
  return text.replace(TERMINAL_BACKGROUND, "");
1321
632
  }
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
633
  /**
1339
634
  * The pinned-profile screen: a followed transcript scroll above the dock. The runtime mounts one such layout,
1340
635
  * so its single `transcript` scroll view is shared by whichever session the parts currently resolve to.
@@ -1544,20 +839,13 @@ function isPackageExtensionSource(sourceInfo) {
1544
839
  const source = sourceInfo?.source ?? "";
1545
840
  return source.startsWith("npm:") || source.startsWith("git:");
1546
841
  }
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
- }
842
+ // Performance: stable painter identities let the neutral viewport retain row-level
843
+ // selection variants while each callback still resolves the live Pi theme.
844
+ const VIEWPORT_THEME = {
845
+ track: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("dim", text)}`,
846
+ thumb: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("accent", text)}`,
847
+ sticky: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", withoutTerminalBackground(text))),
848
+ quietSticky: text => `\u001b[2m${text.replace(/\u001b\[(?:0|22)m/g, "$&\u001b[2m")}\u001b[22m`,
849
+ bottomControl: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", text)),
850
+ selection: (line, from, to) => backgroundSgrSpan(line, from, to, "\u001b[48;2;38;79;120m", "\u001b[49m"),
851
+ };