@bastani/atomic 0.8.13 → 0.8.14

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 (355) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/host-html-template.ts +1 -1
  4. package/dist/builtin/mcp/init.ts +15 -2
  5. package/dist/builtin/mcp/mcp-callback-server.ts +10 -9
  6. package/dist/builtin/mcp/package.json +1 -1
  7. package/dist/builtin/mcp/ui-session.ts +9 -6
  8. package/dist/builtin/subagents/CHANGELOG.md +8 -1
  9. package/dist/builtin/subagents/README.md +39 -32
  10. package/dist/builtin/subagents/package.json +1 -1
  11. package/dist/builtin/subagents/skills/subagent/SKILL.md +11 -11
  12. package/dist/builtin/subagents/src/agents/agent-management.ts +6 -1
  13. package/dist/builtin/subagents/src/agents/agent-serializer.ts +2 -0
  14. package/dist/builtin/subagents/src/agents/agents.ts +44 -19
  15. package/dist/builtin/subagents/src/extension/config.ts +16 -0
  16. package/dist/builtin/subagents/src/extension/fanout-child.ts +246 -0
  17. package/dist/builtin/subagents/src/extension/index.ts +466 -603
  18. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +6 -4
  19. package/dist/builtin/subagents/src/intercom/result-intercom.ts +109 -1
  20. package/dist/builtin/subagents/src/runs/background/async-execution.ts +124 -19
  21. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +41 -6
  22. package/dist/builtin/subagents/src/runs/background/async-resume.ts +28 -15
  23. package/dist/builtin/subagents/src/runs/background/async-status.ts +60 -30
  24. package/dist/builtin/subagents/src/runs/background/result-watcher.ts +111 -54
  25. package/dist/builtin/subagents/src/runs/background/run-id-resolver.ts +83 -0
  26. package/dist/builtin/subagents/src/runs/background/run-status.ts +79 -3
  27. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +46 -1
  28. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +66 -14
  29. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +10 -3
  30. package/dist/builtin/subagents/src/runs/foreground/execution.ts +14 -2
  31. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +320 -23
  32. package/dist/builtin/subagents/src/runs/shared/completion-guard.ts +23 -1
  33. package/dist/builtin/subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +369 -0
  34. package/dist/builtin/subagents/src/runs/shared/nested-events.ts +935 -0
  35. package/dist/builtin/subagents/src/runs/shared/nested-path.ts +52 -0
  36. package/dist/builtin/subagents/src/runs/shared/nested-render.ts +115 -0
  37. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
  38. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +82 -9
  39. package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +1 -1
  40. package/dist/builtin/subagents/src/runs/shared/single-output.ts +12 -2
  41. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +32 -10
  42. package/dist/builtin/subagents/src/runs/shared/worktree.ts +3 -2
  43. package/dist/builtin/subagents/src/shared/artifacts.ts +0 -1
  44. package/dist/builtin/subagents/src/shared/types.ts +96 -1
  45. package/dist/builtin/subagents/src/shared/utils.ts +10 -2
  46. package/dist/builtin/subagents/src/slash/slash-commands.ts +468 -625
  47. package/dist/builtin/subagents/src/tui/render.ts +1227 -2093
  48. package/dist/builtin/web-access/package.json +1 -1
  49. package/dist/builtin/workflows/CHANGELOG.md +24 -0
  50. package/dist/builtin/workflows/README.md +28 -11
  51. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +323 -40
  52. package/dist/builtin/workflows/builtin/ralph.ts +362 -176
  53. package/dist/builtin/workflows/package.json +2 -5
  54. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  55. package/dist/builtin/workflows/skills/skill-creator/LICENSE.txt +202 -0
  56. package/dist/builtin/workflows/skills/skill-creator/SKILL.md +489 -0
  57. package/dist/builtin/workflows/skills/skill-creator/agents/analyzer.md +274 -0
  58. package/dist/builtin/workflows/skills/skill-creator/agents/comparator.md +202 -0
  59. package/dist/builtin/workflows/skills/skill-creator/agents/grader.md +223 -0
  60. package/dist/builtin/workflows/skills/skill-creator/assets/eval_review.html +146 -0
  61. package/dist/builtin/workflows/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  62. package/dist/builtin/workflows/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  63. package/dist/builtin/workflows/skills/skill-creator/references/schemas.md +430 -0
  64. package/dist/builtin/workflows/skills/skill-creator/scripts/__init__.py +0 -0
  65. package/dist/builtin/workflows/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  66. package/dist/builtin/workflows/skills/skill-creator/scripts/generate_report.py +326 -0
  67. package/dist/builtin/workflows/skills/skill-creator/scripts/improve_description.py +247 -0
  68. package/dist/builtin/workflows/skills/skill-creator/scripts/package_skill.py +136 -0
  69. package/dist/builtin/workflows/skills/skill-creator/scripts/quick_validate.py +103 -0
  70. package/dist/builtin/workflows/skills/skill-creator/scripts/run_eval.py +310 -0
  71. package/dist/builtin/workflows/skills/skill-creator/scripts/run_loop.py +328 -0
  72. package/dist/builtin/workflows/skills/skill-creator/scripts/utils.py +47 -0
  73. package/dist/builtin/workflows/src/extension/index.ts +869 -93
  74. package/dist/builtin/workflows/src/extension/render-call.ts +34 -1
  75. package/dist/builtin/workflows/src/extension/render-result.ts +126 -21
  76. package/dist/builtin/workflows/src/extension/runtime.ts +91 -3
  77. package/dist/builtin/workflows/src/extension/wiring.ts +38 -12
  78. package/dist/builtin/workflows/src/extension/workflow-schema.ts +62 -5
  79. package/dist/builtin/workflows/src/runs/background/runner.ts +3 -3
  80. package/dist/builtin/workflows/src/runs/background/status.ts +42 -8
  81. package/dist/builtin/workflows/src/runs/foreground/executor.ts +410 -95
  82. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +5 -2
  83. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +8 -0
  84. package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +6 -4
  85. package/dist/builtin/workflows/src/runs/shared/worktree.ts +3 -2
  86. package/dist/builtin/workflows/src/shared/persistence-restore.ts +138 -5
  87. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +30 -0
  88. package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +78 -120
  89. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +193 -0
  90. package/dist/builtin/workflows/src/shared/store-types.ts +26 -1
  91. package/dist/builtin/workflows/src/shared/store.ts +145 -17
  92. package/dist/builtin/workflows/src/shared/timing.ts +6 -2
  93. package/dist/builtin/workflows/src/shared/workflow-failures.ts +375 -0
  94. package/dist/builtin/workflows/src/tui/chat-surface.ts +68 -17
  95. package/dist/builtin/workflows/src/tui/connectors.ts +2 -2
  96. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +24 -26
  97. package/dist/builtin/workflows/src/tui/graph-canvas.ts +4 -8
  98. package/dist/builtin/workflows/src/tui/graph-view.ts +17 -14
  99. package/dist/builtin/workflows/src/tui/header.ts +38 -0
  100. package/dist/builtin/workflows/src/tui/inline-form-card.ts +161 -238
  101. package/dist/builtin/workflows/src/tui/inline-form-editor.ts +68 -73
  102. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +2 -3
  103. package/dist/builtin/workflows/src/tui/inline-form-store.ts +2 -1
  104. package/dist/builtin/workflows/src/tui/inputs-overlay.ts +1 -3
  105. package/dist/builtin/workflows/src/tui/inputs-picker.ts +286 -399
  106. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +11 -0
  107. package/dist/builtin/workflows/src/tui/node-card.ts +2 -1
  108. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -1
  109. package/dist/builtin/workflows/src/tui/prompt-card.ts +46 -19
  110. package/dist/builtin/workflows/src/tui/run-detail.ts +63 -80
  111. package/dist/builtin/workflows/src/tui/session-confirm.ts +9 -3
  112. package/dist/builtin/workflows/src/tui/session-picker.ts +19 -16
  113. package/dist/builtin/workflows/src/tui/stage-chat-layout.ts +88 -0
  114. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +368 -879
  115. package/dist/builtin/workflows/src/tui/status-helpers.ts +4 -0
  116. package/dist/builtin/workflows/src/tui/status-list.ts +67 -75
  117. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +50 -12
  118. package/dist/builtin/workflows/src/tui/submit-pane.ts +164 -0
  119. package/dist/builtin/workflows/src/tui/switcher.ts +27 -4
  120. package/dist/builtin/workflows/src/tui/text-helpers.ts +98 -4
  121. package/dist/builtin/workflows/src/tui/widget.ts +90 -68
  122. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +23 -2
  123. package/dist/builtin/workflows/src/tui/workflow-list.ts +44 -68
  124. package/dist/cli/file-processor.d.ts.map +1 -1
  125. package/dist/cli/file-processor.js +2 -3
  126. package/dist/cli/file-processor.js.map +1 -1
  127. package/dist/config.d.ts.map +1 -1
  128. package/dist/config.js +3 -10
  129. package/dist/config.js.map +1 -1
  130. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  131. package/dist/core/agent-session-runtime.js +2 -1
  132. package/dist/core/agent-session-runtime.js.map +1 -1
  133. package/dist/core/agent-session-services.d.ts.map +1 -1
  134. package/dist/core/agent-session-services.js +3 -2
  135. package/dist/core/agent-session-services.js.map +1 -1
  136. package/dist/core/agent-session.d.ts +6 -0
  137. package/dist/core/agent-session.d.ts.map +1 -1
  138. package/dist/core/agent-session.js +16 -2
  139. package/dist/core/agent-session.js.map +1 -1
  140. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  141. package/dist/core/atomic-guide-command.js +8 -9
  142. package/dist/core/atomic-guide-command.js.map +1 -1
  143. package/dist/core/auth-storage.d.ts.map +1 -1
  144. package/dist/core/auth-storage.js +3 -2
  145. package/dist/core/auth-storage.js.map +1 -1
  146. package/dist/core/bash-executor.d.ts.map +1 -1
  147. package/dist/core/bash-executor.js +2 -1
  148. package/dist/core/bash-executor.js.map +1 -1
  149. package/dist/core/export-html/index.d.ts.map +1 -1
  150. package/dist/core/export-html/index.js +8 -6
  151. package/dist/core/export-html/index.js.map +1 -1
  152. package/dist/core/export-html/template.js +6 -3
  153. package/dist/core/extensions/loader.d.ts.map +1 -1
  154. package/dist/core/extensions/loader.js +12 -29
  155. package/dist/core/extensions/loader.js.map +1 -1
  156. package/dist/core/model-registry.d.ts.map +1 -1
  157. package/dist/core/model-registry.js +5 -1
  158. package/dist/core/model-registry.js.map +1 -1
  159. package/dist/core/package-manager.d.ts +8 -0
  160. package/dist/core/package-manager.d.ts.map +1 -1
  161. package/dist/core/package-manager.js +145 -58
  162. package/dist/core/package-manager.js.map +1 -1
  163. package/dist/core/prompt-templates.d.ts.map +1 -1
  164. package/dist/core/prompt-templates.js +6 -20
  165. package/dist/core/prompt-templates.js.map +1 -1
  166. package/dist/core/resource-loader.d.ts.map +1 -1
  167. package/dist/core/resource-loader.js +38 -31
  168. package/dist/core/resource-loader.js.map +1 -1
  169. package/dist/core/sdk.d.ts.map +1 -1
  170. package/dist/core/sdk.js +9 -4
  171. package/dist/core/sdk.js.map +1 -1
  172. package/dist/core/session-manager.d.ts.map +1 -1
  173. package/dist/core/session-manager.js +32 -24
  174. package/dist/core/session-manager.js.map +1 -1
  175. package/dist/core/settings-manager.d.ts.map +1 -1
  176. package/dist/core/settings-manager.js +8 -15
  177. package/dist/core/settings-manager.js.map +1 -1
  178. package/dist/core/skills.d.ts.map +1 -1
  179. package/dist/core/skills.js +8 -22
  180. package/dist/core/skills.js.map +1 -1
  181. package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts +5 -4
  182. package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts.map +1 -1
  183. package/dist/core/tools/ask-user-question/state/questionnaire-session.js +34 -11
  184. package/dist/core/tools/ask-user-question/state/questionnaire-session.js.map +1 -1
  185. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts +1 -0
  186. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts.map +1 -1
  187. package/dist/core/tools/ask-user-question/state/selectors/contract.js.map +1 -1
  188. package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts.map +1 -1
  189. package/dist/core/tools/ask-user-question/state/selectors/projections.js +1 -0
  190. package/dist/core/tools/ask-user-question/state/selectors/projections.js.map +1 -1
  191. package/dist/core/tools/ask-user-question/state/state-reducer.d.ts +1 -2
  192. package/dist/core/tools/ask-user-question/state/state-reducer.d.ts.map +1 -1
  193. package/dist/core/tools/ask-user-question/state/state-reducer.js +26 -9
  194. package/dist/core/tools/ask-user-question/state/state-reducer.js.map +1 -1
  195. package/dist/core/tools/ask-user-question/state/state.d.ts +4 -0
  196. package/dist/core/tools/ask-user-question/state/state.d.ts.map +1 -1
  197. package/dist/core/tools/ask-user-question/state/state.js.map +1 -1
  198. package/dist/core/tools/ask-user-question/view/components/option-list-view.d.ts +1 -0
  199. package/dist/core/tools/ask-user-question/view/components/option-list-view.d.ts.map +1 -1
  200. package/dist/core/tools/ask-user-question/view/components/option-list-view.js +1 -0
  201. package/dist/core/tools/ask-user-question/view/components/option-list-view.js.map +1 -1
  202. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts +9 -6
  203. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
  204. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +28 -7
  205. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
  206. package/dist/core/tools/ask-user-question/view/props-adapter.d.ts.map +1 -1
  207. package/dist/core/tools/ask-user-question/view/props-adapter.js +4 -1
  208. package/dist/core/tools/ask-user-question/view/props-adapter.js.map +1 -1
  209. package/dist/core/tools/bash.d.ts.map +1 -1
  210. package/dist/core/tools/bash.js +56 -53
  211. package/dist/core/tools/bash.js.map +1 -1
  212. package/dist/core/tools/edit-diff.d.ts +3 -1
  213. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  214. package/dist/core/tools/edit-diff.js +8 -1
  215. package/dist/core/tools/edit-diff.js.map +1 -1
  216. package/dist/core/tools/edit.d.ts +3 -1
  217. package/dist/core/tools/edit.d.ts.map +1 -1
  218. package/dist/core/tools/edit.js +44 -81
  219. package/dist/core/tools/edit.js.map +1 -1
  220. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  221. package/dist/core/tools/file-mutation-queue.js +27 -12
  222. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  223. package/dist/core/tools/find.d.ts.map +1 -1
  224. package/dist/core/tools/find.js +2 -3
  225. package/dist/core/tools/find.js.map +1 -1
  226. package/dist/core/tools/grep.d.ts.map +1 -1
  227. package/dist/core/tools/grep.js +3 -3
  228. package/dist/core/tools/grep.js.map +1 -1
  229. package/dist/core/tools/ls.d.ts.map +1 -1
  230. package/dist/core/tools/ls.js +5 -5
  231. package/dist/core/tools/ls.js.map +1 -1
  232. package/dist/core/tools/output-accumulator.d.ts +2 -0
  233. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  234. package/dist/core/tools/output-accumulator.js +11 -4
  235. package/dist/core/tools/output-accumulator.js.map +1 -1
  236. package/dist/core/tools/path-utils.d.ts +2 -0
  237. package/dist/core/tools/path-utils.d.ts.map +1 -1
  238. package/dist/core/tools/path-utils.js +39 -21
  239. package/dist/core/tools/path-utils.js.map +1 -1
  240. package/dist/core/tools/read.d.ts.map +1 -1
  241. package/dist/core/tools/read.js +9 -8
  242. package/dist/core/tools/read.js.map +1 -1
  243. package/dist/core/tools/truncate.d.ts.map +1 -1
  244. package/dist/core/tools/truncate.js +12 -2
  245. package/dist/core/tools/truncate.js.map +1 -1
  246. package/dist/core/tools/write.d.ts.map +1 -1
  247. package/dist/core/tools/write.js +20 -35
  248. package/dist/core/tools/write.js.map +1 -1
  249. package/dist/index.d.ts +2 -1
  250. package/dist/index.d.ts.map +1 -1
  251. package/dist/index.js +4 -1
  252. package/dist/index.js.map +1 -1
  253. package/dist/main.d.ts.map +1 -1
  254. package/dist/main.js +5 -6
  255. package/dist/main.js.map +1 -1
  256. package/dist/modes/interactive/chat-input-actions.d.ts +24 -0
  257. package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -0
  258. package/dist/modes/interactive/chat-input-actions.js +179 -0
  259. package/dist/modes/interactive/chat-input-actions.js.map +1 -0
  260. package/dist/modes/interactive/components/chat-message-renderer.d.ts +1 -0
  261. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  262. package/dist/modes/interactive/components/chat-message-renderer.js +14 -3
  263. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  264. package/dist/modes/interactive/components/chat-session-host.d.ts +157 -0
  265. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -0
  266. package/dist/modes/interactive/components/chat-session-host.js +1007 -0
  267. package/dist/modes/interactive/components/chat-session-host.js.map +1 -0
  268. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  269. package/dist/modes/interactive/components/config-selector.js +1 -1
  270. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  271. package/dist/modes/interactive/components/footer.d.ts +1 -0
  272. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  273. package/dist/modes/interactive/components/footer.js +14 -5
  274. package/dist/modes/interactive/components/footer.js.map +1 -1
  275. package/dist/modes/interactive/components/index.d.ts +1 -0
  276. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  277. package/dist/modes/interactive/components/index.js +1 -0
  278. package/dist/modes/interactive/components/index.js.map +1 -1
  279. package/dist/modes/interactive/components/login-dialog.d.ts +9 -1
  280. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  281. package/dist/modes/interactive/components/login-dialog.js +29 -4
  282. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  283. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  284. package/dist/modes/interactive/interactive-mode.js +18 -67
  285. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  286. package/dist/utils/child-process.d.ts +1 -0
  287. package/dist/utils/child-process.d.ts.map +1 -1
  288. package/dist/utils/child-process.js +8 -0
  289. package/dist/utils/child-process.js.map +1 -1
  290. package/dist/utils/clipboard-native.d.ts +3 -1
  291. package/dist/utils/clipboard-native.d.ts.map +1 -1
  292. package/dist/utils/clipboard-native.js +14 -8
  293. package/dist/utils/clipboard-native.js.map +1 -1
  294. package/dist/utils/image-resize-core.d.ts +30 -0
  295. package/dist/utils/image-resize-core.d.ts.map +1 -0
  296. package/dist/utils/image-resize-core.js +124 -0
  297. package/dist/utils/image-resize-core.js.map +1 -0
  298. package/dist/utils/image-resize-worker.d.ts +2 -0
  299. package/dist/utils/image-resize-worker.d.ts.map +1 -0
  300. package/dist/utils/image-resize-worker.js +31 -0
  301. package/dist/utils/image-resize-worker.js.map +1 -0
  302. package/dist/utils/image-resize.d.ts +7 -27
  303. package/dist/utils/image-resize.d.ts.map +1 -1
  304. package/dist/utils/image-resize.js +75 -115
  305. package/dist/utils/image-resize.js.map +1 -1
  306. package/dist/utils/paths.d.ts +16 -1
  307. package/dist/utils/paths.d.ts.map +1 -1
  308. package/dist/utils/paths.js +49 -7
  309. package/dist/utils/paths.js.map +1 -1
  310. package/docs/changelog.mdx +29 -0
  311. package/docs/compaction.md +1 -1
  312. package/docs/custom-provider.md +2 -2
  313. package/docs/development.md +1 -1
  314. package/docs/docs.json +98 -143
  315. package/docs/extensions.md +29 -16
  316. package/docs/favicon.svg +29 -0
  317. package/docs/images/interactive-mode.png +0 -0
  318. package/docs/images/tree-view.png +0 -0
  319. package/docs/images/workflow-command.png +0 -0
  320. package/docs/images/workflow-graph.png +0 -0
  321. package/docs/images/workflow-input-picker.png +0 -0
  322. package/docs/images/workflow-list.png +0 -0
  323. package/docs/index.md +10 -1
  324. package/docs/logo.svg +59 -0
  325. package/docs/packages.md +3 -3
  326. package/docs/providers.md +1 -1
  327. package/docs/quickstart.md +98 -2
  328. package/docs/rpc.md +8 -8
  329. package/docs/sdk.md +23 -12
  330. package/docs/sessions.md +1 -1
  331. package/docs/skills.md +15 -1
  332. package/docs/termux.md +11 -1
  333. package/docs/themes.md +6 -6
  334. package/docs/tui.md +18 -18
  335. package/docs/usage.md +1 -1
  336. package/docs/workflows.md +172 -2
  337. package/examples/extensions/subagent/index.ts +2 -1
  338. package/package.json +6 -6
  339. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/SKILL.md +0 -0
  340. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/element-attributes.md +0 -0
  341. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/playwright-tests.md +0 -0
  342. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/request-mocking.md +0 -0
  343. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/running-code.md +0 -0
  344. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/session-management.md +0 -0
  345. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/spec-driven-testing.md +0 -0
  346. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/storage-state.md +0 -0
  347. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/test-generation.md +0 -0
  348. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/tracing.md +0 -0
  349. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/video-recording.md +0 -0
  350. /package/dist/builtin/{workflows → subagents}/skills/tdd/SKILL.md +0 -0
  351. /package/dist/builtin/{workflows → subagents}/skills/tdd/deep-modules.md +0 -0
  352. /package/dist/builtin/{workflows → subagents}/skills/tdd/interface-design.md +0 -0
  353. /package/dist/builtin/{workflows → subagents}/skills/tdd/mocking.md +0 -0
  354. /package/dist/builtin/{workflows → subagents}/skills/tdd/refactoring.md +0 -0
  355. /package/dist/builtin/{workflows → subagents}/skills/tdd/tests.md +0 -0
@@ -40,23 +40,14 @@
40
40
  */
41
41
 
42
42
  import {
43
- ChatTranscriptComponent,
44
- CustomEditor,
45
- FooterComponent,
46
- ScrollableComponentViewport,
47
- SessionManager,
48
- LiveChatEntriesController,
49
- UsageMeterComponent,
50
- WorkingStatusComponent,
51
- pickWhimsicalWorkingMessage,
52
- renderChatMessageEntry,
43
+ ChatSessionHost,
44
+ type ChatSessionHostStyle,
53
45
  type AgentSession,
54
- type AgentSessionEvent,
55
46
  type ChatMessageEntry,
56
47
  type ChatMessageRenderOptions,
57
48
  type ReadonlyFooterDataProvider,
58
49
  } from "@bastani/atomic";
59
- import { Box, Spacer, Text } from "@earendil-works/pi-tui";
50
+ import { Box, Text } from "@earendil-works/pi-tui";
60
51
  import type {
61
52
  Component,
62
53
  EditorComponent,
@@ -65,12 +56,23 @@ import type {
65
56
  TUI,
66
57
  } from "@earendil-works/pi-tui";
67
58
  import type { Store } from "../shared/store.js";
59
+ import {
60
+ mountStageCustomUi,
61
+ stageUiBroker,
62
+ type MountedStageCustomUi,
63
+ type StageCustomUiRequest,
64
+ type StageUiBroker,
65
+ } from "../shared/stage-ui-broker.js";
68
66
  import type { StageNotice, StageSnapshot } from "../shared/store-types.js";
69
- import { elapsedStageMs } from "../shared/timing.js";
70
67
  import type { GraphTheme } from "./graph-theme.js";
71
68
  import type { StageControlHandle } from "../runs/foreground/stage-control-registry.js";
72
69
  import { BOLD, RESET, hexBg, hexToAnsi, lerpColor } from "./color-utils.js";
73
- import { matchesKey, truncateToWidth, visibleWidth } from "./text-helpers.js";
70
+ import { Key, matchesKey, visibleWidth } from "./text-helpers.js";
71
+ import {
72
+ fitStageChatFrame,
73
+ planStageChatFrame,
74
+ resolveStageChatViewportRows,
75
+ } from "./stage-chat-layout.js";
74
76
 
75
77
  // ---------------------------------------------------------------------------
76
78
  // Options & types
@@ -96,6 +98,7 @@ export interface StageChatViewOpts {
96
98
  requestRender?: () => void;
97
99
  /** Live pi-tui host objects. When present, stage input uses pi's editor UI. */
98
100
  piTui?: TUI;
101
+ piTheme?: unknown;
99
102
  piKeybindings?: unknown;
100
103
  /** Currently installed host editor factory, inherited from extension `ctx.ui.setEditorComponent()`. */
101
104
  piEditorFactory?: (
@@ -105,7 +108,7 @@ export interface StageChatViewOpts {
105
108
  ) => EditorComponent;
106
109
  /** Parent chat rendering settings and extension renderers inherited from the host UI. */
107
110
  getChatRenderSettings?: () =>
108
- | Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd" | "markdownTheme">>
111
+ | Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">>
109
112
  | undefined;
110
113
  /** Parent footer data provider inherited from the host UI for core footer/usage rendering. */
111
114
  footerData?: ReadonlyFooterDataProvider;
@@ -117,6 +120,8 @@ export interface StageChatViewOpts {
117
120
  * Returning `undefined` falls back to the constant 32-row frame.
118
121
  */
119
122
  getViewportRows?: () => number | undefined;
123
+ /** Broker that routes stage-local custom UI, such as ask_user_question, into this node. */
124
+ stageUiBroker?: StageUiBroker;
120
125
  }
121
126
 
122
127
  /**
@@ -147,18 +152,10 @@ type AgentSnapshotMessage = AgentSession["messages"][number];
147
152
  */
148
153
  const VIEW_LINE_COUNT = 32;
149
154
 
150
- /** Header strip — `▎ STAGE wf / stage <meta> ● status` */
155
+ /** Header strip — ` STAGE wf / stage <meta> ● status` without a leading marker glyph. */
151
156
  const HEADER_ROWS = 1;
152
157
  /** Single dim rule between header and body. */
153
158
  const SEP_ROWS = 1;
154
- /** Spinner glyphs — Braille spinner at 80ms per frame. */
155
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
156
- /** Pi's Loader advances at 80ms; use the same cadence for embedded stage chats. */
157
- const ANIMATION_FRAME_MS = 80;
158
- const STREAMING_RENDER_THROTTLE_MS = 80;
159
- const STREAMING_TEXT_TAIL_LINES = 240;
160
- const STREAMING_TEXT_TAIL_CHARS = 16_000;
161
-
162
159
  const ITALIC = "\x1b[3m";
163
160
  const FG_RESET = "\x1b[39m";
164
161
  const WEIGHT_RESET = "\x1b[22m";
@@ -180,41 +177,25 @@ export class StageChatView implements Component, Focusable {
180
177
  private onClose: () => void;
181
178
  private requestRender: (() => void) | undefined;
182
179
  private getViewportRows?: () => number | undefined;
183
- private editor: EditorComponent | undefined;
180
+ private piTui?: TUI;
181
+ private piTheme?: unknown;
182
+ private piKeybindings?: unknown;
183
+ private chatHost: ChatSessionHost<NoticeEntry>;
184
+ private stageUiBroker: StageUiBroker;
185
+ private mountedCustomUi: MountedStageCustomUi | null = null;
184
186
  private getChatRenderSettings?: () =>
185
- | Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd" | "markdownTheme">>
187
+ | Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">>
186
188
  | undefined;
187
189
  private footerData?: ReadonlyFooterDataProvider;
188
190
 
189
- private inputBuffer = "";
190
- private transcript: TranscriptEntry[] = [];
191
- private statusMessage = "";
192
191
  /** True while a pending pause request is in flight (between ctrl+p and resolve). */
193
192
  private localPaused = false;
194
193
  /** De-dup set so the store subscription doesn't re-append known notices. */
195
194
  private seenNoticeIds = new Set<string>();
196
- /** Wall-clock at construction, used to colour the spinner frame stably. */
197
- private attachedAt = Date.now();
198
- /** True after SDK `agent_start` until `agent_end`; mirrors Pi's working-loader lifecycle. */
199
- private sdkBusy = false;
200
- /** Pi-style per-turn working message, populated from coding-agent's message picker. */
201
- private workingMessage: string | undefined;
202
- /** User rows optimistically appended by this embedded editor, de-duped on SDK echo. */
203
- private optimisticUserSignatures = new Set<string>();
204
- /** Pending steering messages emitted by AgentSession queue updates. */
205
- private pendingSteeringMessages: readonly string[] = [];
206
- /** Pending follow-up messages emitted by AgentSession queue updates. */
207
- private pendingFollowUpMessages: readonly string[] = [];
208
- /** Chat-mode repaint driver for Pi-style loaders/spinners. */
209
- private animationTimer: ReturnType<typeof setInterval> | undefined;
210
- /** Coalesces high-frequency SDK deltas while the fixed overlay is streaming. */
211
- private renderThrottleTimer: ReturnType<typeof setTimeout> | undefined;
212
- /** Scrollable fixed-height body viewport for attached chat history. */
213
- private bodyViewport = new ScrollableComponentViewport();
214
- private liveChat: LiveChatEntriesController;
215
195
 
216
196
  private _unsubscribeStore: (() => void) | null = null;
217
197
  private _unsubscribeHandle: (() => void) | null = null;
198
+ private _unregisterStageUiHost: (() => void) | null = null;
218
199
 
219
200
  constructor(opts: StageChatViewOpts) {
220
201
  this.store = opts.store;
@@ -227,14 +208,90 @@ export class StageChatView implements Component, Focusable {
227
208
  this.onClose = opts.onClose;
228
209
  this.requestRender = opts.requestRender;
229
210
  this.getViewportRows = opts.getViewportRows;
211
+ this.piTui = opts.piTui;
212
+ this.piTheme = opts.piTheme;
213
+ this.piKeybindings = opts.piKeybindings;
230
214
  this.getChatRenderSettings = opts.getChatRenderSettings;
231
215
  this.footerData = opts.footerData;
232
- this.liveChat = new LiveChatEntriesController(this.transcript);
233
- this.editor = this._createEditor(
234
- opts.piTui,
235
- opts.piKeybindings,
236
- opts.piEditorFactory,
237
- );
216
+ this.stageUiBroker = opts.stageUiBroker ?? stageUiBroker;
217
+ this.chatHost = new ChatSessionHost<NoticeEntry>({
218
+ style: this._chatHostStyle(),
219
+ commands: {
220
+ ensureAttached: async () => {
221
+ await this._liveHandle()?.ensureAttached();
222
+ },
223
+ prompt: async (text) => {
224
+ const handle = this._liveHandle();
225
+ if (!handle) throw new Error("no live handle on this stage");
226
+ await handle.prompt(text);
227
+ },
228
+ steer: async (text) => {
229
+ const handle = this._liveHandle();
230
+ if (!handle) throw new Error("no live handle on this stage");
231
+ await handle.steer(text);
232
+ },
233
+ followUp: async (text) => {
234
+ const handle = this._liveHandle();
235
+ if (!handle) throw new Error("no live handle on this stage");
236
+ await handle.followUp(text);
237
+ },
238
+ interrupt: async () => {
239
+ const handle = this._liveHandle();
240
+ if (!handle) return;
241
+ const status = this._currentStage()?.status ?? handle.status;
242
+ if (status === "pending" || status === "running" || status === "awaiting_input") {
243
+ await handle.pause();
244
+ return;
245
+ }
246
+ await handle.agentSession?.abort();
247
+ },
248
+ resume: async (message) => {
249
+ const handle = this._liveHandle();
250
+ if (!handle) throw new Error("no live handle on this stage");
251
+ this.localPaused = true;
252
+ await handle.resume(message);
253
+ this.localPaused = false;
254
+ },
255
+ runBash: async (request) => {
256
+ const handle = this._liveHandle();
257
+ if (!handle) throw new Error("no live handle on this stage");
258
+ await handle.ensureAttached();
259
+ const agentSession = handle.agentSession;
260
+ if (!agentSession) throw new Error("no live agent session on this stage");
261
+ return agentSession.executeBash(request.command, request.onChunk, {
262
+ excludeFromContext: request.excludeFromContext,
263
+ });
264
+ },
265
+ abortBash: async () => {
266
+ this._liveHandle()?.agentSession?.abortBash();
267
+ },
268
+ abortCompaction: async () => {
269
+ this._liveHandle()?.agentSession?.abortCompaction();
270
+ },
271
+ handleSlashCommand: async (text) => this._handleSlashCommand(text),
272
+ },
273
+ isBashRunning: () => this._liveHandle()?.agentSession?.isBashRunning === true,
274
+ requestRender: opts.requestRender,
275
+ getAgentSession: () => this._liveHandle()?.agentSession,
276
+ isStreaming: () => this._liveHandle()?.isStreaming === true,
277
+ isPaused: () => this._isPaused(),
278
+ isDisabled: () => this._isBlocked() || !this._liveHandle(),
279
+ tui: opts.piTui,
280
+ keybindings: opts.piKeybindings,
281
+ editorFactory: opts.piEditorFactory,
282
+ editorTheme: editorThemeFromGraphTheme(this.theme),
283
+ getChatRenderSettings: opts.getChatRenderSettings,
284
+ footerData: opts.footerData,
285
+ renderExtraEntry: (entry) => this._noticeRow(entry),
286
+ });
287
+ this._unregisterStageUiHost = this.stageUiBroker.registerHost(this.runId, this.stageId, {
288
+ showCustomUi: (request) => {
289
+ void this._showCustomUi(request);
290
+ },
291
+ hideCustomUi: (request) => {
292
+ this._hideMountedCustomUi(request);
293
+ },
294
+ });
238
295
 
239
296
  // Seed transcript from the live SDK session at attach time, plus any
240
297
  // stage notices the workflow body has already recorded.
@@ -257,72 +314,62 @@ export class StageChatView implements Component, Focusable {
257
314
  // `stage.setModel`, `stage.compact`, …) so they thread through the
258
315
  // transcript without a special render path.
259
316
  changed = this._absorbStageNotices(stage) || changed;
260
- this._syncAnimationTick();
317
+ this.chatHost.syncAnimationTick();
261
318
  if (changed) this.requestRender?.();
262
319
  });
263
320
 
264
321
  if (this.handle) {
265
322
  this._unsubscribeHandle = this.handle.subscribe((event) => {
266
- const changed = this._appendEvent(event);
267
- this._syncAnimationTick();
268
- if (changed) this._requestEventRender();
323
+ this.chatHost.applyAgentEvent(event);
269
324
  });
270
325
  }
271
- this._syncAnimationTick();
326
+ this.chatHost.syncAnimationTick();
272
327
  }
273
328
 
274
- private _createEditor(
275
- tui: TUI | undefined,
276
- keybindings: unknown,
277
- editorFactory:
278
- | ((
279
- tui: TUI,
280
- theme: EditorTheme,
281
- keybindings: unknown,
282
- ) => EditorComponent)
283
- | undefined,
284
- ): EditorComponent | undefined {
285
- if (!tui || !keybindings) return undefined;
286
- const editorTheme = editorThemeFromGraphTheme(this.theme);
287
- const editor =
288
- this._createInheritedEditor(
289
- tui,
290
- editorTheme,
291
- keybindings,
292
- editorFactory,
293
- ) ??
294
- new CustomEditor(
295
- tui,
296
- editorTheme,
297
- keybindings as ConstructorParameters<typeof CustomEditor>[2],
298
- { paddingX: 0, autocompleteMaxVisible: 5 },
299
- );
300
- editor.onChange = (text) => {
301
- this.inputBuffer = text;
302
- };
303
- editor.onSubmit = (text) => {
304
- void this._submit("auto", text);
329
+ private _chatHostStyle(): ChatSessionHostStyle {
330
+ return {
331
+ dim: (text) => paint(text, this.theme.dim),
332
+ text: (text) => paint(text, this.theme.text),
333
+ textMuted: (text) => paint(text, this.theme.textMuted),
334
+ accent: (text) => paint(text, this.theme.accent),
335
+ accentBold: (text) => paint(text, this.theme.accent, { bold: true }),
336
+ rule: (hex, text) => hexToAnsi(hex) + text + RESET,
337
+ cursor: () => cursorBlock(),
338
+ blank: (width) => this._blank(width),
339
+ editorRuleColor: (disabled, agentSession, state) =>
340
+ this._editorRuleColor(disabled, agentSession, state),
305
341
  };
306
- return editor;
307
342
  }
308
343
 
309
- private _createInheritedEditor(
310
- tui: TUI,
311
- editorTheme: EditorTheme,
312
- keybindings: unknown,
313
- editorFactory:
314
- | ((
315
- tui: TUI,
316
- theme: EditorTheme,
317
- keybindings: unknown,
318
- ) => EditorComponent)
319
- | undefined,
320
- ): EditorComponent | undefined {
321
- if (!editorFactory) return undefined;
344
+ private async _showCustomUi(request: StageCustomUiRequest): Promise<void> {
345
+ this.mountedCustomUi?.component.dispose?.();
346
+ this.mountedCustomUi = null;
347
+ if (!this.piTui || this.piTheme === undefined || this.piKeybindings === undefined) {
348
+ this.stageUiBroker.reject(
349
+ request,
350
+ new Error("pi-workflows: stage custom UI cannot mount without attached TUI host"),
351
+ );
352
+ return;
353
+ }
322
354
  try {
323
- return editorFactory(tui, editorTheme, keybindings);
324
- } catch {
325
- return undefined;
355
+ this.mountedCustomUi = await mountStageCustomUi(
356
+ request,
357
+ this.piTui,
358
+ this.piTheme,
359
+ this.piKeybindings,
360
+ this.stageUiBroker,
361
+ () => {
362
+ if (this.mountedCustomUi?.request.id !== request.id) return;
363
+ this.mountedCustomUi.component.dispose?.();
364
+ this.mountedCustomUi = null;
365
+ this.chatHost.focused = this.focused;
366
+ this.chatHost.scrollToBottom();
367
+ this.requestRender?.();
368
+ },
369
+ );
370
+ this.requestRender?.();
371
+ } catch (error) {
372
+ this.stageUiBroker.reject(request, error);
326
373
  }
327
374
  }
328
375
 
@@ -331,115 +378,15 @@ export class StageChatView implements Component, Focusable {
331
378
  // -------------------------------------------------------------------------
332
379
 
333
380
  private _snapshotMessagesFromHandle(): void {
334
- if (!this.handle) return;
335
- this.liveChat.appendMessages(this.handle.messages);
381
+ const handle = this._liveHandle();
382
+ if (!handle) return;
383
+ this.chatHost.appendMessages(handle.messages);
336
384
  }
337
385
 
338
386
  private _snapshotMessagesFromSessionFile(
339
387
  stage: StageSnapshot | undefined,
340
388
  ): void {
341
- if (this.transcript.length > 0) return;
342
- const sessionFile = this.handle?.sessionFile ?? stage?.sessionFile;
343
- if (sessionFile === undefined) return;
344
-
345
- let messages: readonly AgentSnapshotMessage[];
346
- try {
347
- messages = SessionManager.open(sessionFile).buildSessionContext()
348
- .messages as readonly AgentSnapshotMessage[];
349
- } catch {
350
- return;
351
- }
352
-
353
- this.liveChat.appendMessages(messages);
354
- }
355
-
356
- private _appendEvent(event: AgentSessionEvent): boolean {
357
- // Shared live transcript ingestion covers assistant/user/custom messages
358
- // and tool start/update/end rows. StageChatView keeps workflow-only status
359
- // events (pause, compaction captions, animation state) locally.
360
- const type = String((event as { type?: unknown }).type ?? "");
361
- if (type === "message_start") {
362
- const message = (event as { message?: unknown }).message;
363
- if (isUserMessageLike(message)) {
364
- const signature = userMessageSignature(
365
- extractMessageText(message.content),
366
- );
367
- if (this.optimisticUserSignatures.delete(signature)) return false;
368
- }
369
- }
370
- if (isSharedLiveChatEvent(type)) {
371
- const changed = this.liveChat.applyEvent(event);
372
- const toolCallEvent = assistantToolCallEvent(event);
373
- const changedByToolCall = toolCallEvent !== undefined
374
- ? this.liveChat.applyEvent(toolCallEvent)
375
- : false;
376
- return changed || changedByToolCall;
377
- }
378
- switch (type) {
379
- case "agent_start":
380
- this.sdkBusy = true;
381
- this.liveChat.clearPendingTools();
382
- this.statusMessage = "";
383
- return true;
384
-
385
- case "agent_end":
386
- this.sdkBusy = false;
387
- this.workingMessage = undefined;
388
- this.liveChat.clearPendingTools();
389
- this.statusMessage = "";
390
- return true;
391
-
392
- case "turn_start":
393
- this.workingMessage = pickWhimsicalWorkingMessage();
394
- return true;
395
-
396
- case "turn_end":
397
- this.workingMessage = undefined;
398
- return true;
399
-
400
- case "queue_update": {
401
- const queue = event as Extract<AgentSessionEvent, { type: "queue_update" }>;
402
- this.pendingSteeringMessages = queue.steering;
403
- this.pendingFollowUpMessages = queue.followUp;
404
- return true;
405
- }
406
-
407
- // Compatibility with older/headless shims that predate the SDK's
408
- // tool_execution_* events. Project these shims into coding-agent's live
409
- // controller rather than maintaining a second workflow tool renderer.
410
- case "tool_call":
411
- case "tool_use":
412
- return this.liveChat.applyEvent(legacyToolStartEvent(event));
413
-
414
- case "tool_result":
415
- return this.liveChat.applyEvent(legacyToolResultEvent(event));
416
-
417
- case "thinking_delta":
418
- case "thinking":
419
- return this.liveChat.applyEvent(legacyThinkingEvent(event));
420
-
421
- case "compaction_start":
422
- this.sdkBusy = true;
423
- this.statusMessage = "compacting context…";
424
- return true;
425
-
426
- case "compaction_end":
427
- this.sdkBusy = false;
428
- this.statusMessage = "";
429
- return true;
430
-
431
- case "auto_retry_start":
432
- this.sdkBusy = true;
433
- this.statusMessage = "retrying…";
434
- return true;
435
-
436
- case "auto_retry_end":
437
- this.statusMessage = "";
438
- return true;
439
-
440
- default:
441
- return false;
442
- }
389
+ this.chatHost.loadSessionFile(this._liveHandle()?.sessionFile ?? stage?.sessionFile);
443
390
  }
444
391
 
445
392
  private _absorbStageNotices(stage: StageSnapshot | undefined): boolean {
@@ -450,7 +397,7 @@ export class StageChatView implements Component, Focusable {
450
397
  if (this.seenNoticeIds.has(n.id)) continue;
451
398
  this.seenNoticeIds.add(n.id);
452
399
  changed = true;
453
- this.transcript.push({
400
+ this.chatHost.appendExtraEntry({
454
401
  role: "notice",
455
402
  noticeId: n.id,
456
403
  kind: n.kind,
@@ -484,44 +431,15 @@ export class StageChatView implements Component, Focusable {
484
431
  if (typeof reported !== "number" || !Number.isFinite(reported)) {
485
432
  return VIEW_LINE_COUNT;
486
433
  }
487
- return Math.max(VIEW_LINE_COUNT, Math.floor(reported));
434
+ return resolveStageChatViewportRows(reported, VIEW_LINE_COUNT);
488
435
  }
489
436
 
490
- private _isStreaming(): boolean {
491
- return this.sdkBusy || Boolean(this.handle?.isStreaming);
437
+ private _liveHandle(): StageControlHandle | undefined {
438
+ return this.handle?.isDisposed === true ? undefined : this.handle;
492
439
  }
493
440
 
494
- private _hasPendingToolEntries(): boolean {
495
- return this.liveChat.pendingToolIds().length > 0;
496
- }
497
-
498
- private _syncAnimationTick(): void {
499
- const shouldAnimate =
500
- this._isStreaming() || (this.sdkBusy && this._hasPendingToolEntries());
501
- if (shouldAnimate && !this.animationTimer) {
502
- this.animationTimer = setInterval(() => {
503
- this.requestRender?.();
504
- }, ANIMATION_FRAME_MS);
505
- this.animationTimer.unref?.();
506
- return;
507
- }
508
- if (!shouldAnimate && this.animationTimer) {
509
- clearInterval(this.animationTimer);
510
- this.animationTimer = undefined;
511
- }
512
- }
513
-
514
- private _requestEventRender(): void {
515
- if (!this._isStreaming()) {
516
- this.requestRender?.();
517
- return;
518
- }
519
- if (this.renderThrottleTimer) return;
520
- this.renderThrottleTimer = setTimeout(() => {
521
- this.renderThrottleTimer = undefined;
522
- this.requestRender?.();
523
- }, STREAMING_RENDER_THROTTLE_MS);
524
- this.renderThrottleTimer.unref?.();
441
+ private _isStreaming(): boolean {
442
+ return this.chatHost.isStreaming();
525
443
  }
526
444
 
527
445
  private _isBlocked(): boolean {
@@ -534,6 +452,32 @@ export class StageChatView implements Component, Focusable {
534
452
  return this.localPaused || stage?.status === "paused";
535
453
  }
536
454
 
455
+ private _isReadOnlyArchive(stage: StageSnapshot | undefined = this._currentStage()): boolean {
456
+ if (this._liveHandle()) return false;
457
+ if (!stage) return true;
458
+ return stage.status === "completed" || stage.status === "failed" || Boolean(stage.sessionFile);
459
+ }
460
+
461
+ private async _handleSlashCommand(text: string): Promise<boolean> {
462
+ const [command, ...rest] = text.trim().split(/\s+/);
463
+ switch (command) {
464
+ case "/compact": {
465
+ const handle = this._liveHandle();
466
+ if (!handle) return false;
467
+ await handle.ensureAttached();
468
+ if (!handle.agentSession) return false;
469
+ await handle.agentSession.compact(rest.join(" ") || undefined);
470
+ return true;
471
+ }
472
+ case "/quit":
473
+ case "/exit":
474
+ this.onClose();
475
+ return true;
476
+ default:
477
+ return false;
478
+ }
479
+ }
480
+
537
481
  // -------------------------------------------------------------------------
538
482
  // Top-level render — composes header / body / usage / editor / footer
539
483
  // -------------------------------------------------------------------------
@@ -542,44 +486,54 @@ export class StageChatView implements Component, Focusable {
542
486
  const w = Math.max(40, width);
543
487
  const stage = this._currentStage();
544
488
  const blocked = this._isBlocked();
545
- const streaming = this._isStreaming() && !blocked;
546
489
 
490
+ this.chatHost.focused = this.focused;
547
491
  const headerLines = this._renderHeader(w, stage);
548
492
  const sepLines = [this._sepRule(w)];
549
- const pendingLines = this._renderPendingMessages(w);
550
- const workingLines = this._renderWorkingStatus(w, stage, { streaming });
551
- const usageLines = this._renderUsage(w);
552
- const editorLines = this._renderEditor(w, blocked);
553
- const footerLines = this._renderFooter(w);
554
-
555
- const fixed =
556
- HEADER_ROWS +
557
- SEP_ROWS +
558
- pendingLines.length +
559
- workingLines.length +
560
- usageLines.length +
561
- editorLines.length +
562
- footerLines.length;
493
+ const customUiActive = this.mountedCustomUi !== null;
494
+ const readOnlyArchive = this._isReadOnlyArchive(stage);
495
+ const pendingLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderPendingMessages(w);
496
+ const workingLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderWorkingStatus(w);
497
+ const usageLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderUsage(w);
498
+ const editorLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderEditor(w);
499
+ const footerLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderFooter(w);
500
+
563
501
  const totalRows = this._viewLineCount();
564
- const bodyBudget = Math.max(1, totalRows - fixed);
565
- this.bodyViewport.setVisibleRows(bodyBudget);
566
- if (blocked) this.bodyViewport.scrollToBottom();
567
- const bodyLines = blocked
568
- ? this._renderBlockedBody(w, bodyBudget, stage)
569
- : this._renderBody(w, bodyBudget);
502
+ const plan = planStageChatFrame({
503
+ viewportRows: totalRows,
504
+ headerRows: HEADER_ROWS,
505
+ separatorRows: SEP_ROWS,
506
+ pendingRows: pendingLines.length,
507
+ workingRows: workingLines.length,
508
+ usageRows: usageLines.length,
509
+ editorRows: editorLines.length,
510
+ footerRows: footerLines.length,
511
+ });
512
+ const visiblePendingLines = pendingLines.slice(0, plan.pendingRows);
513
+ const visibleWorkingLines = workingLines.slice(0, plan.workingRows);
514
+ const visibleUsageLines = usageLines.slice(0, plan.usageRows);
515
+ const visibleEditorLines = editorLines.slice(0, plan.editorRows);
516
+ const visibleFooterLines = footerLines.slice(0, plan.footerRows);
517
+ const bodyBudget = plan.bodyRows;
518
+ if (blocked) this.chatHost.scrollToBottom();
519
+ const bodyLines = customUiActive
520
+ ? this._renderCustomUiBody(w, bodyBudget)
521
+ : blocked
522
+ ? this._renderBlockedBody(w, bodyBudget, stage)
523
+ : readOnlyArchive
524
+ ? this._renderReadOnlyArchiveBody(w, bodyBudget, stage)
525
+ : this.chatHost.renderBody(w, bodyBudget);
570
526
  const lines = [
571
527
  ...headerLines,
572
528
  ...sepLines,
573
529
  ...bodyLines,
574
- ...pendingLines,
575
- ...workingLines,
576
- ...usageLines,
577
- ...editorLines,
578
- ...footerLines,
530
+ ...visiblePendingLines,
531
+ ...visibleWorkingLines,
532
+ ...visibleUsageLines,
533
+ ...visibleEditorLines,
534
+ ...visibleFooterLines,
579
535
  ];
580
- while (lines.length < totalRows) lines.push(this._blank(w));
581
- if (lines.length > totalRows) lines.length = totalRows;
582
- return lines;
536
+ return fitStageChatFrame(lines, totalRows, this._blank(w));
583
537
  }
584
538
 
585
539
  // -------------------------------------------------------------------------
@@ -592,85 +546,39 @@ export class StageChatView implements Component, Focusable {
592
546
  ): string[] {
593
547
  const t = this.theme;
594
548
  const stageName = stage?.name ?? "stage";
595
- const status = stage?.status ?? (this.handle ? "pending" : "completed");
596
549
 
597
- // Left side: `▎ STAGE <wf> / <stage>`
550
+ // Left side: ` STAGE <wf> / <stage>`
598
551
  const left =
599
- paint("", t.mauve, { bold: true }) +
552
+ paint(" ", t.mauve, { bold: true }) +
600
553
  paint("STAGE", t.textMuted, { bold: true }) +
601
554
  " " +
602
555
  paint(this.workflowName, t.textMuted) +
603
556
  paint(" / ", t.dim) +
604
557
  paint(stageName, t.text, { bold: true });
605
558
 
606
- // Right side: stage meta · status pill
559
+ // Right side: stable session metadata only. Avoid workflow-status chrome
560
+ // in the embedded chat so the surface does not change colour when the
561
+ // workflow stage settles.
607
562
  const meta = this._headerMeta(stage);
608
- const pill = this._statusPill(status);
609
- const right = (meta ? paint(meta, t.dim) + " " : "") + pill.styled + " ";
563
+ const right = meta ? paint(meta, t.dim) + " " : "";
610
564
 
611
565
  const leftW =
612
566
  visibleWidth(this.workflowName) +
613
567
  visibleWidth(stageName) +
614
568
  visibleWidth(" STAGE / ") +
615
569
  1;
616
- const rightW = visibleWidth(meta) + (meta ? 2 : 0) + pill.width + 1;
570
+ const rightW = visibleWidth(meta) + (meta ? 1 : 0);
617
571
  const gap = Math.max(1, width - leftW - rightW);
618
572
  return [left + " ".repeat(gap) + right];
619
573
  }
620
574
 
621
575
  private _headerMeta(stage: StageSnapshot | undefined): string {
622
576
  const parts: string[] = [];
623
- if (stage) {
624
- const dur = stageDurationText(stage);
625
- if (dur) parts.push(dur);
626
- }
627
577
  const sid = this.handle?.sessionId ?? stage?.sessionId;
628
578
  if (sid) parts.push(`session ${shortenId(sid)}`);
629
579
  return parts.join(" · ");
630
580
  }
631
581
 
632
- /**
633
- * Render an inline ` ● status ` pill with the status colour applied to a
634
- * tinted background. Matches the mockup's `.status-pill` vocabulary.
635
- */
636
- private _statusPill(status: string): { styled: string; width: number } {
637
- const t = this.theme;
638
- const map: Record<string, { fg: string; bg: string; label: string }> = {
639
- pending: { fg: t.dim, bg: blendBg(t.bg, t.dim, 0.18), label: "pending" },
640
- running: {
641
- fg: t.accent,
642
- bg: blendBg(t.bg, t.accent, 0.18),
643
- label: "running",
644
- },
645
- paused: {
646
- fg: t.warning,
647
- bg: blendBg(t.bg, t.warning, 0.18),
648
- label: "paused",
649
- },
650
- blocked: {
651
- fg: t.warning,
652
- bg: blendBg(t.bg, t.warning, 0.18),
653
- label: "blocked",
654
- },
655
- completed: {
656
- fg: t.success,
657
- bg: blendBg(t.bg, t.success, 0.18),
658
- label: "completed",
659
- },
660
- failed: {
661
- fg: t.error,
662
- bg: blendBg(t.bg, t.error, 0.18),
663
- label: "failed",
664
- },
665
- };
666
- const cfg = map[status] ?? map.pending!;
667
- const body = ` ● ${cfg.label} `;
668
- return {
669
- styled: hexBg(cfg.bg) + hexToAnsi(cfg.fg) + BOLD + body + RESET,
670
- width: visibleWidth(body),
671
- };
672
- }
673
-
674
582
  private _sepRule(width: number): string {
675
583
  return hexToAnsi(this.theme.borderDim) + "─".repeat(width) + RESET;
676
584
  }
@@ -679,6 +587,50 @@ export class StageChatView implements Component, Focusable {
679
587
  // Body — welcome panel / banner + transcript / blocked
680
588
  // -------------------------------------------------------------------------
681
589
 
590
+ private _renderReadOnlyArchiveBody(
591
+ width: number,
592
+ budget: number,
593
+ stage: StageSnapshot | undefined,
594
+ ): string[] {
595
+ const t = this.theme;
596
+ const calloutRows = 6;
597
+ const transcriptBudget = Math.max(1, budget - calloutRows);
598
+ const lines = this.chatHost.renderBody(width, transcriptBudget);
599
+ const callout: string[] = [];
600
+ callout.push(this._blank(width));
601
+ callout.push(
602
+ ...this._bannerLines(
603
+ width,
604
+ "info",
605
+ "◌",
606
+ "READ-ONLY SESSION",
607
+ stage?.sessionFile ? "archived transcript" : "no live chat session",
608
+ ),
609
+ );
610
+ callout.push(
611
+ ...new Text(
612
+ paint("This node is no longer attached to a live chat session.", t.textMuted),
613
+ 2,
614
+ 0,
615
+ ).render(width),
616
+ );
617
+ callout.push(
618
+ ...new Text(
619
+ paint("esc", t.accent, { bold: true }) +
620
+ paint(" close", t.textMuted) +
621
+ paint(" · ", t.dim) +
622
+ paint("ctrl+d", t.accent, { bold: true }) +
623
+ paint(" return to graph", t.textMuted),
624
+ 2,
625
+ 0,
626
+ ).render(width),
627
+ );
628
+ lines.push(...callout);
629
+ while (lines.length < budget) lines.push(this._blank(width));
630
+ if (lines.length > budget) lines.length = budget;
631
+ return lines;
632
+ }
633
+
682
634
  private _renderBlockedBody(
683
635
  width: number,
684
636
  budget: number,
@@ -721,87 +673,13 @@ export class StageChatView implements Component, Focusable {
721
673
  return lines;
722
674
  }
723
675
 
724
- private _renderBody(
725
- width: number,
726
- budget: number,
727
- ): string[] {
728
- const components: Component[] = [];
729
- // Base chat body: delegate transcript composition to the Pi-style
730
- // transcript component so the attached stage chat uses the same message
731
- // spacing and coding-agent message widgets as the main interactive chat.
732
- if (this.transcript.length > 0) {
733
- components.push(
734
- new ChatTranscriptComponent(this.transcript, (entry) =>
735
- this._renderEntry(entry),
736
- ),
737
- );
738
- }
739
-
740
- // Stream a static status message (e.g. "pausing…") as a dim trailing row.
741
- if (this.statusMessage) {
742
- components.push(new Spacer(1));
743
- components.push(
744
- new Text(paint(this.statusMessage, this.theme.dim), 2, 0),
745
- );
746
- }
747
-
748
- this.bodyViewport.setComponents(components);
749
- return this.bodyViewport.render(width);
750
- }
751
-
752
- // -------------------------------------------------------------------------
753
- // Transcript entry → pi/coding-agent Component. Stage chat deliberately uses
754
- // the same exported message/tool components as the main interactive chat
755
- // instead of maintaining parallel workflow-specific bubbles.
756
- // -------------------------------------------------------------------------
757
-
758
- private _renderEntry(entry: TranscriptEntry): Component {
759
- if (isChatMessageEntry(entry)) {
760
- return renderChatMessageEntry(
761
- this._streamingWindowedEntry(entry),
762
- this._chatMessageRenderOptions(),
763
- );
764
- }
765
- return this._noticeRow(entry);
766
- }
767
-
768
- private _streamingWindowedEntry(entry: ChatMessageEntry): ChatMessageEntry {
769
- if (!this._isStreaming() || this.bodyViewport.getScrollFromBottom() !== 0) {
770
- return entry;
771
- }
772
- if (entry.kind !== "assistant") return entry;
773
- const content = entry.message.content.map((item) => {
774
- if (item.type === "text") {
775
- return { ...item, text: tailStreamingText(item.text) };
776
- }
777
- if (item.type === "thinking") {
778
- return { ...item, thinking: tailStreamingText(item.thinking) };
779
- }
780
- return item;
781
- });
782
- return {
783
- ...entry,
784
- message: {
785
- ...entry.message,
786
- content,
787
- },
788
- };
789
- }
790
-
791
- private _chatMessageRenderOptions(): ChatMessageRenderOptions {
792
- const inherited = this.getChatRenderSettings?.();
793
- return {
794
- ...inherited,
795
- ui: this._toolTui(),
796
- cwd: this.handle?.agentSession?.sessionManager.getCwd() ?? process.cwd(),
797
- showImages: inherited?.showImages ?? true,
798
- };
799
- }
800
-
801
- private _toolTui(): TUI {
802
- return {
803
- requestRender: () => this.requestRender?.(),
804
- } as TUI;
676
+ private _renderCustomUiBody(width: number, budget: number): string[] {
677
+ const component = this.mountedCustomUi?.component;
678
+ if (component) setComponentFocused(component, this.focused);
679
+ const lines = component ? component.render(width) : [];
680
+ const framed = lines.slice(0, budget);
681
+ while (framed.length < budget) framed.push(this._blank(width));
682
+ return framed;
805
683
  }
806
684
 
807
685
  private _noticeRow(entry: NoticeEntry): Component {
@@ -823,14 +701,20 @@ export class StageChatView implements Component, Focusable {
823
701
  // -------------------------------------------------------------------------
824
702
 
825
703
  private _banner(
826
- kind: "warning" | "success" | "error",
704
+ kind: "warning" | "success" | "error" | "info",
827
705
  glyph: string,
828
706
  label: string,
829
707
  meta: string,
830
708
  ): Component {
831
709
  const t = this.theme;
832
710
  const fg =
833
- kind === "warning" ? t.warning : kind === "success" ? t.success : t.error;
711
+ kind === "warning"
712
+ ? t.warning
713
+ : kind === "success"
714
+ ? t.success
715
+ : kind === "info"
716
+ ? t.info
717
+ : t.error;
834
718
  const bg = blendBg(t.bg, fg, 0.1);
835
719
  const head =
836
720
  paintOnFill(glyph, fg, { bold: true }) +
@@ -849,7 +733,7 @@ export class StageChatView implements Component, Focusable {
849
733
  */
850
734
  private _bannerLines(
851
735
  width: number,
852
- kind: "warning" | "success" | "error",
736
+ kind: "warning" | "success" | "error" | "info",
853
737
  glyph: string,
854
738
  label: string,
855
739
  meta: string,
@@ -857,42 +741,14 @@ export class StageChatView implements Component, Focusable {
857
741
  return this._banner(kind, glyph, label, meta).render(width);
858
742
  }
859
743
 
860
- // -------------------------------------------------------------------------
861
- // Editor — top rule + ` ❯ … ` + bottom rule
862
- // -------------------------------------------------------------------------
863
-
864
- private _renderEditor(width: number, blocked: boolean): string[] {
865
- const t = this.theme;
866
- // Disabled only when no live chat handle exists or workflow dependencies
867
- // are blocked. A settled attached stage remains a regular chat session.
868
- const disabled = blocked || !this.handle;
869
- const ruleHex = this._editorRuleColor(disabled);
870
- if (!disabled && this.editor) {
871
- setEditorFocused(this.editor, this.focused);
872
- setEditorPlaceholder(this.editor, undefined);
873
- setEditorBorderColor(this.editor, ruleHex);
874
- return this.editor.render(width);
875
- }
876
- if (this.editor) setEditorFocused(this.editor, false);
877
- const rule = hexToAnsi(ruleHex) + "─".repeat(width) + RESET;
878
-
879
- const glyphHex = disabled ? t.dim : t.accent;
880
- const available = Math.max(1, width - 3);
881
- const value = this.inputBuffer
882
- ? paint(truncateToWidth(this.inputBuffer, available), t.text) + cursorBlock()
883
- : disabled
884
- ? ""
885
- : cursorBlock();
886
-
887
- const left = paint("❯", glyphHex, { bold: true }) + " " + value;
888
- const gap = Math.max(0, width - visibleWidth(stripAnsi(left)));
889
- const body = left + " ".repeat(gap);
890
- return [rule, body, rule];
891
- }
892
-
893
- private _editorRuleColor(disabled: boolean): string {
744
+ private _editorRuleColor(
745
+ disabled: boolean,
746
+ agentSession: AgentSession | undefined,
747
+ state?: { isBashMode: boolean },
748
+ ): string {
894
749
  if (disabled) return this.theme.borderDim;
895
- const level = this.handle?.agentSession?.state.thinkingLevel ?? "off";
750
+ if (state?.isBashMode) return this.theme.warning;
751
+ const level = agentSession?.state.thinkingLevel ?? "off";
896
752
  switch (level) {
897
753
  case "minimal":
898
754
  return this.theme.borderDim;
@@ -910,71 +766,6 @@ export class StageChatView implements Component, Focusable {
910
766
  }
911
767
  }
912
768
 
913
- // -------------------------------------------------------------------------
914
- // Working, usage + footer — mirrors the main chat composer stack
915
- // -------------------------------------------------------------------------
916
-
917
- private _renderWorkingStatus(
918
- width: number,
919
- stage: StageSnapshot | undefined,
920
- flags: { streaming: boolean },
921
- ): string[] {
922
- if (!flags.streaming) return [];
923
- const t = this.theme;
924
- const dur = stageDurationText(stage);
925
- const message = this.workingMessage ?? `Working${dur ? " · " + dur : ""}`;
926
- return new WorkingStatusComponent({
927
- spinner: spinnerFrame(),
928
- message,
929
- spinnerColor: (text) => paint(text, t.accent, { bold: true }),
930
- messageColor: (text) => paint(text, t.textMuted),
931
- }).render(width);
932
- }
933
-
934
- private _renderPendingMessages(width: number): string[] {
935
- if (
936
- this.pendingSteeringMessages.length === 0 &&
937
- this.pendingFollowUpMessages.length === 0
938
- ) {
939
- return [];
940
- }
941
- const lines = [this._blank(width)];
942
- for (const message of this.pendingSteeringMessages) {
943
- lines.push(...this._pendingMessageLine(width, "Steering", message));
944
- }
945
- for (const message of this.pendingFollowUpMessages) {
946
- lines.push(...this._pendingMessageLine(width, "Follow-up", message));
947
- }
948
- return lines;
949
- }
950
-
951
- private _pendingMessageLine(
952
- width: number,
953
- label: "Steering" | "Follow-up",
954
- message: string,
955
- ): string[] {
956
- const text = `${label}: ${message}`;
957
- return new Text(
958
- paint(truncateToWidth(text, Math.max(1, width - 2)), this.theme.dim),
959
- 1,
960
- 0,
961
- ).render(width);
962
- }
963
-
964
- private _renderUsage(width: number): string[] {
965
- const agentSession = this.handle?.agentSession;
966
- if (!agentSession) return [];
967
- return new UsageMeterComponent(agentSession).render(width);
968
- }
969
-
970
- private _renderFooter(width: number): string[] {
971
- const agentSession = this.handle?.agentSession;
972
- if (agentSession && this.footerData) {
973
- return new FooterComponent(agentSession, this.footerData).render(width);
974
- }
975
- return [];
976
- }
977
-
978
769
  // -------------------------------------------------------------------------
979
770
  // Small helpers
980
771
  // -------------------------------------------------------------------------
@@ -992,179 +783,57 @@ export class StageChatView implements Component, Focusable {
992
783
  // -------------------------------------------------------------------------
993
784
 
994
785
  handleInput(data: string): boolean {
995
- if (this.bodyViewport.handleInput(data)) {
786
+ if (this.mountedCustomUi) {
787
+ if (matchesKey(data, Key.ctrl("d"))) {
788
+ this._rejectMountedCustomUi("stage custom UI detached");
789
+ if (this._isPaused()) this.onClose();
790
+ else this.onDetach();
791
+ return true;
792
+ }
793
+ if (matchesKey(data, Key.ctrl("c"))) {
794
+ this._rejectMountedCustomUi("stage custom UI closed");
795
+ this.onClose();
796
+ return true;
797
+ }
798
+ setComponentFocused(this.mountedCustomUi.component, this.focused);
799
+ this.mountedCustomUi.component.handleInput?.(data);
800
+ this.requestRender?.();
801
+ return true;
802
+ }
803
+ if (this.chatHost.handleScrollInput(data)) {
996
804
  return true;
997
805
  }
998
- if (matchesKey(data, "ctrl+d")) {
806
+ if (matchesKey(data, Key.ctrl("d"))) {
807
+ if (this.chatHost.hasInputText()) return this.chatHost.handleInput(data);
999
808
  if (this._isPaused()) this.onClose();
1000
809
  else this.onDetach();
1001
810
  return true;
1002
811
  }
1003
- if (matchesKey(data, "escape")) {
1004
- if (this._canPause()) {
1005
- void this._pause();
1006
- } else {
1007
- this.onClose();
812
+ if (matchesKey(data, Key.escape)) {
813
+ if (
814
+ this._isStreaming() ||
815
+ this.chatHost.isBashRunning() ||
816
+ this.chatHost.isEditingBashCommand()
817
+ ) {
818
+ return this.chatHost.handleInput(data);
1008
819
  }
820
+ this.onClose();
1009
821
  return true;
1010
822
  }
1011
- if (matchesKey(data, "ctrl+c")) {
823
+ if (matchesKey(data, Key.ctrl("c"))) {
1012
824
  this.onClose();
1013
825
  return true;
1014
826
  }
827
+ const readOnlyArchive = this._isReadOnlyArchive();
828
+ if (readOnlyArchive) return true;
1015
829
  const blocked = this._isBlocked();
1016
- if (matchesKey(data, "ctrl+f")) {
1017
- if (blocked) return true;
1018
- void this._submit("followUp");
1019
- return true;
1020
- }
1021
- if (this.editor) {
830
+ if (matchesKey(data, Key.ctrl("f"))) {
1022
831
  if (blocked) return true;
1023
- this.editor.handleInput(data);
832
+ void this.chatHost.submit("followUp");
1024
833
  return true;
1025
834
  }
1026
- if (matchesKey(data, "enter")) {
1027
- if (blocked) return true;
1028
- void this._submit("auto");
1029
- return true;
1030
- }
1031
- if (matchesKey(data, "backspace")) {
1032
- if (blocked) return true;
1033
- this.inputBuffer = this.inputBuffer.slice(0, -1);
1034
- return true;
1035
- }
1036
- if (data.length === 1 && data >= " " && data <= "~") {
1037
- if (blocked) return true;
1038
- this.inputBuffer += data;
1039
- return true;
1040
- }
1041
- return false;
1042
- }
1043
-
1044
- private _canPause(): boolean {
1045
- if (!this.handle || this.localPaused || this._isBlocked()) return false;
1046
- const stage = this._currentStage();
1047
- if (stage?.status === "paused") return false;
1048
- return this._isStreaming();
1049
- }
1050
-
1051
- private async _pause(): Promise<void> {
1052
- if (!this.handle) {
1053
- this.statusMessage = "no live handle on this stage";
1054
- this.requestRender?.();
1055
- return;
1056
- }
1057
- this.localPaused = true;
1058
- this.statusMessage = "pausing…";
1059
- this.requestRender?.();
1060
- try {
1061
- await this.handle.pause();
1062
- this.sdkBusy = false;
1063
- this.statusMessage = "";
1064
- } catch (err) {
1065
- this.statusMessage = `pause failed: ${err instanceof Error ? err.message : String(err)}`;
1066
- this.localPaused = false;
1067
- } finally {
1068
- this._syncAnimationTick();
1069
- this.requestRender?.();
1070
- }
1071
- }
1072
-
1073
- private async _resume(message?: string): Promise<void> {
1074
- if (!this.handle) {
1075
- this.statusMessage = "no live handle on this stage";
1076
- this.requestRender?.();
1077
- return;
1078
- }
1079
- this.localPaused = true;
1080
- this.sdkBusy = true;
1081
- this.statusMessage = "resuming…";
1082
- this._syncAnimationTick();
1083
- this.requestRender?.();
1084
- try {
1085
- await this.handle.resume(message);
1086
- this.localPaused = false;
1087
- this.sdkBusy = false;
1088
- this.statusMessage = "";
1089
- } catch (err) {
1090
- this.sdkBusy = false;
1091
- this.statusMessage = `resume failed: ${err instanceof Error ? err.message : String(err)}`;
1092
- } finally {
1093
- this._syncAnimationTick();
1094
- this.requestRender?.();
1095
- }
1096
- }
1097
-
1098
- private async _submit(
1099
- mode: "auto" | "followUp",
1100
- submittedText?: string,
1101
- ): Promise<void> {
1102
- const text = (submittedText ?? this.inputBuffer).trim();
1103
- if (!text) return;
1104
- this.inputBuffer = "";
1105
- this.editor?.setText("");
1106
- if (!this.handle) {
1107
- this.statusMessage = "no live handle on this stage";
1108
- this.transcript.push({
1109
- role: "system",
1110
- kind: "system",
1111
- text: "(no live handle — message dropped)",
1112
- });
1113
- this.requestRender?.();
1114
- return;
1115
- }
1116
- const isPaused = this._isPaused();
1117
- const isStreaming = this._isStreaming();
1118
- const shouldAppendOptimisticUser = mode === "auto" && !isStreaming;
1119
- if (shouldAppendOptimisticUser) {
1120
- this.liveChat.appendUserText(text);
1121
- this.bodyViewport.scrollToBottom();
1122
- this.optimisticUserSignatures.add(userMessageSignature(text));
1123
- }
1124
- this.requestRender?.();
1125
- try {
1126
- if (isPaused) {
1127
- await this._resume(text);
1128
- return;
1129
- }
1130
- if (mode === "followUp") {
1131
- await this._queueFollowUp(text);
1132
- return;
1133
- }
1134
- if (isStreaming) {
1135
- await this._queueSteer(text);
1136
- } else {
1137
- this.sdkBusy = true;
1138
- this._syncAnimationTick();
1139
- await this.handle.ensureAttached();
1140
- await this.handle.prompt(text);
1141
- this.sdkBusy = false;
1142
- this._syncAnimationTick();
1143
- }
1144
- } catch (err) {
1145
- this.sdkBusy = false;
1146
- this.statusMessage = err instanceof Error ? err.message : String(err);
1147
- this._syncAnimationTick();
1148
- this.requestRender?.();
1149
- }
1150
- }
1151
-
1152
- private async _queueSteer(text: string): Promise<void> {
1153
- const agentSession = this.handle?.agentSession;
1154
- if (agentSession?.isStreaming) {
1155
- await agentSession.prompt(text, { streamingBehavior: "steer" });
1156
- return;
1157
- }
1158
- await this.handle?.steer(text);
1159
- }
1160
-
1161
- private async _queueFollowUp(text: string): Promise<void> {
1162
- const agentSession = this.handle?.agentSession;
1163
- if (agentSession?.isStreaming) {
1164
- await agentSession.prompt(text, { streamingBehavior: "followUp" });
1165
- return;
1166
- }
1167
- await this.handle?.followUp(text);
835
+ if (blocked) return true;
836
+ return this.chatHost.handleInput(data);
1168
837
  }
1169
838
 
1170
839
  invalidate(): void {
@@ -1176,38 +845,51 @@ export class StageChatView implements Component, Focusable {
1176
845
  this._unsubscribeStore = null;
1177
846
  this._unsubscribeHandle?.();
1178
847
  this._unsubscribeHandle = null;
1179
- if (this.animationTimer) {
1180
- clearInterval(this.animationTimer);
1181
- this.animationTimer = undefined;
1182
- }
1183
- if (this.renderThrottleTimer) {
1184
- clearTimeout(this.renderThrottleTimer);
1185
- this.renderThrottleTimer = undefined;
1186
- }
1187
- this.editor = undefined;
848
+ this._rejectMountedCustomUi("stage chat view disposed");
849
+ this._unregisterStageUiHost?.();
850
+ this._unregisterStageUiHost = null;
851
+ this.chatHost.dispose();
852
+ }
853
+
854
+ private _hideMountedCustomUi(request: StageCustomUiRequest): void {
855
+ const mounted = this.mountedCustomUi;
856
+ if (!mounted || mounted.request.id !== request.id) return;
857
+ this.mountedCustomUi = null;
858
+ mounted.component.dispose?.();
859
+ this.chatHost.focused = this.focused;
860
+ this.chatHost.scrollToBottom();
861
+ this.requestRender?.();
862
+ }
863
+
864
+ private _rejectMountedCustomUi(message: string): void {
865
+ const mounted = this.mountedCustomUi;
866
+ if (!mounted) return;
867
+ this.mountedCustomUi = null;
868
+ this.stageUiBroker.reject(mounted.request, new Error(`pi-workflows: ${message}`));
869
+ mounted.component.dispose?.();
1188
870
  }
1189
871
 
1190
872
  // ---- Test seams ----
1191
873
  get _inputBuffer(): string {
1192
- return this.inputBuffer;
874
+ return this.chatHost.inputText();
1193
875
  }
1194
876
  get _transcript(): ReadonlyArray<TranscriptDebugEntry> {
1195
- return this.transcript.flatMap((entry) => transcriptDebugEntries(entry));
877
+ return this.chatHost.entries().flatMap((entry) => transcriptDebugEntries(entry));
1196
878
  }
1197
879
  get _statusMessage(): string {
1198
- return this.statusMessage;
880
+ return this.chatHost.statusText();
1199
881
  }
1200
882
  get _isLocalPaused(): boolean {
1201
883
  return this.localPaused;
1202
884
  }
1203
885
  get _hasAnimationTick(): boolean {
1204
- return this.animationTimer !== undefined;
886
+ return this.chatHost.hasAnimationTick();
1205
887
  }
1206
888
  get _bodyScrollFromBottom(): number {
1207
- return this.bodyViewport.getScrollFromBottom();
889
+ return this.chatHost.bodyScrollFromBottom();
1208
890
  }
1209
891
  get _lastBodyMaxScroll(): number {
1210
- return this.bodyViewport.getMaxScroll();
892
+ return this.chatHost.bodyMaxScroll();
1211
893
  }
1212
894
  }
1213
895
 
@@ -1302,174 +984,19 @@ function transcriptDebugToolOutput(entry: TranscriptEntry): string {
1302
984
  return "";
1303
985
  }
1304
986
 
1305
- function setEditorPlaceholder(
1306
- editor: EditorComponent,
1307
- placeholder: string | undefined,
1308
- ): void {
1309
- const candidate = editor as EditorComponent & {
1310
- setPlaceholder?: (value: string | undefined) => void;
1311
- };
1312
- candidate.setPlaceholder?.(placeholder);
1313
- }
1314
-
1315
987
  function cursorBlock(): string {
1316
988
  return "\x1b[7m \x1b[0m";
1317
989
  }
1318
990
 
1319
- function setEditorBorderColor(editor: EditorComponent, hex: string): void {
1320
- const candidate = editor as EditorComponent & {
1321
- borderColor?: (text: string) => string;
1322
- };
1323
- if (candidate.borderColor !== undefined) {
1324
- candidate.borderColor = (text: string) => hexToAnsi(hex) + text + RESET;
1325
- }
1326
- }
1327
-
1328
- function setEditorFocused(editor: EditorComponent, focused: boolean): void {
1329
- const candidate = editor as EditorComponent & Partial<Focusable>;
991
+ function setComponentFocused(component: Component, focused: boolean): void {
992
+ const candidate = component as Component & Partial<Focusable>;
1330
993
  if ("focused" in candidate) candidate.focused = focused;
1331
994
  }
1332
995
 
1333
- function isSharedLiveChatEvent(type: string): boolean {
1334
- return (
1335
- type === "message_start" ||
1336
- type === "message_update" ||
1337
- type === "message_end" ||
1338
- type === "tool_execution_start" ||
1339
- type === "tool_execution_update" ||
1340
- type === "tool_execution_end"
1341
- );
1342
- }
1343
-
1344
996
  function isChatMessageEntry(entry: TranscriptEntry): entry is ChatMessageEntry {
1345
997
  return "kind" in entry && entry.role !== "notice";
1346
998
  }
1347
999
 
1348
- function isMessageLike(message: unknown): message is { role?: unknown; content?: unknown } {
1349
- return message !== null && typeof message === "object" && "role" in message;
1350
- }
1351
-
1352
- function isUserMessageLike(
1353
- message: unknown,
1354
- ): message is { role: "user"; content?: unknown } {
1355
- return isMessageLike(message) && message.role === "user";
1356
- }
1357
-
1358
- function userMessageSignature(text: string): string {
1359
- return text.trim();
1360
- }
1361
-
1362
- function assistantToolCallEvent(event: AgentSessionEvent): {
1363
- type: "tool_execution_start";
1364
- toolCallId: string;
1365
- toolName: string;
1366
- args: unknown;
1367
- } | undefined {
1368
- const assistantEvent = (event as {
1369
- assistantMessageEvent?: {
1370
- type?: unknown;
1371
- contentIndex?: unknown;
1372
- partial?: unknown;
1373
- toolCall?: unknown;
1374
- };
1375
- }).assistantMessageEvent;
1376
- const streamType = String(assistantEvent?.type ?? "");
1377
- if (!streamType.startsWith("toolcall_")) return undefined;
1378
-
1379
- const explicit = toolCallPayload(assistantEvent?.toolCall);
1380
- if (explicit) return explicit;
1381
-
1382
- const contentIndex = typeof assistantEvent?.contentIndex === "number" ? assistantEvent.contentIndex : undefined;
1383
- if (contentIndex === undefined) return undefined;
1384
- const partial = assistantEvent?.partial;
1385
- if (!isMessageLike(partial) || partial.role !== "assistant") return undefined;
1386
- const content = partial.content;
1387
- if (!Array.isArray(content)) return undefined;
1388
- return toolCallPayload(content[contentIndex]);
1389
- }
1390
-
1391
- function toolCallPayload(value: unknown): {
1392
- type: "tool_execution_start";
1393
- toolCallId: string;
1394
- toolName: string;
1395
- args: unknown;
1396
- } | undefined {
1397
- if (value === null || typeof value !== "object") return undefined;
1398
- const candidate = value as { type?: unknown; id?: unknown; name?: unknown; arguments?: unknown };
1399
- if (candidate.type !== "toolCall") return undefined;
1400
- if (typeof candidate.id !== "string" || typeof candidate.name !== "string") return undefined;
1401
- return {
1402
- type: "tool_execution_start",
1403
- toolCallId: candidate.id,
1404
- toolName: candidate.name,
1405
- args: candidate.arguments ?? {},
1406
- };
1407
- }
1408
-
1409
- function legacyToolStartEvent(event: AgentSessionEvent): {
1410
- type: "tool_execution_start";
1411
- toolCallId: string;
1412
- toolName: string;
1413
- args: unknown;
1414
- } {
1415
- const payload = event as { toolCallId?: unknown; name?: unknown; input?: unknown; args?: unknown };
1416
- const toolName = typeof payload.name === "string" ? payload.name : "tool";
1417
- const toolCallId =
1418
- typeof payload.toolCallId === "string" ? payload.toolCallId : `live-${toolName}`;
1419
- return {
1420
- type: "tool_execution_start",
1421
- toolCallId,
1422
- toolName,
1423
- args: payload.input ?? payload.args ?? {},
1424
- };
1425
- }
1426
-
1427
- function legacyToolResultEvent(event: AgentSessionEvent): {
1428
- type: "tool_execution_end";
1429
- toolCallId: string;
1430
- toolName: string;
1431
- result: unknown;
1432
- isError: boolean;
1433
- } {
1434
- const payload = event as {
1435
- toolCallId?: unknown;
1436
- name?: unknown;
1437
- output?: unknown;
1438
- isError?: unknown;
1439
- };
1440
- const toolName = typeof payload.name === "string" ? payload.name : "tool";
1441
- const toolCallId =
1442
- typeof payload.toolCallId === "string" ? payload.toolCallId : `live-${toolName}`;
1443
- const output = payload.output;
1444
- return {
1445
- type: "tool_execution_end",
1446
- toolCallId,
1447
- toolName,
1448
- result:
1449
- output !== null && typeof output === "object" && "content" in output
1450
- ? output
1451
- : { content: typeof output === "string" ? [{ type: "text", text: output }] : [] },
1452
- isError: payload.isError === true,
1453
- };
1454
- }
1455
-
1456
- function legacyThinkingEvent(event: AgentSessionEvent): {
1457
- type: "message_update";
1458
- assistantMessageEvent: { type: "thinking_delta"; delta: string };
1459
- message: { role: "assistant"; content: [] };
1460
- } {
1461
- const delta = String(
1462
- (event as { delta?: unknown }).delta ??
1463
- (event as { text?: unknown }).text ??
1464
- "",
1465
- );
1466
- return {
1467
- type: "message_update",
1468
- assistantMessageEvent: { type: "thinking_delta", delta },
1469
- message: { role: "assistant", content: [] },
1470
- };
1471
- }
1472
-
1473
1000
  function extractThinkingText(content: unknown): string {
1474
1001
  if (!Array.isArray(content)) return "";
1475
1002
  const parts: string[] = [];
@@ -1515,48 +1042,10 @@ function noticeSummary(n: StageNotice): string {
1515
1042
  return n.from ? `${base} (was ${n.from})` : base;
1516
1043
  }
1517
1044
 
1518
- function tailStreamingText(text: string): string {
1519
- if (
1520
- text.length <= STREAMING_TEXT_TAIL_CHARS &&
1521
- text.split("\n").length <= STREAMING_TEXT_TAIL_LINES
1522
- ) {
1523
- return text;
1524
- }
1525
- const byChars = text.slice(-STREAMING_TEXT_TAIL_CHARS);
1526
- const lines = byChars.split("\n");
1527
- const tail =
1528
- lines.length > STREAMING_TEXT_TAIL_LINES
1529
- ? lines.slice(-STREAMING_TEXT_TAIL_LINES).join("\n")
1530
- : byChars;
1531
- return `[earlier streaming output hidden while attached]\n\n${tail.trimStart()}`;
1532
- }
1533
-
1534
- function stageDurationText(stage: StageSnapshot | undefined): string {
1535
- if (!stage) return "";
1536
- const elapsed = elapsedStageMs(stage);
1537
- return elapsed === undefined ? "" : formatDuration(elapsed);
1538
- }
1539
-
1540
- function formatDuration(ms: number): string {
1541
- const s = Math.floor(Math.max(0, ms) / 1000);
1542
- if (s < 60) return `${s}s`;
1543
- const m = Math.floor(s / 60);
1544
- const rs = s % 60;
1545
- if (m < 60) return rs ? `${m}m ${rs}s` : `${m}m`;
1546
- const h = Math.floor(m / 60);
1547
- const rm = m % 60;
1548
- return rm ? `${h}h ${rm}m` : `${h}h`;
1549
- }
1550
-
1551
1045
  function shortenId(id: string): string {
1552
1046
  return id.length > 10 ? id.slice(0, 8) : id;
1553
1047
  }
1554
1048
 
1555
- function spinnerFrame(): string {
1556
- const idx = Math.floor(Date.now() / 80) % SPINNER_FRAMES.length;
1557
- return SPINNER_FRAMES[idx]!;
1558
- }
1559
-
1560
1049
  function bgFn(hex: string): (text: string) => string {
1561
1050
  const open = hexBg(hex);
1562
1051
  return (text: string) => open + text + RESET;