@north-light/crouter 0.3.170 → 0.3.172

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 (670) hide show
  1. package/dist/api/client.d.ts +37 -11
  2. package/dist/api/client.js +84 -15
  3. package/dist/api/dto/human.d.ts +53 -39
  4. package/dist/api/dto/human.js +2 -3
  5. package/dist/api/dto/inbox.d.ts +0 -1
  6. package/dist/api/dto/nodes.d.ts +12 -0
  7. package/dist/api/dto/review-comments.d.ts +137 -0
  8. package/dist/api/dto/review-comments.js +5 -0
  9. package/dist/api/dto/reviews.d.ts +109 -0
  10. package/dist/api/dto/reviews.js +5 -0
  11. package/dist/api/index.d.ts +2 -0
  12. package/dist/api/index.js +2 -0
  13. package/dist/api/routes.d.ts +17 -4
  14. package/dist/api/routes.js +21 -5
  15. package/dist/build-root.d.ts +1 -16
  16. package/dist/build-root.js +1 -25
  17. package/dist/builtin-memory/00-runtime-base.md +7 -5
  18. package/dist/builtin-memory/04-base-worker.md +1 -1
  19. package/dist/builtin-memory/04-orchestration-kernel.md +7 -7
  20. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  21. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -1
  23. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -1
  24. package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -1
  25. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -1
  26. package/dist/builtin-memory/05-kinds/review/00-base.md +1 -1
  27. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +20 -0
  28. package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -7
  29. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -3
  30. package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -3
  31. package/dist/builtin-memory/design.md +2 -2
  32. package/dist/builtin-memory/insights/capture.md +50 -0
  33. package/dist/builtin-memory/insights/init.md +45 -0
  34. package/dist/builtin-memory/insights/listen.md +9 -0
  35. package/dist/builtin-memory/internal/agent-shaping.md +2 -2
  36. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  37. package/dist/builtin-memory/internal/plugins.md +12 -11
  38. package/dist/builtin-memory/internal/storage-tiers.md +2 -2
  39. package/dist/builtin-memory/{planning.md → plan/roadmap.md} +3 -3
  40. package/dist/builtin-memory/plan.md +19 -0
  41. package/dist/builtin-memory/spec/guide.md +38 -0
  42. package/dist/builtin-memory/spec/requirements.md +31 -0
  43. package/dist/builtin-memory/spec/roadmap.md +35 -0
  44. package/dist/builtin-memory/spec.md +10 -86
  45. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +3 -4
  46. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +49 -5
  47. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +56 -0
  48. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +920 -0
  49. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +221 -156
  50. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +4 -5
  51. package/dist/cli.js +2 -6
  52. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +7 -35
  53. package/dist/clients/attach/__tests__/attach-keybindings.test.js +32 -16
  54. package/dist/clients/attach/__tests__/context-message.test.js +142 -6
  55. package/dist/clients/attach/__tests__/crtr-output.test.js +29 -45
  56. package/dist/clients/attach/__tests__/diagram.test.js +1 -1
  57. package/dist/clients/attach/__tests__/file-review-focus.test.js +48 -0
  58. package/dist/clients/attach/__tests__/group-activity.test.js +266 -0
  59. package/dist/clients/attach/__tests__/input-controller-extension-command.test.js +32 -0
  60. package/dist/clients/attach/__tests__/session-identity-refresh.test.js +32 -0
  61. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -2
  62. package/dist/clients/attach/chrome/header.d.ts +10 -0
  63. package/dist/clients/attach/chrome/header.js +30 -0
  64. package/dist/clients/attach/chrome/recap.d.ts +7 -0
  65. package/dist/clients/attach/chrome/recap.js +19 -0
  66. package/dist/clients/attach/chrome/status-line.d.ts +3 -0
  67. package/dist/clients/attach/chrome/status-line.js +5 -0
  68. package/dist/clients/attach/chrome/widgets.js +11 -21
  69. package/dist/clients/attach/command.js +11 -7
  70. package/dist/clients/attach/config.d.ts +4 -0
  71. package/dist/clients/attach/config.js +3 -0
  72. package/dist/clients/attach/input/controller.d.ts +19 -0
  73. package/dist/clients/attach/input/controller.js +62 -28
  74. package/dist/clients/attach/input/titled-editor.d.ts +0 -4
  75. package/dist/clients/attach/input/titled-editor.js +2 -10
  76. package/dist/clients/attach/overlays/file-review.d.ts +9 -5
  77. package/dist/clients/attach/overlays/file-review.js +9 -70
  78. package/dist/clients/attach/overlays/help.d.ts +33 -0
  79. package/dist/clients/attach/overlays/help.js +204 -0
  80. package/dist/clients/attach/render/activity-slot.d.ts +4 -0
  81. package/dist/clients/attach/render/activity-slot.js +13 -0
  82. package/dist/clients/attach/render/chat-view.d.ts +117 -12
  83. package/dist/clients/attach/render/chat-view.js +604 -135
  84. package/dist/clients/attach/render/context-message.d.ts +2 -1
  85. package/dist/clients/attach/render/context-message.js +17 -13
  86. package/dist/clients/attach/render/crtr-output.js +1 -3
  87. package/dist/clients/attach/render/diagram.js +2 -2
  88. package/dist/clients/attach/render/edit-diff.d.ts +4 -1
  89. package/dist/clients/attach/render/edit-diff.js +7 -11
  90. package/dist/clients/attach/render/group-activity.d.ts +32 -0
  91. package/dist/clients/attach/render/group-activity.js +206 -0
  92. package/dist/clients/attach/render/group-recap.d.ts +10 -0
  93. package/dist/clients/attach/render/group-recap.js +93 -0
  94. package/dist/clients/attach/render/html-markdown.d.ts +17 -0
  95. package/dist/clients/attach/render/html-markdown.js +228 -0
  96. package/dist/clients/attach/render/measured-container.d.ts +96 -0
  97. package/dist/clients/attach/render/measured-container.js +322 -0
  98. package/dist/clients/attach/render/scroll-animation.d.ts +8 -0
  99. package/dist/clients/attach/render/scroll-animation.js +80 -0
  100. package/dist/clients/attach/render/tool-calls.js +6 -6
  101. package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
  102. package/dist/clients/attach/render/transcript-copy.js +81 -0
  103. package/dist/clients/attach/render/viewport.d.ts +29 -0
  104. package/dist/clients/attach/render/viewport.js +159 -0
  105. package/dist/clients/attach/session/bindings.d.ts +10 -7
  106. package/dist/clients/attach/session/bindings.js +23 -8
  107. package/dist/clients/attach/session/connection.js +4 -5
  108. package/dist/clients/attach/session/context.d.ts +6 -4
  109. package/dist/clients/attach/session/frame.d.ts +34 -0
  110. package/dist/clients/attach/session/frame.js +176 -0
  111. package/dist/clients/attach/session/frames.d.ts +4 -1
  112. package/dist/clients/attach/session/frames.js +14 -1
  113. package/dist/clients/attach/session/identity.js +4 -3
  114. package/dist/clients/attach/session/input-wiring.d.ts +14 -4
  115. package/dist/clients/attach/session/input-wiring.js +15 -13
  116. package/dist/clients/attach/session/invariant.d.ts +9 -0
  117. package/dist/clients/attach/session/invariant.js +15 -0
  118. package/dist/clients/attach/session/keys.d.ts +10 -2
  119. package/dist/clients/attach/session/keys.js +13 -5
  120. package/dist/clients/attach/session/layout.d.ts +10 -8
  121. package/dist/clients/attach/session/layout.js +33 -34
  122. package/dist/clients/attach/session/mouse.d.ts +18 -0
  123. package/dist/clients/attach/session/mouse.js +64 -0
  124. package/dist/clients/attach/session/pane-tag.d.ts +3 -0
  125. package/dist/clients/attach/session/pane-tag.js +51 -8
  126. package/dist/clients/attach/session/reconnect.d.ts +3 -0
  127. package/dist/clients/attach/session/reconnect.js +42 -5
  128. package/dist/clients/attach/session/surface-frame.d.ts +20 -0
  129. package/dist/clients/attach/session/surface-frame.js +73 -0
  130. package/dist/clients/attach/session/surface-host.d.ts +64 -0
  131. package/dist/clients/attach/session/surface-host.js +255 -0
  132. package/dist/clients/attach/session/whip.d.ts +17 -0
  133. package/dist/clients/attach/session/whip.js +334 -0
  134. package/dist/clients/attach/slash/dispatch.d.ts +13 -1
  135. package/dist/clients/attach/slash/dispatch.js +51 -29
  136. package/dist/clients/attach/viewer.js +907 -819
  137. package/dist/clients/conversation/conversation.d.ts +65 -0
  138. package/dist/clients/conversation/conversation.js +318 -0
  139. package/dist/clients/conversation/frames.d.ts +30 -0
  140. package/dist/clients/conversation/frames.js +52 -0
  141. package/dist/clients/conversation/region.d.ts +51 -0
  142. package/dist/clients/conversation/region.js +265 -0
  143. package/dist/clients/conversation/submit.d.ts +38 -0
  144. package/dist/clients/conversation/submit.js +51 -0
  145. package/dist/clients/inbox/__tests__/inbox-controller.test.js +247 -0
  146. package/dist/clients/inbox/__tests__/mount-panel.test.js +776 -0
  147. package/dist/clients/inbox/controller.d.ts +79 -0
  148. package/dist/clients/inbox/controller.js +448 -0
  149. package/dist/clients/inbox/deck-adapter.d.ts +29 -0
  150. package/dist/clients/inbox/deck-adapter.js +62 -0
  151. package/dist/clients/inbox/layout.d.ts +8 -0
  152. package/dist/clients/inbox/layout.js +9 -0
  153. package/dist/clients/inbox/resolve.d.ts +10 -0
  154. package/dist/clients/inbox/resolve.js +34 -0
  155. package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +29 -0
  156. package/dist/clients/inbox/review/__tests__/remap.test.js +196 -0
  157. package/dist/clients/inbox/review/__tests__/review-core.test.js +341 -0
  158. package/dist/clients/inbox/review/anchors.d.ts +41 -0
  159. package/dist/clients/inbox/review/anchors.js +89 -0
  160. package/dist/clients/inbox/review/columns.d.ts +22 -0
  161. package/dist/clients/inbox/review/columns.js +40 -0
  162. package/dist/clients/inbox/review/comments-client.d.ts +66 -0
  163. package/dist/clients/inbox/review/comments-client.js +98 -0
  164. package/dist/clients/inbox/review/composition.d.ts +60 -0
  165. package/dist/clients/inbox/review/composition.js +278 -0
  166. package/dist/clients/inbox/review/document-region.d.ts +120 -0
  167. package/dist/clients/inbox/review/document-region.js +902 -0
  168. package/dist/clients/inbox/review/frame.d.ts +45 -0
  169. package/dist/clients/inbox/review/frame.js +215 -0
  170. package/dist/clients/inbox/review/keys.d.ts +67 -0
  171. package/dist/clients/inbox/review/keys.js +130 -0
  172. package/dist/clients/inbox/review/launch.d.ts +10 -0
  173. package/dist/clients/inbox/review/launch.js +129 -0
  174. package/dist/clients/inbox/review/live-source.d.ts +24 -0
  175. package/dist/clients/inbox/review/live-source.js +160 -0
  176. package/dist/clients/inbox/review/nvim.d.ts +13 -0
  177. package/dist/clients/inbox/review/nvim.js +42 -0
  178. package/dist/clients/inbox/review/remap.d.ts +51 -0
  179. package/dist/clients/inbox/review/remap.js +157 -0
  180. package/dist/clients/inbox/review/review-client.d.ts +30 -0
  181. package/dist/clients/inbox/review/review-client.js +107 -0
  182. package/dist/clients/inbox/review/roundtrip.d.ts +5 -0
  183. package/dist/clients/inbox/review/roundtrip.js +36 -0
  184. package/dist/clients/inbox/review/state.d.ts +143 -0
  185. package/dist/clients/inbox/review/state.js +282 -0
  186. package/dist/clients/inbox/review-adapter.d.ts +29 -0
  187. package/dist/clients/inbox/review-adapter.js +92 -0
  188. package/dist/clients/inbox/review-visibility.d.ts +5 -0
  189. package/dist/clients/inbox/review-visibility.js +19 -0
  190. package/dist/clients/inbox/surface.d.ts +19 -0
  191. package/dist/clients/inbox/surface.js +128 -0
  192. package/dist/clients/inbox/tui/ansi.d.ts +66 -0
  193. package/dist/clients/inbox/tui/ansi.js +317 -0
  194. package/dist/clients/inbox/tui/clipboard.d.ts +2 -0
  195. package/dist/clients/inbox/tui/clipboard.js +26 -0
  196. package/dist/clients/inbox/tui/input.d.ts +7 -0
  197. package/dist/clients/inbox/tui/input.js +653 -0
  198. package/dist/clients/inbox/tui/keys.d.ts +28 -0
  199. package/dist/clients/inbox/tui/keys.js +111 -0
  200. package/dist/clients/inbox/tui/panel.d.ts +2 -0
  201. package/dist/clients/inbox/tui/panel.js +217 -0
  202. package/dist/clients/inbox/tui/render.d.ts +37 -0
  203. package/dist/clients/inbox/tui/render.js +476 -0
  204. package/dist/clients/inbox/tui/types.d.ts +57 -0
  205. package/dist/clients/inbox/tui/types.js +1 -0
  206. package/dist/clients/inbox/tui.d.ts +6 -0
  207. package/dist/clients/inbox/tui.js +74 -0
  208. package/dist/clients/surfaces/host.d.ts +66 -0
  209. package/dist/clients/surfaces/host.js +1 -0
  210. package/dist/clients/surfaces/region.d.ts +16 -0
  211. package/dist/clients/surfaces/region.js +1 -0
  212. package/dist/clients/web/__tests__/push-engine.test.js +14 -158
  213. package/dist/clients/web/dev-server.d.ts +3 -3
  214. package/dist/clients/web/dev-server.js +9 -23
  215. package/dist/clients/web/events.js +4 -6
  216. package/dist/clients/web/push-engine.d.ts +2 -66
  217. package/dist/clients/web/push-engine.js +30 -161
  218. package/dist/clients/web/push-registry.d.ts +0 -4
  219. package/dist/clients/web/push-registry.js +2 -7
  220. package/dist/clients/web/server.js +3 -35
  221. package/dist/clients/web/web-client/shared/protocol.d.ts +0 -108
  222. package/dist/clients/web/web-cmd.js +2 -2
  223. package/dist/commands/__tests__/human.test.js +27 -15
  224. package/dist/commands/api-client.js +3 -3
  225. package/dist/commands/canvas-browse.js +1 -1
  226. package/dist/commands/canvas-issue.js +1 -1
  227. package/dist/commands/canvas-use.js +1 -1
  228. package/dist/commands/human/doc.d.ts +2 -0
  229. package/dist/commands/human/doc.js +98 -0
  230. package/dist/commands/human/inbox.d.ts +2 -0
  231. package/dist/commands/human/inbox.js +101 -0
  232. package/dist/commands/human/prompts.d.ts +2 -3
  233. package/dist/commands/human/prompts.js +27 -180
  234. package/dist/commands/human/queue.d.ts +1 -5
  235. package/dist/commands/human/queue.js +54 -216
  236. package/dist/commands/human/review.d.ts +2 -0
  237. package/dist/commands/human/review.js +298 -0
  238. package/dist/commands/human/shared.d.ts +2 -69
  239. package/dist/commands/human/shared.js +1 -181
  240. package/dist/commands/human.js +15 -20
  241. package/dist/commands/memory/__tests__/lint.test.d.ts +1 -0
  242. package/dist/commands/memory/__tests__/lint.test.js +15 -0
  243. package/dist/commands/memory/delete.js +2 -2
  244. package/dist/commands/memory/lint.d.ts +1 -0
  245. package/dist/commands/memory/lint.js +21 -3
  246. package/dist/commands/memory/read.js +4 -4
  247. package/dist/commands/node.js +8 -7
  248. package/dist/commands/pkg/plugin-manage.js +143 -68
  249. package/dist/commands/surface-inspect.js +2 -2
  250. package/dist/commands/surface.js +6 -12
  251. package/dist/commands/sys/__tests__/setup-core.test.js +9 -5
  252. package/dist/commands/sys/__tests__/setup-front-door.test.js +7 -42
  253. package/dist/commands/sys/config.js +29 -3
  254. package/dist/commands/sys/feedback.js +1 -1
  255. package/dist/commands/sys/setup-core.d.ts +12 -11
  256. package/dist/commands/sys/setup-core.js +55 -46
  257. package/dist/commands/sys/setup-wizard.d.ts +4 -1
  258. package/dist/commands/sys/setup-wizard.js +50 -9
  259. package/dist/commands/sys/sync-deps.js +1 -1
  260. package/dist/commands/sys.js +4 -6
  261. package/dist/core/__tests__/broker-sdk-wiring.test.js +6 -1
  262. package/dist/core/__tests__/broker-snapshot-history.test.js +9 -1
  263. package/dist/core/__tests__/dead-node-policy-table.test.js +3 -4
  264. package/dist/core/__tests__/fixtures/fake-engine.d.ts +8 -5
  265. package/dist/core/__tests__/fixtures/fake-engine.js +28 -2
  266. package/dist/core/__tests__/full/broker-provider-retry.test.js +87 -2
  267. package/dist/core/__tests__/full/broker-whip-interrupt.test.d.ts +1 -0
  268. package/dist/core/__tests__/full/broker-whip-interrupt.test.js +39 -0
  269. package/dist/core/__tests__/full/detach-focus.test.js +1 -1
  270. package/dist/core/__tests__/human-deliver-e2e.test.js +64 -64
  271. package/dist/core/__tests__/human-deliver.test.js +71 -345
  272. package/dist/core/__tests__/inline-memory-refs.test.js +2 -12
  273. package/dist/core/__tests__/memory-resolver-precedence.test.js +12 -15
  274. package/dist/core/__tests__/migration.test.js +10 -0
  275. package/dist/core/__tests__/model-pin-durability.test.js +14 -0
  276. package/dist/core/__tests__/phase4-review-migration.test.d.ts +1 -0
  277. package/dist/core/__tests__/phase4-review-migration.test.js +100 -0
  278. package/dist/core/__tests__/phase4-review-store.test.d.ts +1 -0
  279. package/dist/core/__tests__/phase4-review-store.test.js +143 -0
  280. package/dist/core/__tests__/remote-canvas-target.test.js +2 -2
  281. package/dist/core/__tests__/revive.test.js +22 -53
  282. package/dist/core/__tests__/session-cycles.test.js +48 -18
  283. package/dist/core/__tests__/spawn-no-kickoff.test.js +8 -12
  284. package/dist/core/__tests__/stop-guard.test.js +16 -1
  285. package/dist/core/__tests__/tmux-surface.test.js +21 -25
  286. package/dist/core/asset-root.d.ts +7 -0
  287. package/dist/core/asset-root.js +18 -0
  288. package/dist/core/broker-client/client.d.ts +0 -7
  289. package/dist/core/broker-client/client.js +4 -10
  290. package/dist/core/broker-client/transport-relay.d.ts +3 -4
  291. package/dist/core/broker-client/transport-relay.js +3 -4
  292. package/dist/core/canvas/__tests__/attention.test.js +1 -1
  293. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +1 -1
  294. package/dist/core/canvas/__tests__/remote-transport.test.d.ts +1 -0
  295. package/dist/core/{view/__tests__/transport-remote.test.js → canvas/__tests__/remote-transport.test.js} +2 -2
  296. package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
  297. package/dist/core/canvas/attention.js +4 -4
  298. package/dist/core/canvas/canvas.d.ts +6 -11
  299. package/dist/core/canvas/canvas.js +11 -33
  300. package/dist/core/canvas/db.js +207 -38
  301. package/dist/core/canvas/extensions.d.ts +1 -1
  302. package/dist/core/canvas/extensions.js +8 -1
  303. package/dist/core/canvas/index.d.ts +0 -1
  304. package/dist/core/canvas/index.js +0 -1
  305. package/dist/core/canvas/nav-model.d.ts +1 -1
  306. package/dist/core/canvas/nav-model.js +1 -1
  307. package/dist/core/canvas/paths.d.ts +5 -0
  308. package/dist/core/canvas/paths.js +16 -0
  309. package/dist/core/canvas/remote-canvas-source.js +2 -2
  310. package/dist/core/canvas/remote-transport.d.ts +7 -0
  311. package/dist/core/{view/transport-remote.js → canvas/remote-transport.js} +3 -4
  312. package/dist/core/canvas/source.js +1 -1
  313. package/dist/core/canvas/status-glyph.js +4 -12
  314. package/dist/core/canvas/types.d.ts +10 -35
  315. package/dist/core/clipboard-text.d.ts +8 -0
  316. package/dist/core/clipboard-text.js +18 -0
  317. package/dist/core/command-manifests/registry.d.ts +1 -3
  318. package/dist/core/command-plugins/bundle.d.ts +29 -0
  319. package/dist/core/command-plugins/bundle.js +198 -0
  320. package/dist/core/command-plugins/discovery.d.ts +0 -8
  321. package/dist/core/command-plugins/discovery.js +2 -39
  322. package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
  323. package/dist/core/command-plugins/transport/http-fetch.js +17 -95
  324. package/dist/core/command.d.ts +1 -13
  325. package/dist/core/command.js +1 -16
  326. package/dist/core/config.d.ts +4 -0
  327. package/dist/core/config.js +28 -3
  328. package/dist/core/document-lines.d.ts +4 -0
  329. package/dist/core/document-lines.js +6 -0
  330. package/dist/core/feed/feed.d.ts +2 -0
  331. package/dist/core/feed/feed.js +11 -7
  332. package/dist/core/human/__tests__/inbox-cancel-worker.d.ts +1 -0
  333. package/dist/core/human/__tests__/inbox-cancel-worker.js +7 -0
  334. package/dist/core/human/__tests__/inbox-claim-worker.d.ts +1 -0
  335. package/dist/core/human/__tests__/inbox-claim-worker.js +17 -0
  336. package/dist/core/human/__tests__/inbox-core.test.d.ts +1 -0
  337. package/dist/core/human/__tests__/inbox-core.test.js +144 -0
  338. package/dist/core/human/__tests__/inbox-stale-claim-worker.d.ts +1 -0
  339. package/dist/core/human/__tests__/inbox-stale-claim-worker.js +8 -0
  340. package/dist/core/human/__tests__/visible-paths.test.d.ts +1 -0
  341. package/dist/{clients/attach → core/human}/__tests__/visible-paths.test.js +9 -0
  342. package/dist/core/human/claim.d.ts +23 -0
  343. package/dist/core/human/claim.js +67 -0
  344. package/dist/core/human/convention.d.ts +30 -0
  345. package/dist/core/human/convention.js +202 -0
  346. package/dist/core/human/deck-factories.d.ts +8 -0
  347. package/dist/core/human/deck-factories.js +15 -0
  348. package/dist/core/human/deck-schema.d.ts +70 -0
  349. package/dist/core/human/deck-schema.js +92 -0
  350. package/dist/core/human/root.d.ts +7 -0
  351. package/dist/core/human/root.js +37 -0
  352. package/dist/core/human/scan.d.ts +3 -0
  353. package/dist/core/human/scan.js +76 -0
  354. package/dist/core/human/summary.d.ts +8 -0
  355. package/dist/core/human/summary.js +44 -0
  356. package/dist/core/human/tickets.d.ts +49 -0
  357. package/dist/core/human/tickets.js +190 -0
  358. package/dist/core/human/types.d.ts +136 -0
  359. package/dist/core/human/types.js +5 -0
  360. package/dist/{clients/attach → core/human}/visible-paths.d.ts +1 -1
  361. package/dist/{clients/attach → core/human}/visible-paths.js +3 -2
  362. package/dist/core/inspector/chrome.d.ts +6 -0
  363. package/dist/core/inspector/chrome.js +14 -0
  364. package/dist/core/inspector/contract.d.ts +105 -0
  365. package/dist/core/inspector/contract.js +1 -0
  366. package/dist/core/inspector/core.d.ts +4 -5
  367. package/dist/core/inspector/core.js +3 -5
  368. package/dist/core/inspector/host.d.ts +46 -0
  369. package/dist/core/{tui → inspector}/host.js +42 -208
  370. package/dist/core/inspector/text.d.ts +2 -2
  371. package/dist/core/inspector/transport.d.ts +6 -0
  372. package/dist/core/inspector/transport.js +36 -0
  373. package/dist/core/inspector/tui.d.ts +2 -2
  374. package/dist/core/inspector/tui.js +15 -15
  375. package/dist/core/keybindings/__tests__/inbox-affordance.test.js +11 -11
  376. package/dist/core/keybindings/__tests__/resolve.test.js +10 -19
  377. package/dist/core/keybindings/attach-control.d.ts +9 -3
  378. package/dist/core/keybindings/attach-control.js +3 -1
  379. package/dist/core/keybindings/catalog.d.ts +6 -5
  380. package/dist/core/keybindings/catalog.js +61 -133
  381. package/dist/core/keybindings/inbox.d.ts +5 -6
  382. package/dist/core/keybindings/inbox.js +9 -10
  383. package/dist/core/keybindings/index.d.ts +3 -3
  384. package/dist/core/keybindings/index.js +3 -3
  385. package/dist/core/keybindings/persistence.d.ts +5 -1
  386. package/dist/core/keybindings/persistence.js +24 -6
  387. package/dist/core/keybindings/resolve.d.ts +6 -0
  388. package/dist/core/keybindings/resolve.js +10 -3
  389. package/dist/core/memory/inline-ref-grammar.js +2 -3
  390. package/dist/core/memory-resolver.d.ts +27 -29
  391. package/dist/core/memory-resolver.js +45 -39
  392. package/dist/core/model-routes.d.ts +33 -0
  393. package/dist/core/model-routes.js +42 -3
  394. package/dist/core/preview-registry.js +17 -55
  395. package/dist/core/profiles/select.js +3 -3
  396. package/dist/core/{view/remote-canvas-target.d.ts → remote-canvas-target.d.ts} +2 -2
  397. package/dist/core/{view/remote-canvas-target.js → remote-canvas-target.js} +4 -4
  398. package/dist/core/review/__tests__/capture-origin.test.d.ts +1 -0
  399. package/dist/core/review/__tests__/capture-origin.test.js +139 -0
  400. package/dist/core/review/__tests__/comments.test.d.ts +1 -0
  401. package/dist/core/review/__tests__/comments.test.js +98 -0
  402. package/dist/core/review/__tests__/project.test.d.ts +1 -0
  403. package/dist/core/review/__tests__/project.test.js +71 -0
  404. package/dist/core/review/__tests__/remap.test.d.ts +1 -0
  405. package/dist/core/review/__tests__/remap.test.js +159 -0
  406. package/dist/core/review/__tests__/stage-identity.test.d.ts +1 -0
  407. package/dist/core/review/__tests__/stage-identity.test.js +140 -0
  408. package/dist/core/review/birth.d.ts +7 -0
  409. package/dist/core/review/birth.js +23 -0
  410. package/dist/core/review/comments.d.ts +69 -0
  411. package/dist/core/review/comments.js +295 -0
  412. package/dist/core/review/companion.d.ts +35 -0
  413. package/dist/core/review/companion.js +205 -0
  414. package/dist/core/review/document.d.ts +27 -0
  415. package/dist/core/review/document.js +138 -0
  416. package/dist/core/review/project.d.ts +13 -0
  417. package/dist/core/review/project.js +58 -0
  418. package/dist/core/review/realize.d.ts +19 -0
  419. package/dist/core/review/realize.js +217 -0
  420. package/dist/core/review/remap.d.ts +24 -0
  421. package/dist/core/review/remap.js +156 -0
  422. package/dist/core/review/signal.d.ts +5 -0
  423. package/dist/core/review/signal.js +31 -0
  424. package/dist/core/review/stage.d.ts +26 -0
  425. package/dist/core/review/stage.js +184 -0
  426. package/dist/core/review/store.d.ts +46 -0
  427. package/dist/core/review/store.js +163 -0
  428. package/dist/core/review/ticket-filter.d.ts +17 -0
  429. package/dist/core/review/ticket-filter.js +32 -0
  430. package/dist/core/review/types.d.ts +130 -0
  431. package/dist/core/review/types.js +23 -0
  432. package/dist/core/runtime/__tests__/session-visibility.test.d.ts +1 -0
  433. package/dist/core/runtime/__tests__/session-visibility.test.js +119 -0
  434. package/dist/core/runtime/bearings.d.ts +5 -17
  435. package/dist/core/runtime/bearings.js +88 -71
  436. package/dist/core/runtime/broker-protocol.d.ts +31 -8
  437. package/dist/core/runtime/broker.d.ts +15 -1
  438. package/dist/core/runtime/broker.js +573 -141
  439. package/dist/core/runtime/canvas-extensions.d.ts +4 -5
  440. package/dist/core/runtime/canvas-extensions.js +5 -5
  441. package/dist/core/runtime/front-door.js +4 -4
  442. package/dist/core/runtime/headless-pi.d.ts +24 -0
  443. package/dist/core/runtime/headless-pi.js +152 -0
  444. package/dist/core/runtime/kickoff.d.ts +1 -5
  445. package/dist/core/runtime/kickoff.js +1 -5
  446. package/dist/core/runtime/model-registry.d.ts +3 -0
  447. package/dist/core/runtime/model-registry.js +13 -0
  448. package/dist/core/runtime/naming-persist.d.ts +4 -0
  449. package/dist/core/runtime/naming-persist.js +27 -1
  450. package/dist/core/runtime/naming.d.ts +17 -12
  451. package/dist/core/runtime/naming.js +91 -140
  452. package/dist/core/runtime/node-read.js +19 -10
  453. package/dist/core/runtime/nodes.d.ts +9 -14
  454. package/dist/core/runtime/nodes.js +3 -53
  455. package/dist/core/runtime/package-health.d.ts +6 -21
  456. package/dist/core/runtime/package-health.js +12 -123
  457. package/dist/core/runtime/placement-tmux.d.ts +2 -1
  458. package/dist/core/runtime/placement-tmux.js +6 -4
  459. package/dist/core/runtime/placement.d.ts +1 -0
  460. package/dist/core/runtime/placement.js +4 -8
  461. package/dist/core/runtime/promote.js +3 -0
  462. package/dist/core/runtime/recap.d.ts +2 -7
  463. package/dist/core/runtime/recap.js +35 -69
  464. package/dist/core/runtime/revive.js +34 -77
  465. package/dist/core/runtime/session-cycles.d.ts +4 -0
  466. package/dist/core/runtime/session-cycles.js +14 -1
  467. package/dist/core/runtime/session-visibility.d.ts +37 -0
  468. package/dist/core/runtime/session-visibility.js +83 -0
  469. package/dist/core/runtime/spawn.js +10 -22
  470. package/dist/core/runtime/stop-guard.d.ts +1 -1
  471. package/dist/core/runtime/stop-guard.js +18 -13
  472. package/dist/core/runtime/surface-bg.js +1 -1
  473. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  474. package/dist/core/runtime/tmux-chrome.js +1 -1
  475. package/dist/core/runtime/tmux.d.ts +35 -7
  476. package/dist/core/runtime/tmux.js +439 -62
  477. package/dist/core/runtime/tool-group-summary.d.ts +19 -0
  478. package/dist/core/runtime/tool-group-summary.js +88 -0
  479. package/dist/core/scope.d.ts +2 -7
  480. package/dist/core/scope.js +9 -35
  481. package/dist/core/substrate/schema.d.ts +2 -2
  482. package/dist/core/termrender/code-doc.d.ts +12 -0
  483. package/dist/core/termrender/code-doc.js +92 -0
  484. package/dist/core/termrender/display.d.ts +12 -0
  485. package/dist/core/termrender/display.js +19 -0
  486. package/dist/core/termrender/termrender.d.ts +73 -0
  487. package/dist/core/termrender/termrender.js +795 -0
  488. package/dist/core/termrender/version.d.ts +1 -0
  489. package/dist/core/termrender/version.js +1 -0
  490. package/dist/core/tui/terminal.js +1 -2
  491. package/dist/core/user-settings.d.ts +71 -3
  492. package/dist/core/user-settings.js +83 -1
  493. package/dist/daemon/api/__tests__/inbox.test.js +44 -133
  494. package/dist/daemon/api/handlers/human.d.ts +0 -10
  495. package/dist/daemon/api/handlers/human.js +104 -702
  496. package/dist/daemon/api/handlers/inbox.js +113 -142
  497. package/dist/daemon/api/handlers/review-comments.d.ts +2 -0
  498. package/dist/daemon/api/handlers/review-comments.js +500 -0
  499. package/dist/daemon/api/handlers/reviews.d.ts +2 -0
  500. package/dist/daemon/api/handlers/reviews.js +287 -0
  501. package/dist/daemon/api/map.d.ts +22 -7
  502. package/dist/daemon/api/map.js +159 -11
  503. package/dist/daemon/api/server.js +4 -0
  504. package/dist/daemon/cron-run.js +6 -9
  505. package/dist/daemon/crtrd.js +17 -33
  506. package/dist/daemon/fleet.d.ts +1 -8
  507. package/dist/daemon/fleet.js +4 -30
  508. package/dist/daemon/human/finish.d.ts +14 -0
  509. package/dist/daemon/human/finish.js +115 -0
  510. package/dist/daemon/human/sweep.d.ts +6 -0
  511. package/dist/daemon/human/sweep.js +49 -0
  512. package/dist/daemon/messaging/node-message.d.ts +18 -0
  513. package/dist/daemon/messaging/node-message.js +70 -0
  514. package/dist/daemon/review/comment-notify.d.ts +26 -0
  515. package/dist/daemon/review/comment-notify.js +107 -0
  516. package/dist/daemon/review/deliver.d.ts +7 -0
  517. package/dist/daemon/review/deliver.js +56 -0
  518. package/dist/daemon/review/finish.d.ts +22 -0
  519. package/dist/daemon/review/finish.js +254 -0
  520. package/dist/daemon/review/sweep.d.ts +11 -0
  521. package/dist/daemon/review/sweep.js +120 -0
  522. package/dist/index.d.ts +2 -0
  523. package/dist/index.js +2 -2
  524. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +42 -64
  525. package/dist/pi-extensions/__tests__/canvas-recap.test.d.ts +1 -0
  526. package/dist/pi-extensions/__tests__/canvas-recap.test.js +159 -0
  527. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +16 -38
  528. package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
  529. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
  530. package/dist/pi-extensions/canvas-context-intro.d.ts +8 -11
  531. package/dist/pi-extensions/canvas-context-intro.js +50 -104
  532. package/dist/pi-extensions/canvas-recap.d.ts +15 -2
  533. package/dist/pi-extensions/canvas-recap.js +192 -69
  534. package/dist/pi-extensions/canvas-review-boundary.d.ts +59 -0
  535. package/dist/pi-extensions/canvas-review-boundary.js +81 -0
  536. package/dist/pi-extensions/canvas-stophook.d.ts +3 -3
  537. package/dist/pi-extensions/canvas-stophook.js +78 -128
  538. package/dist/pi-extensions/canvas-structured-output.js +52 -8
  539. package/dist/shared/generated-context.d.ts +29 -4
  540. package/dist/shared/generated-context.js +122 -7
  541. package/dist/shared/tool-groups.d.ts +23 -0
  542. package/dist/shared/tool-groups.js +60 -0
  543. package/dist/shared/working-activity.d.ts +8 -0
  544. package/dist/shared/working-activity.js +26 -0
  545. package/dist/types.d.ts +36 -2
  546. package/dist/types.js +51 -0
  547. package/dist/web-client/assets/index-CCPaLeW7.css +2 -0
  548. package/dist/web-client/assets/index-kyOWsnZK.js +86 -0
  549. package/dist/web-client/index.html +2 -2
  550. package/dist/web-client/sw.js +1 -1
  551. package/docs/compat/hearth-crtr-v4.md +2 -2
  552. package/docs/compat/hearth-crtr-v5.md +4 -2
  553. package/docs/public-api.md +3 -3
  554. package/package.json +21 -14
  555. package/runtime.lock.json +4661 -1453
  556. package/scripts/postinstall.mjs +15 -2
  557. package/dist/builtin-pi-packages/pi-mode-switch/README.md +0 -36
  558. package/dist/builtin-pi-packages/pi-mode-switch/bin/mode +0 -36
  559. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +0 -449
  560. package/dist/builtin-pi-packages/pi-mode-switch/package.json +0 -14
  561. package/dist/builtin-pi-packages/pi-mode-switch/tsconfig.json +0 -9
  562. package/dist/builtin-views/_lib/states.mjs +0 -161
  563. package/dist/builtin-views/canvas/core.mjs +0 -657
  564. package/dist/builtin-views/canvas/text.mjs +0 -58
  565. package/dist/builtin-views/canvas/tui.mjs +0 -168
  566. package/dist/builtin-views/canvas/web.jsx +0 -121
  567. package/dist/builtin-views/chat/core.mjs +0 -684
  568. package/dist/builtin-views/chat/text.mjs +0 -101
  569. package/dist/builtin-views/chat/tui.mjs +0 -360
  570. package/dist/builtin-views/chat/web.jsx +0 -362
  571. package/dist/builtin-views/git-pr/core.mjs +0 -673
  572. package/dist/builtin-views/git-pr/text.mjs +0 -84
  573. package/dist/builtin-views/git-pr/tui.mjs +0 -301
  574. package/dist/builtin-views/git-pr/web.jsx +0 -216
  575. package/dist/builtin-views/inbox/_lib/render.mjs +0 -175
  576. package/dist/builtin-views/inbox/core.mjs +0 -1273
  577. package/dist/builtin-views/inbox/text.mjs +0 -73
  578. package/dist/builtin-views/inbox/tui.mjs +0 -314
  579. package/dist/builtin-views/inbox/web.jsx +0 -188
  580. package/dist/builtin-views/linkedin/core.mjs +0 -906
  581. package/dist/builtin-views/linkedin/text.mjs +0 -69
  582. package/dist/builtin-views/linkedin/tui.mjs +0 -413
  583. package/dist/builtin-views/linkedin/web.jsx +0 -206
  584. package/dist/builtin-views/prompt-review/core.mjs +0 -735
  585. package/dist/builtin-views/prompt-review/text.mjs +0 -15
  586. package/dist/builtin-views/prompt-review/tui.mjs +0 -196
  587. package/dist/builtin-views/prompt-review/web.jsx +0 -484
  588. package/dist/builtin-views/workspace-sidebar/__tests__/core.test.js +0 -50
  589. package/dist/builtin-views/workspace-sidebar/__tests__/core.test.ts +0 -53
  590. package/dist/builtin-views/workspace-sidebar/core.mjs +0 -670
  591. package/dist/builtin-views/workspace-sidebar/text.mjs +0 -53
  592. package/dist/builtin-views/workspace-sidebar/tui.mjs +0 -141
  593. package/dist/builtin-views/workspace-sidebar/web.jsx +0 -109
  594. package/dist/clients/attach/session/mode.d.ts +0 -25
  595. package/dist/clients/attach/session/mode.js +0 -97
  596. package/dist/commands/sys/promptstudio.d.ts +0 -2
  597. package/dist/commands/sys/promptstudio.js +0 -65
  598. package/dist/commands/view-cycle.d.ts +0 -2
  599. package/dist/commands/view-cycle.js +0 -130
  600. package/dist/commands/view-list.d.ts +0 -2
  601. package/dist/commands/view-list.js +0 -66
  602. package/dist/commands/view-new.d.ts +0 -2
  603. package/dist/commands/view-new.js +0 -74
  604. package/dist/commands/view-pick.d.ts +0 -6
  605. package/dist/commands/view-pick.js +0 -129
  606. package/dist/commands/view-run.d.ts +0 -2
  607. package/dist/commands/view-run.js +0 -214
  608. package/dist/commands/view.d.ts +0 -2
  609. package/dist/commands/view.js +0 -31
  610. package/dist/commands/workspace.d.ts +0 -2
  611. package/dist/commands/workspace.js +0 -165
  612. package/dist/core/__tests__/chat-view-reconnect.test.js +0 -105
  613. package/dist/core/__tests__/full/consult-startup-failure.test.js +0 -124
  614. package/dist/core/canvas/__tests__/human-work-outbox.test.js +0 -123
  615. package/dist/core/canvas/human-work-outbox.d.ts +0 -73
  616. package/dist/core/canvas/human-work-outbox.js +0 -261
  617. package/dist/core/command-plugins/store.d.ts +0 -16
  618. package/dist/core/command-plugins/store.js +0 -64
  619. package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +0 -40
  620. package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
  621. package/dist/core/runtime/fault-recovery-nudge.js +0 -3
  622. package/dist/core/tui/__tests__/host-keybindings.test.js +0 -113
  623. package/dist/core/tui/host.d.ts +0 -66
  624. package/dist/core/view/__tests__/transport-cache.test.js +0 -62
  625. package/dist/core/view/bridge.d.ts +0 -10
  626. package/dist/core/view/bridge.js +0 -31
  627. package/dist/core/view/chrome.d.ts +0 -9
  628. package/dist/core/view/chrome.js +0 -22
  629. package/dist/core/view/contract.d.ts +0 -205
  630. package/dist/core/view/contract.js +0 -23
  631. package/dist/core/view/loader.d.ts +0 -31
  632. package/dist/core/view/loader.js +0 -188
  633. package/dist/core/view/stream-local.d.ts +0 -3
  634. package/dist/core/view/stream-local.js +0 -231
  635. package/dist/core/view/transport-cache.d.ts +0 -8
  636. package/dist/core/view/transport-cache.js +0 -38
  637. package/dist/core/view/transport-local.d.ts +0 -7
  638. package/dist/core/view/transport-local.js +0 -83
  639. package/dist/core/view/transport-remote.d.ts +0 -2
  640. package/dist/core/view/transport.d.ts +0 -10
  641. package/dist/core/view/transport.js +0 -15
  642. package/dist/pi-extensions/naming-tool.d.ts +0 -24
  643. package/dist/pi-extensions/naming-tool.js +0 -67
  644. package/dist/prompts/view.d.ts +0 -7
  645. package/dist/prompts/view.js +0 -172
  646. package/dist/web/ViewChrome.d.ts +0 -7
  647. package/dist/web/ViewChrome.js +0 -28
  648. package/dist/web/ViewPane.d.ts +0 -39
  649. package/dist/web/ViewPane.js +0 -48
  650. package/dist/web/index.d.ts +0 -8
  651. package/dist/web/index.js +0 -22
  652. package/dist/web/runtime.d.ts +0 -39
  653. package/dist/web/runtime.js +0 -232
  654. package/dist/web/states.d.ts +0 -24
  655. package/dist/web/states.js +0 -24
  656. package/dist/web/transport-http.d.ts +0 -5
  657. package/dist/web/transport-http.js +0 -34
  658. package/dist/web/transport-stream.d.ts +0 -3
  659. package/dist/web/transport-stream.js +0 -204
  660. package/dist/web-client/assets/index-U_NZ66VE.js +0 -79
  661. package/dist/web-client/assets/index-aaeu4adv.css +0 -2
  662. /package/dist/{builtin-views/workspace-sidebar/__tests__/core.test.d.ts → clients/attach/__tests__/file-review-focus.test.d.ts} +0 -0
  663. /package/dist/clients/attach/__tests__/{visible-paths.test.d.ts → group-activity.test.d.ts} +0 -0
  664. /package/dist/{core/__tests__/chat-view-reconnect.test.d.ts → clients/attach/__tests__/input-controller-extension-command.test.d.ts} +0 -0
  665. /package/dist/{core/__tests__/full/consult-startup-failure.test.d.ts → clients/attach/__tests__/session-identity-refresh.test.d.ts} +0 -0
  666. /package/dist/{core/canvas/__tests__/human-work-outbox.test.d.ts → clients/inbox/__tests__/inbox-controller.test.d.ts} +0 -0
  667. /package/dist/{core/keybindings/__tests__/bespoke-consumers.test.d.ts → clients/inbox/__tests__/mount-panel.test.d.ts} +0 -0
  668. /package/dist/{core/tui/__tests__/host-keybindings.test.d.ts → clients/inbox/review/__tests__/editor-roundtrip.test.d.ts} +0 -0
  669. /package/dist/{core/view/__tests__/transport-cache.test.d.ts → clients/inbox/review/__tests__/remap.test.d.ts} +0 -0
  670. /package/dist/{core/view/__tests__/transport-remote.test.d.ts → clients/inbox/review/__tests__/review-core.test.d.ts} +0 -0
@@ -7,11 +7,11 @@
7
7
  // spawns pi and never opens the session — it only consumes the relayed
8
8
  // `AgentSessionEvent` stream + the `welcome` snapshot over `view.sock`.
9
9
  //
10
- // Ownership boundary: ChatView owns the chat scroll `Container` it is handed,
11
- // plus an activity/status area pinned to the bottom of that container (working
12
- // spinner, compaction/retry indicators, inline status/error lines). It does NOT
13
- // own the editor, the footer, or the terminal title — those are chrome laid out
14
- // by the attach command (T7). Events that only affect that chrome
10
+ // Ownership boundary: ChatView owns the chat scroll `Container` it is handed.
11
+ // Its activity state (working spinner, compaction/retry indicators) is signaled
12
+ // to the viewer-owned fixed slot when one is supplied. It does NOT own the
13
+ // editor, the footer, or the terminal title — those are chrome laid out by the
14
+ // attach command (T7). Events that only affect that chrome
15
15
  // (queue_update / session_info_changed / thinking_level_changed) are forwarded
16
16
  // to the optional `onFooterEvent` sink instead of rendered here.
17
17
  //
@@ -26,25 +26,29 @@ import { attachMarkdownTheme } from '../config.js';
26
26
  import { ContextMessageComponent } from './context-message.js';
27
27
  import { CRTR_OUTPUT_CUSTOM_TYPE, CrtrOutputMessageComponent, createCrtrBashToolDefinition, } from './crtr-output.js';
28
28
  import { CRTR_CYCLE_DIVIDER_CUSTOM_TYPE } from '../../../core/runtime/session-cycles.js';
29
- import { generatedContextPresentation } from '../../../shared/generated-context.js';
29
+ import { generatedContextExpandedBody, generatedContextPresentation, isRuntimeRestartContinuation, } from '../../../shared/generated-context.js';
30
+ import { assistantVisibleText, isTrueUserMessage, normalizeToolGroupSummary, normalizeToolGroupSummaryRecord, } from '../../../shared/tool-groups.js';
30
31
  import { transformMessageDiagrams } from './diagram.js';
32
+ import { transformMessageHtml } from './html-markdown.js';
31
33
  import { styleAttachMarkdownHeadings, styleAttachMessageMarkdown, styleAttachSummaryMarkdown, } from './markdown-headings.js';
32
34
  import { createCrtrEditToolDefinition } from './edit-diff.js';
35
+ import { GroupActivityRecorder } from './group-activity.js';
36
+ import { GroupRecapComponent } from './group-recap.js';
33
37
  import { FoldedToolCallController, createCrtrPlainBashToolDefinition, createCrtrReadToolDefinition, createCrtrWriteToolDefinition, } from './tool-calls.js';
34
38
  import { ringCompletionBell } from '../chrome/completion-bell.js';
35
- import { extractFilePaths } from '../visible-paths.js';
39
+ import { extractFilePaths } from '#core/human/visible-paths';
36
40
  import { keepWhenCondensed, liveRegionStart } from './condensed-history.js';
37
41
  import { ViewerAssistantMessageComponent } from './assistant-message.js';
42
+ import { CompositeViewportSource, MeasuredContainer, } from './measured-container.js';
38
43
  import { readConfig, updateRawConfigAtomically } from '../../../core/config.js';
39
44
  import { DEFAULT_CONDENSED_HISTORY, DEFAULT_LIVE_CYCLES } from '../../../types.js';
45
+ import { DEFAULT_WORKING_ACTIVITY } from '../../../shared/working-activity.js';
40
46
  /** Theme-derived chrome colors (spinner / dim / error) are supplied by the
41
47
  * attach command via `ChatViewOptions.palette` (built from the live theme in
42
- * config.ts). pi does not re-export the raw `theme` instance, so when no
43
- * palette is passed (ChatView used standalone) these defaults stand in: a faint
44
- * dim and ANSI red, matching the palette's own fallbacks. The message components
48
+ * config.ts). pi does not re-export the raw `theme` instance, so standalone
49
+ * ChatViews receive the same semantic ANSI fallback palette. The message components
45
50
  * use the attach Markdown theme, which preserves the user's pi theme while making
46
51
  * fenced code visually distinct. */
47
- const defaultSpinnerStyle = (s) => `\x1b[2m${s}\x1b[22m`;
48
52
  const defaultDimStyle = (s) => `\x1b[2m${s}\x1b[22m`;
49
53
  /** How many catch-up messages `applySnapshot` builds between event-loop yields.
50
54
  * Keeps large non-streaming snapshots from monopolizing the main thread so
@@ -85,6 +89,27 @@ function readFoldFinishedTools() {
85
89
  return false;
86
90
  }
87
91
  }
92
+ /** Whether this attach should start folded tool groups on their broker-generated
93
+ * summaries. The setting is deliberately attach-local: the broker remains the
94
+ * single summary producer and every viewer independently chooses its display. */
95
+ function readSummarizeToolCalls() {
96
+ try {
97
+ return readConfig('user').summarize_tool_calls;
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ /** Whether folded tool recaps list their actual activity or use compact counts.
104
+ * The detailed form is the default; each attach reads its own presentation choice. */
105
+ function readDetailedToolRecaps() {
106
+ try {
107
+ return readConfig('user').detailed_tool_recaps;
108
+ }
109
+ catch {
110
+ return true;
111
+ }
112
+ }
88
113
  /** Persist the fold view setting to user config. Best-effort: a viewer must never
89
114
  * die because the config file is unwritable — the in-viewer toggle still applies. */
90
115
  function persistFoldFinishedTools(value) {
@@ -166,6 +191,8 @@ class FoldedToolSeparator extends Spacer {
166
191
  }
167
192
  }
168
193
  class MinimizableToolComponent extends ToolExecutionComponent {
194
+ /** Bound by ChatView at append time; pi's base component does not expose it. */
195
+ activityCallId;
169
196
  minimized = false;
170
197
  expandedState = false;
171
198
  foldController;
@@ -207,51 +234,35 @@ class MinimizableToolComponent extends ToolExecutionComponent {
207
234
  }
208
235
  }
209
236
  const defaultErrorStyle = (s) => `\x1b[31m${s}\x1b[39m`;
210
- /** Keep settled conversation history out of picker/editor render hot paths. The
211
- * root TUI still compares the cached lines, but it no longer re-renders every
212
- * markdown/tool component for each repeated navigation key. */
213
- class RenderCacheContainer extends Container {
214
- childCache = new Map();
215
- markDirty(component) {
216
- if (component)
217
- this.childCache.delete(component);
218
- else
219
- this.childCache.clear();
220
- }
221
- removeChild(component) {
222
- super.removeChild(component);
223
- this.childCache.delete(component);
224
- }
225
- clear() {
226
- super.clear();
227
- this.childCache.clear();
228
- }
229
- invalidate() {
230
- super.invalidate();
231
- this.childCache.clear();
232
- }
233
- render(width) {
234
- const lines = [];
235
- for (const child of this.children) {
236
- let cached = this.childCache.get(child);
237
- if (!cached || cached.width !== width) {
238
- cached = { width, lines: child.render(width) };
239
- this.childCache.set(child, cached);
240
- }
241
- lines.push(...cached.lines);
242
- }
243
- return lines;
244
- }
245
- }
237
+ const defaultAttachPalette = {
238
+ accent: (s) => `\x1b[33m${s}\x1b[39m`,
239
+ active: (s) => `\x1b[36m${s}\x1b[39m`,
240
+ info: (s) => `\x1b[34m${s}\x1b[39m`,
241
+ muted: defaultDimStyle,
242
+ faint: defaultDimStyle,
243
+ border: (s) => s,
244
+ surface: (s) => s,
245
+ bold: (s) => `\x1b[1m${s}\x1b[22m`,
246
+ error: defaultErrorStyle,
247
+ warning: (s) => `\x1b[33m${s}\x1b[39m`,
248
+ diffAdded: (s) => `\x1b[32m${s}\x1b[39m`,
249
+ diffRemoved: (s) => `\x1b[91m${s}\x1b[39m`,
250
+ bashMode: (s) => `\x1b[32m${s}\x1b[39m`,
251
+ bashModeAlt: (s) => `\x1b[92m${s}\x1b[39m`,
252
+ };
246
253
  export class ChatView {
247
254
  tui;
248
255
  /** The chat scroll area handed in by the attach command. */
249
256
  container;
250
257
  /** Settled history is cached independently from the live status area so picker
251
258
  * navigation does not re-render a long conversation on every key repeat. */
252
- historyContainer = new RenderCacheContainer();
253
- /** Activity/status area below history. Holds working/compaction/retry loaders. */
254
- statusContainer = new Container();
259
+ historyContainer = new MeasuredContainer();
260
+ /** Standalone ChatViews render activity inline; the attach viewer supplies
261
+ * `onActivityChange`, so its transcript excludes this container. */
262
+ statusContainer = new MeasuredContainer();
263
+ /** Optional banner, above history and status in the viewport's flat source. */
264
+ bannerContainer;
265
+ transcript;
255
266
  /** Where `append()` puts a component. Normally `historyContainer`; retargeted to
256
267
  * a nested container while condensed history is being built, so the same
257
268
  * message-rendering path serves both regions. */
@@ -267,17 +278,37 @@ export class ChatView {
267
278
  hiddenThinkingLabel;
268
279
  /** Ctrl+O: pi's two-state tool-output expansion. */
269
280
  toolOutputExpanded;
270
- /** The view setting (Alt+C → w → f): fold every SETTLED tool call to its single
281
+ /** The view setting (Alt+C → z → f): fold every SETTLED tool call to its single
271
282
  * call line. Independent of `toolOutputExpanded` — it survives an expand/collapse
272
283
  * and only ever applies to tool calls that are done. */
273
284
  foldSettledTools = readFoldFinishedTools();
285
+ /** Summary mode only changes Ctrl+O when both persistent settings are on. */
286
+ summarizeToolCalls = readSummarizeToolCalls();
287
+ detailedToolRecaps = readDetailedToolRecaps();
288
+ showGroupSummaries = this.summarizeToolCalls && this.foldSettledTools;
289
+ /** Groups exist only while building/rendering the full-fidelity live region. */
290
+ groups = [];
291
+ openGroup;
292
+ groupSummaries = new Map();
293
+ streamingWasGroupMember = false;
274
294
  onFooterEvent;
295
+ /** Viewer hook that places activity in its fixed slot, when available. */
296
+ onActivityChange;
275
297
  /** Theme-derived colorizers for the activity/status area. */
276
298
  spinnerStyle;
277
299
  dimStyle;
278
300
  errorStyle;
301
+ /** Palette shared with recap rows that render outside pi tool callbacks. */
302
+ recapPalette;
279
303
  /** Bold accent for the expanded crtr-context label. */
280
304
  labelStyle = (s) => `\x1b[1m${s}\x1b[22m`;
305
+ /** Raw broker messages retained beside their viewer components so a local
306
+ * Ctrl+O disclosure toggle can re-project settled Markdown without touching
307
+ * persisted/copyable source. Includes condensed-history components. */
308
+ assistantSources = new Map();
309
+ /** Undefined honors each `<details open>` attribute until Ctrl+O establishes
310
+ * one viewer-wide disclosure state. */
311
+ detailsExpandedOverride;
281
312
  /** The assistant message component currently being streamed (between
282
313
  * message_start and message_end for an assistant turn). */
283
314
  streamingComponent;
@@ -293,6 +324,10 @@ export class ChatView {
293
324
  bashComponent;
294
325
  /** The single active activity loader (working/compaction/retry), if any. */
295
326
  activityLoader;
327
+ /** Broker-selected normal label for this turn; compaction/retry temporarily
328
+ * replace it and restore it when the run remains active. */
329
+ workingActivity = DEFAULT_WORKING_ACTIVITY;
330
+ runActive = false;
296
331
  // -- Single-writer snapshot ordering -------------------------------------
297
332
  // A non-streaming catch-up yields to the event loop mid-build (so queued stdin
298
333
  // drains), which opens a window where later broker frames could otherwise
@@ -320,6 +355,12 @@ export class ChatView {
320
355
  transcriptTexts = [];
321
356
  /** Existing file paths found anywhere in the rendered transcript, newest first. */
322
357
  transcriptFilePaths = [];
358
+ /** Canonical copyable transcript blocks, in their rendered visual order. */
359
+ copyBlockRecords = [];
360
+ copyIndex = new Map();
361
+ /** Registrations made while building condensed history are collapsed onto its
362
+ * outer container once that container becomes a top-level viewport block. */
363
+ condensedCopyBlocks = new WeakMap();
323
364
  constructor(tui, container, opts = {}) {
324
365
  this.tui = tui;
325
366
  this.container = container;
@@ -332,21 +373,34 @@ export class ChatView {
332
373
  this.liveCycles = opts.liveCycles ?? readLiveCycles();
333
374
  this.condensedHistory = opts.condensedHistory ?? readCondensedHistory();
334
375
  this.onFooterEvent = opts.onFooterEvent;
335
- this.spinnerStyle = opts.palette?.active ?? defaultSpinnerStyle;
336
- this.dimStyle = opts.palette?.muted ?? defaultDimStyle;
337
- this.errorStyle = opts.palette?.error ?? defaultErrorStyle;
376
+ this.onActivityChange = opts.onActivityChange;
377
+ this.recapPalette = opts.palette ?? defaultAttachPalette;
378
+ this.spinnerStyle = this.recapPalette.active;
379
+ this.dimStyle = this.recapPalette.muted;
380
+ this.errorStyle = this.recapPalette.error;
338
381
  // Banner sits above history and OUTSIDE it, so resetChat() (which clears only
339
382
  // historyContainer) never wipes it — it persists across every welcome snapshot.
340
383
  if (opts.banner && opts.banner.length > 0) {
341
- const b = new Container();
342
- b.addChild(new Spacer(1));
384
+ const banner = new MeasuredContainer();
385
+ banner.addChild(new Spacer(1));
343
386
  for (const line of opts.banner)
344
- b.addChild(new Text(line, 1, 0));
345
- b.addChild(new Spacer(1));
346
- this.container.addChild(b);
387
+ banner.addChild(new Text(line, 1, 0));
388
+ banner.addChild(new Spacer(1));
389
+ this.bannerContainer = banner;
390
+ this.container.addChild(banner);
347
391
  }
348
392
  this.container.addChild(this.historyContainer);
349
- this.container.addChild(this.statusContainer);
393
+ if (!this.onActivityChange)
394
+ this.container.addChild(this.statusContainer);
395
+ this.transcript = new CompositeViewportSource([
396
+ ...(this.bannerContainer ? [this.bannerContainer] : []),
397
+ this.historyContainer,
398
+ ...(this.onActivityChange ? [] : [this.statusContainer]),
399
+ ]);
400
+ }
401
+ /** The transcript as one flat ordered block list for the viewport. */
402
+ transcriptSource() {
403
+ return this.transcript;
350
404
  }
351
405
  // -------------------------------------------------------------------------
352
406
  // Catch-up: render the welcome snapshot, then live events resume.
@@ -365,6 +419,11 @@ export class ChatView {
365
419
  this.applyingSnapshot = true;
366
420
  this.deferredFrames.length = 0;
367
421
  this.resetChat();
422
+ this.workingActivity = typeof snapshot.workingActivity === 'string' && snapshot.workingActivity.trim() !== ''
423
+ ? snapshot.workingActivity
424
+ : DEFAULT_WORKING_ACTIVITY;
425
+ this.runActive = snapshot.state.isStreaming;
426
+ this.seedToolGroupSummaries(snapshot.toolGroupSummaries);
368
427
  const renderedPendingTools = new Map();
369
428
  const messages = snapshot.messages;
370
429
  const lastIndex = messages.length - 1;
@@ -379,7 +438,7 @@ export class ChatView {
379
438
  if (generation !== this.snapshotGeneration)
380
439
  return;
381
440
  if (condensed)
382
- this.append(condensed);
441
+ this.appendCondensedBlock(condensed);
383
442
  }
384
443
  for (let i = liveStart; i < messages.length; i++) {
385
444
  // Yield to the event loop every SNAPSHOT_YIELD_EVERY messages so queued
@@ -401,7 +460,7 @@ export class ChatView {
401
460
  if (message.role === 'assistant') {
402
461
  // Keep the `/copy` source current from the catch-up transcript too, not
403
462
  // only the live stream (you usually attach AFTER the last reply landed).
404
- const text = this.userMessageText(message);
463
+ const text = assistantVisibleText(message);
405
464
  if (text.trim()) {
406
465
  this.lastAssistantText = text;
407
466
  this.pushTranscriptText(text);
@@ -416,42 +475,65 @@ export class ChatView {
416
475
  snapshot.state.isStreaming &&
417
476
  message.stopReason !== 'aborted' &&
418
477
  message.stopReason !== 'error';
478
+ const hasProse = assistantVisibleText(message).trim() !== '';
479
+ const toolCalls = this.assistantToolCalls(message);
480
+ const endedWithFailure = message.stopReason === 'aborted' || message.stopReason === 'error';
481
+ const terminalErrorMessage = message.stopReason === 'aborted'
482
+ ? 'Interrupted'
483
+ : (message.errorMessage ?? 'Error');
484
+ // A terminal assistant failure remains visible as its own boundary. Settle
485
+ // any interrupted calls so the completed span before it can still recap.
486
+ if (endedWithFailure) {
487
+ for (const component of renderedPendingTools.values()) {
488
+ const result = { content: [{ type: 'text', text: terminalErrorMessage }], isError: true };
489
+ component.updateResult(result);
490
+ this.registerCopyBlock(component, 'tool', terminalErrorMessage);
491
+ this.settleGroupTool(component, result, true);
492
+ }
493
+ renderedPendingTools.clear();
494
+ }
495
+ if (hasProse || endedWithFailure)
496
+ this.closeOpenGroup();
419
497
  let separator;
420
498
  if (isStreamingTail) {
421
499
  this.streamingComponent = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
422
- this.append(this.streamingComponent);
423
- this.streamingComponent.updateContent(styleAttachMessageMarkdown(message));
424
- separator = this.streamingToolSeparator = this.appendFoldedToolSeparator();
500
+ this.appendAssistantPart(this.streamingComponent, !hasProse);
501
+ this.registerCopyBlock(this.streamingComponent, 'assistant', '');
502
+ this.streamingWasGroupMember = !hasProse;
503
+ this.updateAssistantComponent(this.streamingComponent, message);
504
+ this.registerCopyBlock(this.streamingComponent, 'assistant', assistantVisibleText(message));
505
+ separator = this.streamingToolSeparator = this.appendAssistantSeparator(!hasProse);
425
506
  }
426
507
  else {
427
- separator = this.appendAssistantMessage(message);
508
+ separator = this.appendAssistantMessage(message, !hasProse && !endedWithFailure);
428
509
  }
429
- const toolCalls = this.assistantToolCalls(message);
430
510
  if (separator)
431
511
  this.setAssistantSeparatorContent(separator, message);
432
512
  for (const content of toolCalls) {
433
513
  this.pushTranscriptValue(content.arguments);
434
- const component = this.makeToolComponent(content.name, content.id, content.arguments);
435
- this.append(component);
514
+ const component = this.appendToolComponent(content.name, content.id, content.arguments);
436
515
  // (isStreamingTail is never aborted/error, so its tools fall through to
437
516
  // renderedPendingTools → pendingTools, where live tool events find them.)
438
517
  if (message.stopReason === 'aborted' || message.stopReason === 'error') {
439
- const errorMessage = message.stopReason === 'aborted'
440
- ? 'Interrupted'
441
- : (message.errorMessage ?? 'Error');
442
- component.updateResult({ content: [{ type: 'text', text: errorMessage }], isError: true });
518
+ component.updateResult({ content: [{ type: 'text', text: terminalErrorMessage }], isError: true });
519
+ this.registerCopyBlock(component, 'tool', terminalErrorMessage);
520
+ this.settleGroupTool(component, { content: [{ type: 'text', text: terminalErrorMessage }], isError: true }, true);
443
521
  }
444
522
  else {
445
523
  renderedPendingTools.set(content.id, component);
446
524
  }
447
525
  }
526
+ if (endedWithFailure)
527
+ this.closeOpenGroup();
448
528
  }
449
529
  else if (message.role === 'toolResult') {
450
530
  this.pushTranscriptText(this.userMessageText(message));
451
531
  const component = renderedPendingTools.get(message.toolCallId);
452
532
  if (component) {
453
533
  component.updateResult(message);
534
+ this.registerCopyBlock(component, 'tool', this.toolResultText(message));
454
535
  this.historyContainer.markDirty(component);
536
+ this.settleGroupTool(component, message, message.isError === true);
455
537
  renderedPendingTools.delete(message.toolCallId);
456
538
  }
457
539
  }
@@ -464,13 +546,12 @@ export class ChatView {
464
546
  }
465
547
  // A rebuild constructs fresh components, which are born unfolded; re-apply the
466
548
  // setting so a reconnect/welcome does not silently unfold the transcript.
467
- if (this.foldSettledTools)
468
- this.applyToolDisplay();
549
+ this.applyToolDisplay();
469
550
  // If we attached mid-turn, the `agent_start` that spins up the working loader
470
551
  // already fired before we connected — restore it from the snapshot's streaming
471
552
  // state so an actively-working node shows the spinner right away.
472
553
  if (snapshot.state.isStreaming)
473
- this.setActivity(this.makeLoader('Working...'));
554
+ this.setActivity(this.makeLoader(this.workingActivity));
474
555
  this.tui.requestRender();
475
556
  // Build complete and still current (a mismatch would have returned above):
476
557
  // open the gate and replay, in wire order, any live frames that arrived
@@ -492,7 +573,9 @@ export class ChatView {
492
573
  async buildCondensedBlock(messages, generation) {
493
574
  const condensed = new Container();
494
575
  const previousTarget = this.appendTarget;
576
+ const copyBlocks = [];
495
577
  this.appendTarget = condensed;
578
+ this.condensedCopyBlocks.set(condensed, copyBlocks);
496
579
  try {
497
580
  let built = 0;
498
581
  for (const message of messages) {
@@ -513,8 +596,9 @@ export class ChatView {
513
596
  }
514
597
  finally {
515
598
  this.appendTarget = previousTarget;
599
+ this.condensedCopyBlocks.delete(condensed);
516
600
  }
517
- return condensed.children.length > 0 ? condensed : undefined;
601
+ return condensed.children.length > 0 ? { component: condensed, copyBlocks } : undefined;
518
602
  }
519
603
  /** Defer a live broker frame when a snapshot build is in flight. Returns true
520
604
  * (and enqueues the replay closure) while building so the caller returns
@@ -533,14 +617,26 @@ export class ChatView {
533
617
  for (const replay of frames)
534
618
  replay();
535
619
  }
620
+ /** Accept the broker's per-turn random choice before its following agent_start. */
621
+ setWorkingActivity(activity) {
622
+ if (this.deferIfBuilding(() => this.setWorkingActivity(activity)))
623
+ return;
624
+ this.workingActivity = activity.trim() === '' ? DEFAULT_WORKING_ACTIVITY : activity;
625
+ }
536
626
  /** Clear live turn state after the broker reports completion or the viewer observes dormancy. */
537
627
  settleRun() {
628
+ this.runActive = false;
629
+ for (const component of this.pendingTools.values())
630
+ this.settleGroupTool(component);
538
631
  if (this.streamingComponent) {
539
632
  this.historyContainer.removeChild(this.streamingComponent);
633
+ this.unregisterCopyBlock(this.streamingComponent);
634
+ this.assistantSources.delete(this.streamingComponent);
540
635
  this.streamingComponent = undefined;
541
636
  }
542
637
  this.streamingToolSeparator = undefined;
543
638
  this.pendingTools.clear();
639
+ this.applyToolDisplay();
544
640
  this.setActivity(undefined);
545
641
  this.settleBashComponent();
546
642
  }
@@ -555,36 +651,54 @@ export class ChatView {
555
651
  // Pure client-side render state — no broker round-trip — ported from pi's
556
652
  // `setToolsExpanded` / `toggleThinkingBlockVisibility`.
557
653
  // -------------------------------------------------------------------------
558
- /** Ctrl+O / app.tools.expand — flip global tool-output expansion and apply it to
559
- * every rendered component that supports it. Returns the new expanded state so
560
- * the caller can surface a notice. Condensed cycles hold no tool components, so
561
- * they are unaffected by design. */
654
+ /** Ctrl+O prefers summaries when the user enabled both folding and summary
655
+ * generation. The same disclosure state drives assistant-authored `<details>`
656
+ * sections, so the transcript has one reveal-more action rather than two. */
562
657
  toggleToolsExpanded() {
563
- this.toolOutputExpanded = !this.toolOutputExpanded;
658
+ let result;
659
+ if (this.summarizeToolCalls && this.foldSettledTools) {
660
+ this.showGroupSummaries = !this.showGroupSummaries;
661
+ this.toolOutputExpanded = !this.showGroupSummaries;
662
+ result = { kind: 'summaries', shown: this.showGroupSummaries };
663
+ }
664
+ else {
665
+ this.toolOutputExpanded = !this.toolOutputExpanded;
666
+ result = { kind: 'output', expanded: this.toolOutputExpanded };
667
+ }
668
+ this.detailsExpandedOverride = this.toolOutputExpanded;
669
+ this.rerenderAssistantMessages();
564
670
  this.applyToolDisplay();
565
- return this.toolOutputExpanded;
671
+ return result;
566
672
  }
567
- /** The `fold finished tool calls` view setting (`/fold-tools`, Alt+C → w → f).
673
+ /** The `fold finished tool calls` view setting (`/fold-tools`, Alt+C → z → f).
568
674
  * Returns the new state so the caller can surface a notice. */
569
675
  toggleFoldSettledTools() {
570
676
  this.foldSettledTools = !this.foldSettledTools;
571
677
  persistFoldFinishedTools(this.foldSettledTools);
678
+ // Turning folding back on restores the configured default (summaries),
679
+ // rather than preserving a stale Ctrl+O choice from an unfolded view.
680
+ if (this.summarizeToolCalls && this.foldSettledTools)
681
+ this.showGroupSummaries = true;
572
682
  this.applyToolDisplay();
573
683
  return this.foldSettledTools;
574
684
  }
575
685
  /** Push both display settings onto every rendered component. A tool call still in
576
- * `pendingTools` is in flight, so it is never folded — you keep watching live
577
- * output, and it folds on its own once `tool_execution_end` settles it. */
686
+ * flight is never hidden or minimized. Only a settled group with a broker-known
687
+ * summary collapses; incomplete/failed generation leaves its folded call lines. */
578
688
  applyToolDisplay() {
579
689
  const inFlight = new Set(this.pendingTools.values());
580
- this.updateFoldedToolSeparators(inFlight);
690
+ for (const group of this.groups)
691
+ for (const tool of group.inFlight)
692
+ inFlight.add(tool);
581
693
  for (const child of this.historyContainer.children) {
582
- if (child instanceof MinimizableToolComponent) {
694
+ if (child instanceof MinimizableToolComponent)
583
695
  child.setMinimized(this.foldSettledTools && !inFlight.has(child));
584
- }
585
696
  if (isExpandable(child))
586
697
  child.setExpanded(this.toolOutputExpanded);
587
698
  }
699
+ for (const group of this.groups)
700
+ this.applyGroupDisplay(group);
701
+ this.updateFoldedToolSeparators(inFlight);
588
702
  this.historyContainer.markDirty();
589
703
  this.tui.requestRender();
590
704
  }
@@ -615,8 +729,11 @@ export class ChatView {
615
729
  return;
616
730
  switch (event.type) {
617
731
  case 'agent_start': {
732
+ this.runActive = true;
733
+ for (const component of this.pendingTools.values())
734
+ this.settleGroupTool(component);
618
735
  this.pendingTools.clear();
619
- this.setActivity(this.makeLoader('Working...'));
736
+ this.setActivity(this.makeLoader(this.workingActivity));
620
737
  break;
621
738
  }
622
739
  case 'agent_end': {
@@ -644,23 +761,28 @@ export class ChatView {
644
761
  this.addMessageToChat(event.message);
645
762
  }
646
763
  else if (event.message.role === 'assistant') {
764
+ const hasProse = assistantVisibleText(event.message).trim() !== '';
765
+ if (hasProse)
766
+ this.closeOpenGroup();
647
767
  this.streamingComponent = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
648
- this.append(this.streamingComponent);
649
- this.streamingComponent.updateContent(styleAttachMessageMarkdown(event.message));
650
- this.streamingToolSeparator = this.appendFoldedToolSeparator();
768
+ this.streamingWasGroupMember = !hasProse;
769
+ this.appendAssistantPart(this.streamingComponent, !hasProse);
770
+ this.registerCopyBlock(this.streamingComponent, 'assistant', '');
771
+ this.updateAssistantComponent(this.streamingComponent, event.message);
772
+ this.registerCopyBlock(this.streamingComponent, 'assistant', assistantVisibleText(event.message));
773
+ this.streamingToolSeparator = this.appendAssistantSeparator(!hasProse);
651
774
  this.setAssistantSeparatorContent(this.streamingToolSeparator, event.message);
652
775
  }
653
776
  break;
654
777
  }
655
778
  case 'message_update': {
656
779
  if (this.streamingComponent && event.message.role === 'assistant') {
657
- // Render any ALREADY-COMPLETE diagram fence to a diagram mid-stream
658
- // (transformMessageDiagrams only matches closed fences and is memoized,
659
- // so an unclosed fence still streaming stays raw and settles later).
660
- // This makes a diagram pop the instant its closing ``` arrives instead
661
- // of waiting for message_end — noticeable when more text follows it.
662
- const updated = styleAttachMessageMarkdown(transformMessageDiagrams(event.message, this.renderWidth()));
663
- this.streamingComponent.updateContent(updated);
780
+ this.promoteStreamingAssistantToBoundary(event.message);
781
+ // Upgrade complete rich-Markdown constructs mid-stream. Incomplete
782
+ // Mermaid fences and HTML pairs remain literal until their closing
783
+ // marker arrives, then this same settled-message pipeline projects them.
784
+ this.updateAssistantComponent(this.streamingComponent, event.message);
785
+ this.registerCopyBlock(this.streamingComponent, 'assistant', assistantVisibleText(event.message));
664
786
  this.historyContainer.markDirty(this.streamingComponent);
665
787
  const toolCalls = this.assistantToolCalls(event.message);
666
788
  if (this.streamingToolSeparator)
@@ -670,11 +792,11 @@ export class ChatView {
670
792
  const existing = this.pendingTools.get(content.id);
671
793
  if (existing) {
672
794
  existing.updateArgs(content.arguments);
795
+ this.noteGroupTool(existing, content.name, content.arguments);
673
796
  this.historyContainer.markDirty(existing);
674
797
  }
675
798
  else {
676
- const component = this.makeToolComponent(content.name, content.id, content.arguments);
677
- this.append(component);
799
+ const component = this.appendToolComponent(content.name, content.id, content.arguments);
678
800
  this.pendingTools.set(content.id, component);
679
801
  }
680
802
  }
@@ -684,10 +806,16 @@ export class ChatView {
684
806
  case 'message_end': {
685
807
  if (event.message.role !== 'assistant')
686
808
  break;
809
+ const endedWithFailure = event.message.stopReason === 'aborted' || event.message.stopReason === 'error';
810
+ // A terminal failure is always its own visible boundary, including an
811
+ // empty abort created by yield or a daemon restart.
812
+ this.promoteStreamingAssistantToBoundary(event.message, endedWithFailure);
687
813
  // Stash the completed reply's text for `/copy` (skip aborted/error turns,
688
814
  // whose text is partial or absent).
815
+ if (this.streamingComponent)
816
+ this.registerCopyBlock(this.streamingComponent, 'assistant', assistantVisibleText(event.message));
689
817
  if (event.message.stopReason !== 'aborted' && event.message.stopReason !== 'error') {
690
- const text = this.userMessageText(event.message);
818
+ const text = assistantVisibleText(event.message);
691
819
  if (text.trim()) {
692
820
  this.lastAssistantText = text;
693
821
  this.pushTranscriptText(text);
@@ -696,10 +824,9 @@ export class ChatView {
696
824
  if (this.streamingComponent) {
697
825
  const stopReason = event.message.stopReason;
698
826
  let errorMessage = event.message.errorMessage;
699
- // An aborted turn is a user interrupt (Esc). The provider tends to
700
- // attach its own generic text ("Request was aborted."), so override it
701
- // outright — don't gate on the absence of an errorMessage — with a
702
- // plain, human label.
827
+ // An aborted turn may be a user interrupt or a lifecycle interruption.
828
+ // The provider tends to attach its own generic text ("Request was
829
+ // aborted."), so override it outright with one plain, human label.
703
830
  if (stopReason === 'aborted') {
704
831
  errorMessage = 'Interrupted';
705
832
  // Surface the abort on the assistant bubble itself, mirroring pi
@@ -707,14 +834,14 @@ export class ChatView {
707
834
  // updateContent) so the rendered message shows the annotation.
708
835
  event.message.errorMessage = errorMessage;
709
836
  }
710
- // Render every supported complete diagram fence in the settled reply
711
- // before it reaches the markdown renderer (no-op when absent).
712
- const finalMessage = styleAttachMessageMarkdown(transformMessageDiagrams(event.message, this.renderWidth()));
713
- this.streamingComponent.updateContent(finalMessage);
837
+ // Project every supported rich-Markdown construct in the settled reply.
838
+ this.updateAssistantComponent(this.streamingComponent, event.message);
714
839
  if (stopReason === 'aborted' || stopReason === 'error') {
715
840
  const text = errorMessage ?? 'Error';
716
841
  for (const component of this.pendingTools.values()) {
717
842
  component.updateResult({ content: [{ type: 'text', text }], isError: true });
843
+ this.registerCopyBlock(component, 'tool', text);
844
+ this.settleGroupTool(component, { content: [{ type: 'text', text }], isError: true }, true);
718
845
  this.historyContainer.markDirty(component);
719
846
  }
720
847
  this.pendingTools.clear();
@@ -730,16 +857,24 @@ export class ChatView {
730
857
  this.streamingComponent = undefined;
731
858
  this.streamingToolSeparator = undefined;
732
859
  }
860
+ if (endedWithFailure) {
861
+ this.closeOpenGroup();
862
+ // A summary normally follows this terminal frame, but re-evaluate now
863
+ // too so an early recap immediately folds the tools before this seam.
864
+ this.applyToolDisplay();
865
+ }
733
866
  break;
734
867
  }
735
868
  case 'tool_execution_start': {
736
869
  this.pushTranscriptValue(event.args);
737
870
  let component = this.pendingTools.get(event.toolCallId);
738
871
  if (!component) {
739
- component = this.makeToolComponent(event.toolName, event.toolCallId, event.args);
740
- this.append(component);
872
+ component = this.appendToolComponent(event.toolName, event.toolCallId, event.args);
741
873
  this.pendingTools.set(event.toolCallId, component);
742
874
  }
875
+ else {
876
+ this.noteGroupTool(component, event.toolName, event.args);
877
+ }
743
878
  component.markExecutionStarted();
744
879
  this.historyContainer.markDirty(component);
745
880
  break;
@@ -749,6 +884,7 @@ export class ChatView {
749
884
  const component = this.pendingTools.get(event.toolCallId);
750
885
  if (component) {
751
886
  component.updateResult({ ...event.partialResult, isError: false }, true);
887
+ this.registerCopyBlock(component, 'tool', this.toolResultText(event.partialResult));
752
888
  this.historyContainer.markDirty(component);
753
889
  }
754
890
  break;
@@ -758,11 +894,10 @@ export class ChatView {
758
894
  const component = this.pendingTools.get(event.toolCallId);
759
895
  if (component) {
760
896
  component.updateResult({ ...event.result, isError: event.isError });
897
+ this.registerCopyBlock(component, 'tool', this.toolResultText(event.result));
761
898
  this.pendingTools.delete(event.toolCallId);
762
- // Settled → it now obeys the fold setting it was exempt from while live.
763
- if (this.foldSettledTools)
764
- component.setMinimized(true);
765
- this.updateFoldedToolSeparators();
899
+ this.settleGroupTool(component, event.result, event.isError);
900
+ this.applyToolDisplay();
766
901
  this.historyContainer.markDirty(component);
767
902
  }
768
903
  break;
@@ -773,7 +908,7 @@ export class ChatView {
773
908
  break;
774
909
  }
775
910
  case 'compaction_end': {
776
- this.setActivity(undefined);
911
+ this.setActivity(this.runActive ? this.makeLoader(this.workingActivity) : undefined);
777
912
  if (event.aborted) {
778
913
  this.showStatus(event.reason === 'manual' ? 'Compaction cancelled' : 'Auto-compaction cancelled');
779
914
  }
@@ -804,7 +939,7 @@ export class ChatView {
804
939
  break;
805
940
  }
806
941
  case 'auto_retry_end': {
807
- this.setActivity(undefined);
942
+ this.setActivity(this.runActive ? this.makeLoader(this.workingActivity) : undefined);
808
943
  if (!event.success) {
809
944
  this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError ?? 'Unknown error'}`);
810
945
  }
@@ -822,7 +957,7 @@ export class ChatView {
822
957
  break;
823
958
  }
824
959
  // No unconditional markDirty here: appends/removes invalidate through
825
- // RenderCacheContainer, and branches that mutate a child in place explicitly
960
+ // MeasuredContainer, and branches that mutate a child in place explicitly
826
961
  // mark it dirty. Activity-only events leave settled history cached while the
827
962
  // independent status loader animates.
828
963
  this.tui.requestRender();
@@ -836,6 +971,7 @@ export class ChatView {
836
971
  bashStart(command, excludeFromContext) {
837
972
  if (this.deferIfBuilding(() => this.bashStart(command, excludeFromContext)))
838
973
  return;
974
+ this.closeOpenGroup();
839
975
  this.settleBashComponent();
840
976
  let component;
841
977
  component = new BashExecutionComponent(command, this.componentTui(() => component), excludeFromContext);
@@ -872,11 +1008,27 @@ export class ChatView {
872
1008
  if (generated !== null &&
873
1009
  (message.role !== 'custom' || message.display === true)) {
874
1010
  this.pushTranscriptText(generated.body);
875
- this.append(new ContextMessageComponent(generated.body, this.toolOutputExpanded, this.dimStyle, (s) => this.spinnerStyle(this.labelStyle(s)), generated.label, ` ${generated.summary} — ctrl+o to expand`));
1011
+ const component = new ContextMessageComponent(generatedContextExpandedBody(generated), this.toolOutputExpanded, this.dimStyle, (s) => this.spinnerStyle(this.labelStyle(s)), generated.label, ` ${generated.summary} — ctrl+o to expand`, generated.label === 'crtr context' || generated.kind === 'revive' || generated.kind === 'inbox');
1012
+ // A successor broker's exact restart continuation is a visible seam, not
1013
+ // a group member: the preceding span keeps its own recap and the next tool
1014
+ // call begins a broker-aligned fresh group.
1015
+ if (isRuntimeRestartContinuation(message)) {
1016
+ this.closeOpenGroup();
1017
+ this.append(component);
1018
+ // Bearings and revive kickoffs only belong to an already-open run; they
1019
+ // never create a summary block by themselves. Other injected context may.
1020
+ }
1021
+ else if (generated.kind === 'bearings' || generated.kind === 'revive') {
1022
+ this.appendExistingGroupMember(component);
1023
+ }
1024
+ else {
1025
+ this.appendGroupMember(component);
1026
+ }
876
1027
  return;
877
1028
  }
878
1029
  switch (message.role) {
879
1030
  case 'bashExecution': {
1031
+ this.closeOpenGroup();
880
1032
  this.pushTranscriptText(message.output ?? '');
881
1033
  const component = new BashExecutionComponent(message.command, this.tui, message.excludeFromContext);
882
1034
  if (message.output)
@@ -894,7 +1046,7 @@ export class ChatView {
894
1046
  if (message.display) {
895
1047
  this.pushTranscriptText(this.userMessageText(message));
896
1048
  if (message.customType === CRTR_OUTPUT_CUSTOM_TYPE) {
897
- this.append(new CrtrOutputMessageComponent(this.userMessageText(message), this.toolOutputExpanded, this.crtrOutputTheme()));
1049
+ this.appendGroupMember(new CrtrOutputMessageComponent(this.userMessageText(message), this.toolOutputExpanded, this.crtrOutputTheme()));
898
1050
  break;
899
1051
  }
900
1052
  // A refresh-yield cycle boundary (session-cycles.ts, synthetic — never
@@ -902,6 +1054,7 @@ export class ChatView {
902
1054
  // message card, so scrolling through a multi-cycle node's full history
903
1055
  // reads as one conversation with clear seams between cycles.
904
1056
  if (message.customType === CRTR_CYCLE_DIVIDER_CUSTOM_TYPE) {
1057
+ this.closeOpenGroup();
905
1058
  this.append(new Spacer(1));
906
1059
  this.append(new Text(this.dimStyle(`── ${this.userMessageText(message)} ──`), 1, 0));
907
1060
  this.append(new Spacer(1));
@@ -909,11 +1062,12 @@ export class ChatView {
909
1062
  }
910
1063
  const component = new CustomMessageComponent(styleAttachMessageMarkdown(message), undefined, attachMarkdownTheme());
911
1064
  component.setExpanded(this.toolOutputExpanded);
912
- this.append(component);
1065
+ this.appendGroupMember(component);
913
1066
  }
914
1067
  break;
915
1068
  }
916
1069
  case 'compactionSummary': {
1070
+ this.closeOpenGroup();
917
1071
  this.append(new Spacer(1));
918
1072
  const component = new CompactionSummaryMessageComponent(styleAttachSummaryMarkdown(message), attachMarkdownTheme());
919
1073
  component.setExpanded(this.toolOutputExpanded);
@@ -921,35 +1075,47 @@ export class ChatView {
921
1075
  break;
922
1076
  }
923
1077
  case 'branchSummary': {
1078
+ this.closeOpenGroup();
924
1079
  this.append(new Spacer(1));
925
1080
  const component = new BranchSummaryMessageComponent(styleAttachSummaryMarkdown(message), attachMarkdownTheme());
926
1081
  component.setExpanded(this.toolOutputExpanded);
927
1082
  this.append(component);
1083
+ this.registerCopyBlock(component, 'assistant', message.summary);
928
1084
  break;
929
1085
  }
930
1086
  case 'user': {
1087
+ if (isTrueUserMessage(message))
1088
+ this.closeOpenGroup();
931
1089
  const textContent = this.userMessageText(message);
932
1090
  if (!textContent)
933
1091
  break;
934
1092
  this.pushTranscriptText(textContent);
935
1093
  if (this.chatChildCount() > 0)
936
- this.append(new Spacer(1));
1094
+ this.appendUserPart(new Spacer(1), !isTrueUserMessage(message));
937
1095
  const skillBlock = parseSkillBlock(textContent);
938
1096
  if (skillBlock) {
939
1097
  const component = new SkillInvocationMessageComponent({ ...skillBlock, content: styleAttachMarkdownHeadings(skillBlock.content) }, attachMarkdownTheme());
940
1098
  component.setExpanded(this.toolOutputExpanded);
941
- this.append(component);
1099
+ this.appendUserPart(component, !isTrueUserMessage(message));
1100
+ this.registerCopyBlock(component, 'user', textContent);
942
1101
  if (skillBlock.userMessage) {
943
- this.append(new UserMessageComponent(styleAttachMarkdownHeadings(skillBlock.userMessage), attachMarkdownTheme()));
1102
+ const userMessage = new UserMessageComponent(styleAttachMarkdownHeadings(skillBlock.userMessage), attachMarkdownTheme());
1103
+ this.appendUserPart(userMessage, !isTrueUserMessage(message));
1104
+ this.registerCopyBlock(userMessage, 'user', skillBlock.userMessage);
944
1105
  }
945
1106
  }
946
1107
  else {
947
- this.append(new UserMessageComponent(styleAttachMarkdownHeadings(textContent), attachMarkdownTheme()));
1108
+ const component = new UserMessageComponent(styleAttachMarkdownHeadings(textContent), attachMarkdownTheme());
1109
+ this.appendUserPart(component, !isTrueUserMessage(message));
1110
+ this.registerCopyBlock(component, 'user', textContent);
948
1111
  }
949
1112
  break;
950
1113
  }
951
1114
  case 'assistant': {
952
- this.appendAssistantMessage(message);
1115
+ const hasProse = assistantVisibleText(message).trim() !== '';
1116
+ if (hasProse)
1117
+ this.closeOpenGroup();
1118
+ this.appendAssistantMessage(message, !hasProse);
953
1119
  break;
954
1120
  }
955
1121
  // toolResult is rendered inline with its tool call (handled in applySnapshot).
@@ -961,19 +1127,176 @@ export class ChatView {
961
1127
  // Helpers
962
1128
  // -------------------------------------------------------------------------
963
1129
  /** Render an assistant reply plus its initially-hidden boundary separator. */
964
- appendAssistantMessage(message) {
965
- this.append(new ViewerAssistantMessageComponent(styleAttachMessageMarkdown(transformMessageDiagrams(message, this.renderWidth())), this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel));
966
- const separator = this.appendFoldedToolSeparator();
1130
+ appendAssistantMessage(message, groupMember = false) {
1131
+ const component = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
1132
+ this.updateAssistantComponent(component, message);
1133
+ this.appendAssistantPart(component, groupMember);
1134
+ this.registerCopyBlock(component, 'assistant', assistantVisibleText(message));
1135
+ const separator = this.appendAssistantSeparator(groupMember);
967
1136
  this.setAssistantSeparatorContent(separator, message);
968
1137
  return separator;
969
1138
  }
970
1139
  /** Add a separator in transcript order now; it renders only after a following
971
1140
  * tool settles in the folded view. */
972
- appendFoldedToolSeparator() {
1141
+ appendAssistantSeparator(groupMember = false) {
973
1142
  const separator = new FoldedToolSeparator();
974
- this.append(separator);
1143
+ if (groupMember)
1144
+ this.appendGroupMember(separator);
1145
+ else
1146
+ this.append(separator);
975
1147
  return separator;
976
1148
  }
1149
+ appendAssistantPart(component, groupMember) {
1150
+ if (groupMember)
1151
+ this.appendGroupMember(component);
1152
+ else
1153
+ this.append(component);
1154
+ }
1155
+ appendUserPart(component, groupMember) {
1156
+ if (groupMember)
1157
+ this.appendGroupMember(component);
1158
+ else
1159
+ this.append(component);
1160
+ }
1161
+ /** Only the full-fidelity trailing region gets summary groups. Condensed
1162
+ * history intentionally drops tool traffic, so grouping there would create
1163
+ * phantom summary rows and cross the live/condensed boundary. */
1164
+ isLiveGroupRegion() {
1165
+ return this.appendTarget === this.historyContainer;
1166
+ }
1167
+ ensureGroup() {
1168
+ if (!this.isLiveGroupRegion())
1169
+ return undefined;
1170
+ if (this.openGroup)
1171
+ return this.openGroup;
1172
+ const recap = new GroupRecapComponent();
1173
+ const group = {
1174
+ members: [],
1175
+ recap,
1176
+ activity: new GroupActivityRecorder(this.cwd),
1177
+ toolCount: 0,
1178
+ inFlight: new Set(),
1179
+ };
1180
+ this.append(recap);
1181
+ // The recap is a direct history child for cache identity, but zero-row and
1182
+ // hidden until a settled broker summary makes this group collapsible.
1183
+ this.historyContainer.setHidden(recap, true);
1184
+ this.groups.push(group);
1185
+ this.openGroup = group;
1186
+ return group;
1187
+ }
1188
+ closeOpenGroup() {
1189
+ this.openGroup = undefined;
1190
+ }
1191
+ appendGroupMember(component) {
1192
+ const group = this.ensureGroup();
1193
+ this.append(component);
1194
+ if (group)
1195
+ group.members.push(component);
1196
+ }
1197
+ /** Used for context that may join an existing run but must not create one. */
1198
+ appendExistingGroupMember(component) {
1199
+ this.append(component);
1200
+ if (this.isLiveGroupRegion() && this.openGroup)
1201
+ this.openGroup.members.push(component);
1202
+ }
1203
+ removeGroupMember(component) {
1204
+ for (const group of this.groups) {
1205
+ const index = group.members.indexOf(component);
1206
+ if (index !== -1)
1207
+ group.members.splice(index, 1);
1208
+ }
1209
+ this.historyContainer.setHidden(component, false);
1210
+ }
1211
+ /** Add one tool as a group member, moving the assistant's boundary separator
1212
+ * behind the new summary row without changing the separator's identity. */
1213
+ appendToolComponent(name, id, args) {
1214
+ const trailingSeparator = this.isLiveGroupRegion() &&
1215
+ !this.openGroup &&
1216
+ this.historyContainer.children.at(-1) instanceof FoldedToolSeparator
1217
+ ? this.historyContainer.children.at(-1)
1218
+ : undefined;
1219
+ if (trailingSeparator)
1220
+ this.historyContainer.removeChild(trailingSeparator);
1221
+ const group = this.ensureGroup();
1222
+ if (trailingSeparator) {
1223
+ this.append(trailingSeparator);
1224
+ group?.members.push(trailingSeparator);
1225
+ }
1226
+ const component = this.makeToolComponent(name, id, args);
1227
+ component.activityCallId = id;
1228
+ this.appendGroupMember(component);
1229
+ this.registerCopyBlock(component, 'tool', '');
1230
+ if (group) {
1231
+ group.key ??= id;
1232
+ group.toolCount++;
1233
+ group.inFlight.add(component);
1234
+ group.activity.noteCall(id, name, args);
1235
+ }
1236
+ return component;
1237
+ }
1238
+ noteGroupTool(component, name, args) {
1239
+ if (!component.activityCallId)
1240
+ return;
1241
+ for (const group of this.groups) {
1242
+ if (group.members.includes(component))
1243
+ group.activity.noteCall(component.activityCallId, name, args);
1244
+ }
1245
+ }
1246
+ settleGroupTool(component, result, isError = false) {
1247
+ for (const group of this.groups) {
1248
+ group.inFlight.delete(component);
1249
+ if (!component.activityCallId || !group.members.includes(component))
1250
+ continue;
1251
+ if (result === undefined)
1252
+ group.activity.resolveWithoutResult(component.activityCallId);
1253
+ else
1254
+ group.activity.settle(component.activityCallId, result, isError);
1255
+ }
1256
+ }
1257
+ /** An assistant starts as a non-boundary while its text is empty. Visible prose
1258
+ * or a terminal failure turns it into a boundary without recreating either
1259
+ * pi-owned component (their direct-child cache identities remain intact). */
1260
+ promoteStreamingAssistantToBoundary(message, force = false) {
1261
+ if (!this.streamingWasGroupMember || (!force && assistantVisibleText(message).trim() === ''))
1262
+ return;
1263
+ if (this.streamingComponent)
1264
+ this.removeGroupMember(this.streamingComponent);
1265
+ if (this.streamingToolSeparator)
1266
+ this.removeGroupMember(this.streamingToolSeparator);
1267
+ this.streamingWasGroupMember = false;
1268
+ this.closeOpenGroup();
1269
+ }
1270
+ seedToolGroupSummaries(summaries) {
1271
+ this.groupSummaries.clear();
1272
+ for (const [key, summary] of Object.entries(normalizeToolGroupSummaryRecord(summaries))) {
1273
+ this.groupSummaries.set(key, summary);
1274
+ }
1275
+ }
1276
+ /** Broker frames can arrive before or after their group's events, and snapshot
1277
+ * rebuilding defers them through the single-writer gate, so this stores first
1278
+ * and applies to every currently-rendered matching group. */
1279
+ applyToolGroupSummary(key, summary) {
1280
+ if (this.deferIfBuilding(() => this.applyToolGroupSummary(key, summary)))
1281
+ return;
1282
+ const normalized = normalizeToolGroupSummary(summary);
1283
+ if (key.trim() === '' || normalized === null)
1284
+ return;
1285
+ this.groupSummaries.set(key, normalized);
1286
+ this.applyToolDisplay();
1287
+ }
1288
+ applyGroupDisplay(group) {
1289
+ const summary = group.key === undefined ? undefined : this.groupSummaries.get(group.key);
1290
+ const collapsed = this.foldSettledTools &&
1291
+ this.showGroupSummaries &&
1292
+ group.toolCount > 0 &&
1293
+ group.inFlight.size === 0 &&
1294
+ summary !== undefined;
1295
+ group.recap.setRecap(collapsed ? summary : undefined, group.toolCount, group.activity.activity(), this.detailedToolRecaps, this.recapPalette);
1296
+ this.historyContainer.setHidden(group.recap, !collapsed);
1297
+ for (const member of group.members)
1298
+ this.historyContainer.setHidden(member, collapsed);
1299
+ }
977
1300
  /** Make each separator visible only if its immediately-following tool call is
978
1301
  * actually folded; in-flight calls remain flush with their live output. */
979
1302
  updateFoldedToolSeparators(inFlight = new Set(this.pendingTools.values())) {
@@ -984,7 +1307,9 @@ export class ChatView {
984
1307
  continue;
985
1308
  const followingTool = children[i + 1];
986
1309
  separator.setFolded(this.foldSettledTools &&
1310
+ !this.historyContainer.hiddenChildren.has(separator) &&
987
1311
  followingTool instanceof MinimizableToolComponent &&
1312
+ !this.historyContainer.hiddenChildren.has(followingTool) &&
988
1313
  !inFlight.has(followingTool));
989
1314
  this.historyContainer.markDirty(separator);
990
1315
  }
@@ -1037,6 +1362,33 @@ export class ChatView {
1037
1362
  append(child) {
1038
1363
  this.appendTarget.addChild(child);
1039
1364
  }
1365
+ /** Project one raw assistant message through every attach-local Markdown
1366
+ * transform. This is the sole pipeline used by snapshots, streaming, settled
1367
+ * messages, and Ctrl+O re-renders. */
1368
+ renderAssistantMessage(message) {
1369
+ const withDiagrams = transformMessageDiagrams(message, this.renderWidth());
1370
+ const withHtml = transformMessageHtml(withDiagrams, {
1371
+ detailsExpanded: this.detailsExpandedOverride,
1372
+ styles: {
1373
+ kbd: (text) => this.recapPalette.active(this.recapPalette.bold(`‹${text}›`)),
1374
+ mark: (text) => `\x1b[7m${this.recapPalette.bold(this.recapPalette.accent(text))}\x1b[27m`,
1375
+ hint: this.dimStyle,
1376
+ },
1377
+ });
1378
+ return styleAttachMessageMarkdown(withHtml);
1379
+ }
1380
+ /** Retain canonical source and update one stable transcript component with its
1381
+ * viewer-local projection. */
1382
+ updateAssistantComponent(component, message) {
1383
+ this.assistantSources.set(component, message);
1384
+ component.updateContent(this.renderAssistantMessage(message));
1385
+ }
1386
+ /** Re-project every assistant component in place after a disclosure toggle. */
1387
+ rerenderAssistantMessages() {
1388
+ for (const [component, source] of this.assistantSources) {
1389
+ component.updateContent(this.renderAssistantMessage(source));
1390
+ }
1391
+ }
1040
1392
  /** Approximate render width for inline mermaid diagrams. Only affects
1041
1393
  * termrender's trailing padding (stripped in the transform), so the terminal
1042
1394
  * column count is close enough without threading the live pane width here. */
@@ -1054,15 +1406,34 @@ export class ChatView {
1054
1406
  this.settleBashComponent();
1055
1407
  this.streamingComponent = undefined;
1056
1408
  this.streamingToolSeparator = undefined;
1409
+ this.streamingWasGroupMember = false;
1057
1410
  this.foldedToolSeparatorContents.clear();
1411
+ this.assistantSources.clear();
1412
+ this.groups.length = 0;
1413
+ this.openGroup = undefined;
1414
+ this.groupSummaries.clear();
1058
1415
  this.pendingTools.clear();
1059
1416
  this.lastAssistantText = undefined;
1417
+ this.copyBlockRecords.length = 0;
1418
+ this.copyIndex.clear();
1060
1419
  this.transcriptTexts.length = 0;
1061
1420
  this.transcriptFilePaths.length = 0;
1062
1421
  this.historyContainer.clear();
1063
1422
  }
1064
1423
  makeLoader(message) {
1065
- return new Loader(this.tui, this.spinnerStyle, this.dimStyle, message);
1424
+ // Loader updates itself on an interval. Its TUI callback has to evict the
1425
+ // status block's cached frame before requesting the next render.
1426
+ let loader;
1427
+ const statusTui = {
1428
+ requestRender: (force) => {
1429
+ if (loader && !this.onActivityChange)
1430
+ this.statusContainer.markDirty(loader);
1431
+ this.tui.requestRender(force);
1432
+ },
1433
+ terminal: this.tui.terminal,
1434
+ };
1435
+ loader = new Loader(statusTui, this.spinnerStyle, this.dimStyle, message);
1436
+ return loader;
1066
1437
  }
1067
1438
  /** Let a self-updating history child invalidate only its own cached lines. */
1068
1439
  componentTui(component) {
@@ -1086,16 +1457,22 @@ export class ChatView {
1086
1457
  /** Swap the single active activity indicator (working/compaction/retry). */
1087
1458
  setActivity(loader) {
1088
1459
  this.activityLoader?.stop();
1089
- this.statusContainer.clear();
1090
1460
  this.activityLoader = loader;
1461
+ if (this.onActivityChange) {
1462
+ this.onActivityChange(loader);
1463
+ return;
1464
+ }
1465
+ this.statusContainer.clear();
1091
1466
  if (loader)
1092
1467
  this.statusContainer.addChild(loader);
1093
1468
  }
1094
1469
  showStatus(message) {
1470
+ this.closeOpenGroup();
1095
1471
  this.append(new Spacer(1));
1096
1472
  this.append(new Text(this.dimStyle(message), 1, 0));
1097
1473
  }
1098
1474
  showError(message) {
1475
+ this.closeOpenGroup();
1099
1476
  this.append(new Spacer(1));
1100
1477
  this.append(new Text(this.errorStyle(message), 1, 0));
1101
1478
  }
@@ -1131,6 +1508,98 @@ export class ChatView {
1131
1508
  const t = this.lastAssistantText?.trim();
1132
1509
  return t ? t : undefined;
1133
1510
  }
1511
+ /** Copyable source text for a transcript block, if it has any. */
1512
+ blockText(block) {
1513
+ const index = this.copyIndex.get(block);
1514
+ return index === undefined ? undefined : this.copyBlockRecords[index]?.text;
1515
+ }
1516
+ /** The canonical ordered copy record, oldest → newest. */
1517
+ copyBlocks() {
1518
+ return this.copyBlockRecords;
1519
+ }
1520
+ /** Newest tool call's result text (`/copy-tool`). */
1521
+ getLastToolOutputText() {
1522
+ for (let i = this.copyBlockRecords.length - 1; i >= 0; i--) {
1523
+ const copyBlock = this.copyBlockRecords[i];
1524
+ if (copyBlock.kind !== 'tool')
1525
+ continue;
1526
+ return copyBlock.text.trim() === '' ? undefined : copyBlock.text;
1527
+ }
1528
+ return undefined;
1529
+ }
1530
+ /** Every copyable block's text, oldest → newest, joined by blank lines. */
1531
+ getTranscriptPlainText() {
1532
+ return this.copyBlockRecords
1533
+ .map((copyBlock) => copyBlock.text)
1534
+ .filter((text) => text.trim() !== '')
1535
+ .join('\n\n');
1536
+ }
1537
+ registerCopyBlock(block, kind, text) {
1538
+ const condensedCopyBlocks = this.condensedCopyBlocks.get(this.appendTarget);
1539
+ if (condensedCopyBlocks) {
1540
+ const index = condensedCopyBlocks.findIndex((copyBlock) => copyBlock.block === block);
1541
+ if (index === -1)
1542
+ condensedCopyBlocks.push({ block, kind, text });
1543
+ else
1544
+ condensedCopyBlocks[index].text = text;
1545
+ return;
1546
+ }
1547
+ const index = this.copyIndex.get(block);
1548
+ if (index === undefined) {
1549
+ this.copyIndex.set(block, this.copyBlockRecords.length);
1550
+ this.copyBlockRecords.push({ block, kind, text });
1551
+ }
1552
+ else {
1553
+ this.copyBlockRecords[index].text = text;
1554
+ }
1555
+ }
1556
+ unregisterCopyBlock(block) {
1557
+ const condensedCopyBlocks = this.condensedCopyBlocks.get(this.appendTarget);
1558
+ if (condensedCopyBlocks) {
1559
+ const index = condensedCopyBlocks.findIndex((copyBlock) => copyBlock.block === block);
1560
+ if (index !== -1)
1561
+ condensedCopyBlocks.splice(index, 1);
1562
+ return;
1563
+ }
1564
+ const index = this.copyIndex.get(block);
1565
+ if (index === undefined)
1566
+ return;
1567
+ this.copyBlockRecords.splice(index, 1);
1568
+ this.copyIndex.delete(block);
1569
+ for (let i = index; i < this.copyBlockRecords.length; i++) {
1570
+ this.copyIndex.set(this.copyBlockRecords[i].block, i);
1571
+ }
1572
+ }
1573
+ appendCondensedBlock({ component, copyBlocks }) {
1574
+ this.append(component);
1575
+ this.registerCopyBlock(component, 'condensed', copyBlocks.map((copyBlock) => copyBlock.text).filter((text) => text.trim() !== '').join('\n\n'));
1576
+ }
1577
+ /** Flatten arbitrary tool result payloads into the visible text copy target. */
1578
+ toolResultText(result) {
1579
+ if (typeof result === 'string')
1580
+ return result;
1581
+ if (typeof result === 'object' && result !== null && 'content' in result) {
1582
+ const content = result.content;
1583
+ if (typeof content === 'string')
1584
+ return content;
1585
+ if (Array.isArray(content)) {
1586
+ return content
1587
+ .filter((part) => typeof part === 'object' &&
1588
+ part !== null &&
1589
+ part.type === 'text' &&
1590
+ typeof part.text === 'string')
1591
+ .map((part) => part.text)
1592
+ .join('');
1593
+ }
1594
+ }
1595
+ try {
1596
+ const text = JSON.stringify(result);
1597
+ return typeof text === 'string' ? text : '';
1598
+ }
1599
+ catch {
1600
+ return '';
1601
+ }
1602
+ }
1134
1603
  pushTranscriptText(text) {
1135
1604
  const trimmed = text.trim();
1136
1605
  if (trimmed === '')