@bubblebrain-ai/bubble 0.0.52 → 0.0.53

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 (481) hide show
  1. package/README.md +4 -12
  2. package/dist/agent/routing-catalog.js +3 -2
  3. package/dist/agent/subagent/router.js +5 -0
  4. package/dist/agent/subagent/runtime.d.ts +1 -0
  5. package/dist/agent/subagent/runtime.js +13 -1
  6. package/dist/agent/subagent-control.d.ts +4 -0
  7. package/dist/agent/subagent-control.js +1 -0
  8. package/dist/agent/subagent-store.js +2 -0
  9. package/dist/agent/workflow/control.d.ts +2 -0
  10. package/dist/agent/workflow/runs.d.ts +1 -0
  11. package/dist/agent/workflow/runs.js +3 -0
  12. package/dist/agent.d.ts +11 -1
  13. package/dist/agent.js +158 -57
  14. package/dist/bin.js +2 -44
  15. package/dist/config.d.ts +14 -1
  16. package/dist/config.js +45 -4
  17. package/dist/context/budget.d.ts +7 -1
  18. package/dist/context/budget.js +25 -2
  19. package/dist/context/compact-llm.d.ts +1 -1
  20. package/dist/context/compact-llm.js +5 -2
  21. package/dist/context/overflow.js +2 -0
  22. package/dist/context/projector.d.ts +8 -0
  23. package/dist/context/projector.js +55 -6
  24. package/dist/context/tool-output-truncate.d.ts +17 -1
  25. package/dist/context/tool-output-truncate.js +171 -49
  26. package/dist/feishu/agent-host/run-driver.js +3 -0
  27. package/dist/feishu/serve.js +2 -0
  28. package/dist/goal/command.d.ts +1 -1
  29. package/dist/goal/command.js +7 -2
  30. package/dist/main.d.ts +1 -1
  31. package/dist/main.js +38 -8
  32. package/dist/memory/db.js +0 -7
  33. package/dist/model-catalog.js +25 -6
  34. package/dist/model-config.js +3 -2
  35. package/dist/model-pricing.d.ts +24 -5
  36. package/dist/model-pricing.js +0 -0
  37. package/dist/model-selection.js +13 -4
  38. package/dist/network/chatgpt-transport.js +0 -34
  39. package/dist/network/errors.d.ts +19 -0
  40. package/dist/network/errors.js +24 -0
  41. package/dist/network/provider-transport.d.ts +0 -4
  42. package/dist/network/provider-transport.js +3 -35
  43. package/dist/network/retry.d.ts +6 -1
  44. package/dist/network/retry.js +9 -0
  45. package/dist/provider-model-policy.d.ts +16 -0
  46. package/dist/provider-model-policy.js +33 -0
  47. package/dist/provider-registry.d.ts +1 -1
  48. package/dist/provider-registry.js +73 -10
  49. package/dist/provider-transform.js +32 -4
  50. package/dist/provider.d.ts +6 -2
  51. package/dist/provider.js +95 -6
  52. package/dist/question/controller.js +5 -4
  53. package/dist/question/normalize.d.ts +6 -0
  54. package/dist/question/normalize.js +11 -0
  55. package/dist/rewind.d.ts +20 -0
  56. package/dist/rewind.js +29 -0
  57. package/dist/sdk/index.d.ts +1 -1
  58. package/dist/sdk/index.js +11 -3
  59. package/dist/session.d.ts +7 -0
  60. package/dist/session.js +20 -0
  61. package/dist/skills/discovery.js +18 -0
  62. package/dist/skills/registry.d.ts +12 -2
  63. package/dist/skills/registry.js +40 -5
  64. package/dist/skills/types.d.ts +2 -0
  65. package/dist/slash-commands/commands.js +120 -81
  66. package/dist/slash-commands/registry.d.ts +2 -1
  67. package/dist/slash-commands/registry.js +3 -0
  68. package/dist/slash-commands/types.d.ts +22 -4
  69. package/dist/stats/usage.d.ts +3 -1
  70. package/dist/stats/usage.js +22 -13
  71. package/dist/tasks/manager.d.ts +4 -2
  72. package/dist/tasks/manager.js +34 -9
  73. package/dist/tasks/promotion.d.ts +2 -2
  74. package/dist/tasks/promotion.js +2 -2
  75. package/dist/tools/agent-lifecycle.js +4 -0
  76. package/dist/tools/bash.d.ts +3 -1
  77. package/dist/tools/bash.js +13 -2
  78. package/dist/tools/grep.d.ts +5 -1
  79. package/dist/tools/grep.js +318 -69
  80. package/dist/tools/index.d.ts +3 -1
  81. package/dist/tools/index.js +1 -0
  82. package/dist/tools/question.js +5 -4
  83. package/dist/tools/read.js +40 -21
  84. package/dist/tui/animation-clock.d.ts +14 -0
  85. package/dist/tui/animation-clock.js +34 -0
  86. package/dist/tui/app.d.ts +176 -0
  87. package/dist/tui/app.js +1647 -0
  88. package/dist/tui/components/approval-dialog.d.ts +43 -0
  89. package/dist/tui/components/approval-dialog.js +247 -0
  90. package/dist/tui/components/bottom-sheet.d.ts +9 -0
  91. package/dist/tui/components/bottom-sheet.js +32 -0
  92. package/dist/tui/components/context-info.d.ts +54 -0
  93. package/dist/tui/components/context-info.js +388 -0
  94. package/dist/tui/components/feedback-dialog.d.ts +56 -0
  95. package/dist/tui/components/feedback-dialog.js +262 -0
  96. package/dist/tui/components/feishu-setup-dialog.d.ts +65 -0
  97. package/dist/tui/components/feishu-setup-dialog.js +332 -0
  98. package/dist/tui/components/image-preview.d.ts +32 -0
  99. package/dist/tui/components/image-preview.js +130 -0
  100. package/dist/tui/components/plan-dialog.d.ts +34 -0
  101. package/dist/tui/components/plan-dialog.js +196 -0
  102. package/dist/tui/components/provider-key-input.d.ts +18 -0
  103. package/dist/tui/components/provider-key-input.js +49 -0
  104. package/dist/tui/components/question-dialog.d.ts +25 -0
  105. package/dist/tui/components/question-dialog.js +263 -0
  106. package/dist/tui/components/responsive-transcript.d.ts +31 -0
  107. package/dist/tui/components/responsive-transcript.js +77 -0
  108. package/dist/tui/components/rewind-picker.d.ts +62 -0
  109. package/dist/tui/components/rewind-picker.js +317 -0
  110. package/dist/tui/components/session-picker.d.ts +49 -0
  111. package/dist/tui/components/session-picker.js +303 -0
  112. package/dist/tui/components/skills-panel.d.ts +59 -0
  113. package/dist/tui/components/skills-panel.js +568 -0
  114. package/dist/tui/components/stats-panel.d.ts +40 -0
  115. package/dist/tui/components/stats-panel.js +301 -0
  116. package/dist/tui/components/streaming-message.d.ts +69 -0
  117. package/dist/tui/components/streaming-message.js +393 -0
  118. package/dist/tui/components/subagent-inspector.d.ts +28 -0
  119. package/dist/tui/components/subagent-inspector.js +127 -0
  120. package/dist/tui/components/task-inspector.d.ts +26 -0
  121. package/dist/tui/components/task-inspector.js +70 -0
  122. package/dist/tui/components/tasks-pane.d.ts +88 -0
  123. package/dist/tui/components/tasks-pane.js +416 -0
  124. package/dist/tui/components/transcript.d.ts +96 -0
  125. package/dist/tui/components/transcript.js +771 -0
  126. package/dist/tui/components/welcome.d.ts +20 -0
  127. package/dist/tui/components/welcome.js +133 -0
  128. package/dist/tui/components/workflow-inspector.d.ts +30 -0
  129. package/dist/tui/components/workflow-inspector.js +136 -0
  130. package/dist/tui/composer-autocomplete.d.ts +61 -0
  131. package/dist/tui/composer-autocomplete.js +383 -0
  132. package/dist/tui/composer-style.d.ts +7 -0
  133. package/dist/tui/composer-style.js +29 -0
  134. package/dist/tui/controller/agent-event-reducer.d.ts +75 -0
  135. package/dist/tui/controller/agent-event-reducer.js +238 -0
  136. package/dist/tui/controller/composer-controller.d.ts +95 -0
  137. package/dist/tui/controller/composer-controller.js +502 -0
  138. package/dist/tui/controller/controller.d.ts +194 -0
  139. package/dist/tui/controller/controller.js +1035 -0
  140. package/dist/tui/controller/effects.d.ts +71 -0
  141. package/dist/tui/controller/effects.js +1 -0
  142. package/dist/tui/controller/goal-runtime-controller.d.ts +47 -0
  143. package/dist/tui/controller/goal-runtime-controller.js +202 -0
  144. package/dist/tui/controller/input-queue-machine.d.ts +71 -0
  145. package/dist/tui/controller/input-queue-machine.js +121 -0
  146. package/dist/tui/controller/intents.d.ts +56 -0
  147. package/dist/tui/controller/intents.js +6 -0
  148. package/dist/tui/controller/overlay-controller.d.ts +64 -0
  149. package/dist/tui/controller/overlay-controller.js +129 -0
  150. package/dist/tui/controller/ports.d.ts +57 -0
  151. package/dist/tui/controller/ports.js +9 -0
  152. package/dist/tui/controller/session-transition.d.ts +54 -0
  153. package/dist/tui/controller/session-transition.js +68 -0
  154. package/dist/tui/controller/state.d.ts +14 -0
  155. package/dist/tui/controller/state.js +36 -0
  156. package/dist/tui/controller/task-runtime-controller.d.ts +66 -0
  157. package/dist/tui/controller/task-runtime-controller.js +158 -0
  158. package/dist/tui/footer.d.ts +37 -0
  159. package/dist/tui/footer.js +80 -0
  160. package/dist/{tui-ink → tui/formatting}/edit-diff.d.ts +1 -1
  161. package/dist/{tui-ink → tui/formatting}/edit-diff.js +1 -1
  162. package/dist/{tui-ink/app-helpers.d.ts → tui/formatting/summary.d.ts} +4 -4
  163. package/dist/{tui-ink/app-helpers.js → tui/formatting/summary.js} +2 -2
  164. package/dist/tui/fullscreen.d.ts +48 -0
  165. package/dist/tui/fullscreen.js +255 -0
  166. package/dist/tui/image-display.d.ts +4 -0
  167. package/dist/tui/image-display.js +30 -12
  168. package/dist/tui/markdown-style.d.ts +5 -0
  169. package/dist/tui/markdown-style.js +23 -0
  170. package/dist/{tui-ink → tui/model}/composer-buffer.js +1 -1
  171. package/dist/tui/model/composer-types.d.ts +17 -0
  172. package/dist/tui/model/composer-types.js +1 -0
  173. package/dist/{tui-ink → tui/model}/display-history.d.ts +7 -1
  174. package/dist/{tui-ink → tui/model}/display-reconstruct.d.ts +1 -1
  175. package/dist/{tui-ink → tui/model}/display-reconstruct.js +54 -6
  176. package/dist/{tui-ink → tui/model}/file-mentions.d.ts +2 -0
  177. package/dist/{tui-ink → tui/model}/file-mentions.js +4 -0
  178. package/dist/tui/model/image-attachment.d.ts +15 -0
  179. package/dist/tui/model/image-attachment.js +1 -0
  180. package/dist/{tui-ink → tui/model}/image-paste.d.ts +7 -1
  181. package/dist/{tui-ink → tui/model}/image-paste.js +46 -0
  182. package/dist/tui/model/input-history.d.ts +1 -0
  183. package/dist/tui/model/input-history.js +1 -0
  184. package/dist/{tui-ink → tui/model}/input-queue.d.ts +1 -1
  185. package/dist/{tui-ink → tui/model}/recent-activity.js +1 -1
  186. package/dist/{tui-ink → tui/model}/subagent-view.d.ts +17 -2
  187. package/dist/{tui-ink → tui/model}/subagent-view.js +125 -4
  188. package/dist/{tui-ink → tui/model}/submit-dedupe.d.ts +1 -1
  189. package/dist/tui/model/theme-style.d.ts +10 -0
  190. package/dist/tui/model/theme-style.js +79 -0
  191. package/dist/{tui-ink → tui/model}/theme.d.ts +11 -23
  192. package/dist/{tui-ink → tui/model}/theme.js +23 -38
  193. package/dist/{tui-ink → tui/model}/trace-groups.d.ts +2 -0
  194. package/dist/{tui-ink → tui/model}/trace-groups.js +91 -30
  195. package/dist/tui/model/trace-interaction.d.ts +63 -0
  196. package/dist/tui/model/trace-interaction.js +87 -0
  197. package/dist/tui/model-picker-data.d.ts +1 -2
  198. package/dist/tui/model-picker-data.js +8 -1
  199. package/dist/tui/model-switch.js +2 -0
  200. package/dist/{tui-ink → tui}/run-session-picker.d.ts +2 -3
  201. package/dist/tui/run-session-picker.js +64 -0
  202. package/dist/tui/run.d.ts +72 -0
  203. package/dist/tui/run.js +139 -0
  204. package/dist/tui/terminal/multiplexer.d.ts +5 -0
  205. package/dist/tui/terminal/multiplexer.js +14 -0
  206. package/dist/tui/testing/fake-agent.d.ts +24 -0
  207. package/dist/tui/testing/fake-agent.js +22 -0
  208. package/dist/tui/testing/spy-host.d.ts +21 -0
  209. package/dist/tui/testing/spy-host.js +55 -0
  210. package/dist/tui/trace-groups.js +27 -6
  211. package/dist/types.d.ts +7 -0
  212. package/node_modules/@bubblebrain-ai/pi-tui/LICENSE +21 -0
  213. package/node_modules/@bubblebrain-ai/pi-tui/README.md +854 -0
  214. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.d.ts +26 -0
  215. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.d.ts.map +1 -0
  216. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.js +121 -0
  217. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.js.map +1 -0
  218. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.d.ts +86 -0
  219. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.d.ts.map +1 -0
  220. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.js +649 -0
  221. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.js.map +1 -0
  222. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.d.ts +13 -0
  223. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.d.ts.map +1 -0
  224. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.js +36 -0
  225. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.js.map +1 -0
  226. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.d.ts +22 -0
  227. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.d.ts.map +1 -0
  228. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.js +99 -0
  229. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.js.map +1 -0
  230. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.d.ts +22 -0
  231. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.d.ts.map +1 -0
  232. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.js +36 -0
  233. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.js.map +1 -0
  234. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.d.ts +330 -0
  235. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.d.ts.map +1 -0
  236. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.js +2344 -0
  237. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.js.map +1 -0
  238. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.d.ts +7 -0
  239. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.d.ts.map +1 -0
  240. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.js +43 -0
  241. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.js.map +1 -0
  242. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.d.ts +28 -0
  243. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.d.ts.map +1 -0
  244. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.js +82 -0
  245. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.js.map +1 -0
  246. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.d.ts +48 -0
  247. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.d.ts.map +1 -0
  248. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.js +390 -0
  249. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.js.map +1 -0
  250. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.d.ts +31 -0
  251. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.d.ts.map +1 -0
  252. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.js +67 -0
  253. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.js.map +1 -0
  254. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.d.ts +103 -0
  255. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.d.ts.map +1 -0
  256. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.js +797 -0
  257. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.js.map +1 -0
  258. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.d.ts +56 -0
  259. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.d.ts.map +1 -0
  260. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.js +179 -0
  261. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.js.map +1 -0
  262. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.d.ts +53 -0
  263. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.d.ts.map +1 -0
  264. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.js +175 -0
  265. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.js.map +1 -0
  266. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.d.ts +50 -0
  267. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.d.ts.map +1 -0
  268. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.js +174 -0
  269. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.js.map +1 -0
  270. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.d.ts +12 -0
  271. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.d.ts.map +1 -0
  272. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.js +22 -0
  273. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.js.map +1 -0
  274. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.d.ts +32 -0
  275. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.d.ts.map +1 -0
  276. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.js +109 -0
  277. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.js.map +1 -0
  278. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.d.ts +19 -0
  279. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.d.ts.map +1 -0
  280. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.js +81 -0
  281. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.js.map +1 -0
  282. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.d.ts +13 -0
  283. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.d.ts.map +1 -0
  284. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.js +48 -0
  285. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.js.map +1 -0
  286. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.d.ts +8 -0
  287. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.d.ts.map +1 -0
  288. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.js +26 -0
  289. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.js.map +1 -0
  290. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.d.ts +39 -0
  291. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.d.ts.map +1 -0
  292. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.js +2 -0
  293. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.js.map +1 -0
  294. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.d.ts +16 -0
  295. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.d.ts.map +1 -0
  296. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.js +110 -0
  297. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.js.map +1 -0
  298. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.d.ts +31 -0
  299. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.d.ts.map +1 -0
  300. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.js +42 -0
  301. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.js.map +1 -0
  302. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.d.ts +273 -0
  303. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.d.ts.map +1 -0
  304. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.js +231 -0
  305. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.js.map +1 -0
  306. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.d.ts +184 -0
  307. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.d.ts.map +1 -0
  308. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.js +1174 -0
  309. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.js.map +1 -0
  310. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.d.ts +28 -0
  311. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.d.ts.map +1 -0
  312. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.js +46 -0
  313. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.js.map +1 -0
  314. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.d.ts +10 -0
  315. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.d.ts.map +1 -0
  316. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.js +1268 -0
  317. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.js.map +1 -0
  318. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.d.ts +40 -0
  319. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.d.ts.map +1 -0
  320. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.js +6 -0
  321. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.js.map +1 -0
  322. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.d.ts +42 -0
  323. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.d.ts.map +1 -0
  324. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.js +335 -0
  325. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.js.map +1 -0
  326. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.d.ts +3 -0
  327. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.d.ts.map +1 -0
  328. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.js +60 -0
  329. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.js.map +1 -0
  330. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.d.ts +56 -0
  331. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.d.ts.map +1 -0
  332. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.js +363 -0
  333. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.js.map +1 -0
  334. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.d.ts +10 -0
  335. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.d.ts.map +1 -0
  336. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.js +59 -0
  337. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.js.map +1 -0
  338. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.d.ts +116 -0
  339. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.d.ts.map +1 -0
  340. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.js +518 -0
  341. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.js.map +1 -0
  342. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.d.ts +142 -0
  343. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.d.ts.map +1 -0
  344. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.js +544 -0
  345. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.js.map +1 -0
  346. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.d.ts +11 -0
  347. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.d.ts.map +1 -0
  348. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.js +11 -0
  349. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.js.map +1 -0
  350. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +140 -0
  351. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -0
  352. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +1334 -0
  353. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -0
  354. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.d.ts +39 -0
  355. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.d.ts.map +1 -0
  356. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.js +536 -0
  357. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.js.map +1 -0
  358. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +338 -0
  359. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -0
  360. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +1016 -0
  361. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -0
  362. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.d.ts +17 -0
  363. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.d.ts.map +1 -0
  364. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.js +27 -0
  365. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.js.map +1 -0
  366. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.d.ts +96 -0
  367. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.d.ts.map +1 -0
  368. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.js +1193 -0
  369. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.js.map +1 -0
  370. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.d.ts +25 -0
  371. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.d.ts.map +1 -0
  372. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.js +96 -0
  373. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.js.map +1 -0
  374. package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.d.ts +73 -0
  375. package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.d.ts.map +1 -0
  376. package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.js +188 -0
  377. package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.js.map +1 -0
  378. package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/README.md +20 -0
  379. package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/build.sh +62 -0
  380. package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  381. package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  382. package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/src/darwin-modifiers.c +70 -0
  383. package/node_modules/@bubblebrain-ai/pi-tui/native/win32/README.md +22 -0
  384. package/node_modules/@bubblebrain-ai/pi-tui/native/win32/build.mjs +229 -0
  385. package/node_modules/@bubblebrain-ai/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  386. package/node_modules/@bubblebrain-ai/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  387. package/node_modules/@bubblebrain-ai/pi-tui/native/win32/src/win32-console-mode.c +119 -0
  388. package/node_modules/@bubblebrain-ai/pi-tui/package.json +66 -0
  389. package/node_modules/get-east-asian-width/index.d.ts +60 -0
  390. package/node_modules/get-east-asian-width/index.js +30 -0
  391. package/node_modules/get-east-asian-width/license +9 -0
  392. package/node_modules/get-east-asian-width/lookup-data.js +21 -0
  393. package/node_modules/get-east-asian-width/lookup.js +138 -0
  394. package/node_modules/get-east-asian-width/package.json +71 -0
  395. package/node_modules/get-east-asian-width/readme.md +65 -0
  396. package/node_modules/get-east-asian-width/utilities.js +24 -0
  397. package/package.json +23 -10
  398. package/packages/pi-tui/node_modules/marked/LICENSE +44 -0
  399. package/packages/pi-tui/node_modules/marked/README.md +115 -0
  400. package/packages/pi-tui/node_modules/marked/bin/main.js +284 -0
  401. package/packages/pi-tui/node_modules/marked/bin/marked.js +16 -0
  402. package/packages/pi-tui/node_modules/marked/lib/marked.d.ts +760 -0
  403. package/packages/pi-tui/node_modules/marked/lib/marked.esm.js +77 -0
  404. package/packages/pi-tui/node_modules/marked/lib/marked.esm.js.map +7 -0
  405. package/packages/pi-tui/node_modules/marked/lib/marked.umd.js +79 -0
  406. package/packages/pi-tui/node_modules/marked/lib/marked.umd.js.map +7 -0
  407. package/packages/pi-tui/node_modules/marked/man/marked.1 +113 -0
  408. package/packages/pi-tui/node_modules/marked/man/marked.1.md +93 -0
  409. package/packages/pi-tui/node_modules/marked/package.json +103 -0
  410. package/dist/tui-ink/app.d.ts +0 -76
  411. package/dist/tui-ink/app.js +0 -2377
  412. package/dist/tui-ink/approval/approval-dialog.d.ts +0 -13
  413. package/dist/tui-ink/approval/approval-dialog.js +0 -184
  414. package/dist/tui-ink/approval/diff-view.d.ts +0 -7
  415. package/dist/tui-ink/approval/diff-view.js +0 -44
  416. package/dist/tui-ink/approval/select.d.ts +0 -35
  417. package/dist/tui-ink/approval/select.js +0 -98
  418. package/dist/tui-ink/code-highlight.d.ts +0 -8
  419. package/dist/tui-ink/code-highlight.js +0 -121
  420. package/dist/tui-ink/command-palette.d.ts +0 -32
  421. package/dist/tui-ink/command-palette.js +0 -158
  422. package/dist/tui-ink/compaction-progress.d.ts +0 -19
  423. package/dist/tui-ink/compaction-progress.js +0 -74
  424. package/dist/tui-ink/detect-theme.d.ts +0 -1
  425. package/dist/tui-ink/detect-theme.js +0 -1
  426. package/dist/tui-ink/feedback-dialog.d.ts +0 -19
  427. package/dist/tui-ink/feedback-dialog.js +0 -133
  428. package/dist/tui-ink/feishu-setup-picker.d.ts +0 -5
  429. package/dist/tui-ink/feishu-setup-picker.js +0 -271
  430. package/dist/tui-ink/footer.d.ts +0 -26
  431. package/dist/tui-ink/footer.js +0 -51
  432. package/dist/tui-ink/input-box.d.ts +0 -189
  433. package/dist/tui-ink/input-box.js +0 -1279
  434. package/dist/tui-ink/input-history.d.ts +0 -1
  435. package/dist/tui-ink/input-history.js +0 -1
  436. package/dist/tui-ink/markdown.d.ts +0 -103
  437. package/dist/tui-ink/markdown.js +0 -747
  438. package/dist/tui-ink/message-list.d.ts +0 -49
  439. package/dist/tui-ink/message-list.js +0 -841
  440. package/dist/tui-ink/model-picker.d.ts +0 -143
  441. package/dist/tui-ink/model-picker.js +0 -966
  442. package/dist/tui-ink/plan-confirm.d.ts +0 -7
  443. package/dist/tui-ink/plan-confirm.js +0 -115
  444. package/dist/tui-ink/question-dialog.d.ts +0 -8
  445. package/dist/tui-ink/question-dialog.js +0 -135
  446. package/dist/tui-ink/rewind-picker.d.ts +0 -8
  447. package/dist/tui-ink/rewind-picker.js +0 -102
  448. package/dist/tui-ink/run-session-picker.js +0 -22
  449. package/dist/tui-ink/run.d.ts +0 -62
  450. package/dist/tui-ink/run.js +0 -103
  451. package/dist/tui-ink/session-picker.d.ts +0 -10
  452. package/dist/tui-ink/session-picker.js +0 -113
  453. package/dist/tui-ink/stats-panel.d.ts +0 -11
  454. package/dist/tui-ink/stats-panel.js +0 -65
  455. package/dist/tui-ink/subagent-inspector.d.ts +0 -21
  456. package/dist/tui-ink/subagent-inspector.js +0 -205
  457. package/dist/tui-ink/use-terminal-size.d.ts +0 -4
  458. package/dist/tui-ink/use-terminal-size.js +0 -48
  459. package/dist/tui-ink/waiting-indicator.d.ts +0 -12
  460. package/dist/tui-ink/waiting-indicator.js +0 -129
  461. package/dist/tui-ink/welcome.d.ts +0 -25
  462. package/dist/tui-ink/welcome.js +0 -88
  463. package/dist/tui-ink/wordmark.d.ts +0 -8
  464. package/dist/tui-ink/wordmark.js +0 -103
  465. /package/dist/{tui-ink → tui/model}/composer-buffer.d.ts +0 -0
  466. /package/dist/{tui-ink → tui/model}/display-history.js +0 -0
  467. /package/dist/{tui-ink → tui/model}/input-queue.js +0 -0
  468. /package/dist/{tui-ink → tui/model}/key-events.d.ts +0 -0
  469. /package/dist/{tui-ink → tui/model}/key-events.js +0 -0
  470. /package/dist/{tui-ink → tui/model}/paste-operation-tracker.d.ts +0 -0
  471. /package/dist/{tui-ink → tui/model}/paste-operation-tracker.js +0 -0
  472. /package/dist/{tui-ink → tui/model}/recent-activity.d.ts +0 -0
  473. /package/dist/{tui-ink → tui/model}/submit-dedupe.js +0 -0
  474. /package/dist/{tui-ink → tui/model}/sync-output.d.ts +0 -0
  475. /package/dist/{tui-ink → tui/model}/sync-output.js +0 -0
  476. /package/dist/{tui-ink → tui/model}/terminal-env.d.ts +0 -0
  477. /package/dist/{tui-ink → tui/model}/terminal-env.js +0 -0
  478. /package/dist/{tui-ink → tui/model}/terminal-mouse.d.ts +0 -0
  479. /package/dist/{tui-ink → tui/model}/terminal-mouse.js +0 -0
  480. /package/dist/{tui-ink → tui/model}/width.d.ts +0 -0
  481. /package/dist/{tui-ink → tui/model}/width.js +0 -0
@@ -0,0 +1,1647 @@
1
+ /**
2
+ * Bubble production TUI on the vendored pi-tui renderer.
3
+ *
4
+ * Owns Bubble's terminal product surface: transcript (user cards, assistant
5
+ * text, reasoning, tool traces), composer
6
+ * with native history, queue/steer while running, status footer, blocking
7
+ * overlays (plan approval, tool approval, questions), model/theme pickers,
8
+ * task wake notices, and clean terminal lifecycle on exit.
9
+ *
10
+ * All runtime logic flows through BubbleTuiController + OverlayRequestController;
11
+ * this file owns rendering and input routing only.
12
+ */
13
+ import process from "node:process";
14
+ import { ProcessTerminal, TuiAltScreen, TuiMainScreen, VStack, Editor, Input, ScrollView, Markdown, matchesKey, isKeyRelease, isViewportTUI, } from "@bubblebrain-ai/pi-tui";
15
+ import { StreamingMessageComponent } from "./components/streaming-message.js";
16
+ import { ResponsiveTranscriptComponent } from "./components/responsive-transcript.js";
17
+ import { WelcomeBannerComponent } from "./components/welcome.js";
18
+ import { ApprovalDialogComponent } from "./components/approval-dialog.js";
19
+ import { PlanDialogComponent } from "./components/plan-dialog.js";
20
+ import { FeedbackDialogComponent } from "./components/feedback-dialog.js";
21
+ import { FeishuSetupDialogComponent } from "./components/feishu-setup-dialog.js";
22
+ import { QuestionDialogComponent } from "./components/question-dialog.js";
23
+ import { ProviderKeyInputComponent } from "./components/provider-key-input.js";
24
+ import { RewindPickerComponent, } from "./components/rewind-picker.js";
25
+ import { TaskStatusBarComponent, TasksPaneComponent, } from "./components/tasks-pane.js";
26
+ import { WorkflowInspectorComponent } from "./components/workflow-inspector.js";
27
+ import { SubagentInspectorComponent } from "./components/subagent-inspector.js";
28
+ import { TaskInspectorComponent } from "./components/task-inspector.js";
29
+ import { ContextInfoComponent } from "./components/context-info.js";
30
+ import { StatsPanelComponent } from "./components/stats-panel.js";
31
+ import { SkillsPanelComponent } from "./components/skills-panel.js";
32
+ import { SessionPickerComponent } from "./components/session-picker.js";
33
+ import { ComposerImagePreviewComponent, ImageViewerComponent } from "./components/image-preview.js";
34
+ import { registry as slashRegistry } from "../slash-commands/index.js";
35
+ import { collectUsageStatsBundle } from "../stats/usage.js";
36
+ import { ComposerController, } from "./controller/composer-controller.js";
37
+ import { OverlayRequestController } from "./controller/overlay-controller.js";
38
+ import { createTranscriptTheme, projectTranscript } from "./components/transcript.js";
39
+ import { friendlyCwd, sessionBasename } from "./formatting/summary.js";
40
+ import { formatExternalRuntimeFooterLabel, ResponsiveFooterComponent } from "./footer.js";
41
+ import { ComposerAutocompleteProvider } from "./composer-autocomplete.js";
42
+ import { COMPOSER_EDITOR_OPTIONS, createComposerEditorTheme } from "./composer-style.js";
43
+ import { SessionManager } from "../session.js";
44
+ import { collectFeedback } from "../feedback/collect.js";
45
+ import { TraceInteractionState } from "./model/trace-interaction.js";
46
+ import { parseSkillInvocation } from "../skills/invocation.js";
47
+ import { getNextPermissionMode } from "../permission/mode.js";
48
+ import { maskKey } from "../config.js";
49
+ import { copyToClipboard } from "../clipboard.js";
50
+ import { displayImagesFromPayload, formatImageUserDisplayText, nextImageDisplayLabelStart } from "./image-display.js";
51
+ import { createAssistantMarkdownTheme } from "./markdown-style.js";
52
+ import { resolveThemePalette, } from "./model/theme.js";
53
+ import { themeBackgroundCodes, themeDim, themeForeground } from "./model/theme-style.js";
54
+ import { TuiAnimationClock } from "./animation-clock.js";
55
+ export class PiTuiApp {
56
+ options;
57
+ tui;
58
+ editor;
59
+ composer;
60
+ composerPreview;
61
+ composerSlot = new VStack([]);
62
+ viewportScroll = null;
63
+ transcriptBox = new VStack([]);
64
+ streamingMessage;
65
+ markdown;
66
+ markdownRenderer = (text, width) => {
67
+ this.markdown.setText(text);
68
+ return this.markdown.render(width);
69
+ };
70
+ settledTranscript;
71
+ welcome;
72
+ footer;
73
+ tasksPane;
74
+ taskStatusBar;
75
+ animationClock;
76
+ traceInteraction = new TraceInteractionState();
77
+ overlays;
78
+ themeMode;
79
+ detectedTheme;
80
+ themeOverrides;
81
+ theme;
82
+ transcriptTheme;
83
+ gitBranch;
84
+ metadataManager = null;
85
+ metadataUnsubscribe = null;
86
+ currentUpdateNotice;
87
+ updateNoticeRefreshStarted = false;
88
+ showReasoning = false;
89
+ verboseTrace = false;
90
+ controllerUnsubscribe = null;
91
+ questionUnsubscribe = null;
92
+ activeQuestion = null;
93
+ questionQueue = [];
94
+ providerKeyPhase = null;
95
+ rewindPhase = null;
96
+ rewindPreviewMessageIndex;
97
+ feishuSetupPhase = null;
98
+ disposed = false;
99
+ /** Compatibility/debug view backed by the canonical persistent history. */
100
+ get history() {
101
+ return this.composer.historyTexts();
102
+ }
103
+ constructor(options) {
104
+ this.options = options;
105
+ const terminal = options.terminal ?? new ProcessTerminal();
106
+ this.currentUpdateNotice = options.updateNotice;
107
+ this.themeMode = options.themeMode ?? "auto";
108
+ this.detectedTheme = options.detectedTheme
109
+ ?? (this.themeMode === "auto" ? "dark" : this.themeMode);
110
+ this.themeOverrides = options.themeOverrides;
111
+ this.theme = resolveThemePalette(this.themeMode, this.detectedTheme, this.themeOverrides).palette;
112
+ this.transcriptTheme = createTranscriptTheme(() => this.theme);
113
+ // Pick the renderer before start(). Entering fullscreen after the regular
114
+ // renderer has painted necessarily exposes a main-screen frame first.
115
+ // Constructing TuiAltScreen here makes its 1049h transition the first UI
116
+ // write and keeps the entire product surface on one application instance.
117
+ this.tui = (options.uiMode ?? "fullscreen") === "fullscreen"
118
+ ? new TuiAltScreen(terminal, undefined, undefined, {
119
+ mouseMotion: "all",
120
+ screenBackground: () => themeBackgroundCodes(this.theme.background),
121
+ })
122
+ : new TuiMainScreen(terminal);
123
+ this.markdown = new Markdown("", 0, 0, createAssistantMarkdownTheme(() => this.theme));
124
+ this.streamingMessage = new StreamingMessageComponent(8, () => this.tui.requestRender(), (action) => this.handleTraceAction(action), () => this.theme);
125
+ this.editor = new Editor(this.tui, createComposerEditorTheme(() => this.theme), COMPOSER_EDITOR_OPTIONS);
126
+ this.composer = new ComposerController({
127
+ editor: this.editor,
128
+ scope: {
129
+ sessionFile: this.activeSessionManager().getSessionFile(),
130
+ cwd: process.cwd(),
131
+ },
132
+ nextImageLabelStart: nextImageDisplayLabelStart(options.controller.getTranscript()),
133
+ onSubmit: (payload) => this.handleSubmit(payload),
134
+ onNotice: (message) => this.pushNotice(message),
135
+ onStateChange: () => this.renderSnapshot(),
136
+ onOpenImage: (item) => this.openImageViewer(item),
137
+ allowImageAttachments: () => !this.activeSessionManager().getMetadata?.().externalRuntime,
138
+ readClipboardImage: options.readClipboardImage,
139
+ historyFilePath: options.inputHistoryFilePath,
140
+ getTheme: () => this.theme,
141
+ // Injected terminals are isolated hosts and must not mutate the user's
142
+ // real ~/.bubble history unless they explicitly provide a history file.
143
+ persistHistory: options.terminal === undefined || options.inputHistoryFilePath !== undefined,
144
+ });
145
+ this.composerPreview = new ComposerImagePreviewComponent(() => this.composer.previewAttachment(), () => this.theme);
146
+ this.editor.setAutocompleteProvider(new ComposerAutocompleteProvider({
147
+ cwd: process.cwd(),
148
+ commands: () => slashRegistry.list(),
149
+ skills: () => this.options.skillRegistry?.summaries() ?? [],
150
+ uiMode: () => this.tui.mode,
151
+ registry: this.options.registry,
152
+ thinkingLevel: () => this.options.agent.thinking,
153
+ providerId: () => this.options.agent.providerId,
154
+ themeMode: () => this.themeMode,
155
+ detectedTheme: () => this.detectedTheme,
156
+ onModelSuggestionsChanged: () => {
157
+ if (!this.disposed)
158
+ this.editor.refreshAutocomplete();
159
+ },
160
+ }));
161
+ this.settledTranscript = new ResponsiveTranscriptComponent(() => ({
162
+ messages: this.options.controller.getTranscript(),
163
+ options: this.transcriptRenderOptions(),
164
+ }), { onTraceAction: (action) => this.handleTraceAction(action) });
165
+ this.welcome = new WelcomeBannerComponent(() => {
166
+ const { agent } = this.options;
167
+ return {
168
+ cwd: friendlyCwd(process.cwd()),
169
+ session: sessionBasename(this.activeSessionManager().getSessionFile()),
170
+ model: agent.model,
171
+ provider: agent.providerId,
172
+ thinking: agent.thinking,
173
+ updateNotice: this.currentUpdateNotice,
174
+ theme: this.theme,
175
+ };
176
+ });
177
+ this.footer = new ResponsiveFooterComponent(() => {
178
+ const extra = [];
179
+ const steerCount = this.options.controller.pendingSteerCount();
180
+ const queuedCount = this.options.controller.queuedInputCount();
181
+ const pendingImages = this.composer.pendingImageCount();
182
+ if (steerCount)
183
+ extra.push(themeForeground(this.theme.warning, `steer ×${steerCount}`));
184
+ if (queuedCount)
185
+ extra.push(themeForeground(this.theme.warning, `queue ×${queuedCount}`));
186
+ if (pendingImages)
187
+ extra.push(themeDim(this.theme.dim, `reading image ×${pendingImages}`));
188
+ const session = this.activeSessionManager();
189
+ const metadata = typeof session?.getMetadata === "function" ? session.getMetadata() : undefined;
190
+ const runtimeLabel = formatExternalRuntimeFooterLabel(metadata?.externalRuntime);
191
+ return {
192
+ agent: this.options.agent,
193
+ cwd: friendlyCwd(process.cwd()),
194
+ branch: this.gitBranch,
195
+ sessionTitle: runtimeLabel ? undefined : metadata?.title,
196
+ runtimeLabel,
197
+ extra,
198
+ mode: this.options.agent.mode,
199
+ goalLine: runtimeLabel ? undefined : this.options.controller.getGoalIndicator?.(),
200
+ theme: this.theme,
201
+ // At two rows or fewer, preserve the focused editor body + border.
202
+ hidden: this.tui.terminal.rows <= 2,
203
+ };
204
+ });
205
+ this.tasksPane = new TasksPaneComponent(() => ({
206
+ groups: this.options.controller.getSubagentGroups?.() ?? [],
207
+ workflows: this.options.controller.getWorkflows?.() ?? [],
208
+ tasks: this.options.controller.getBackgroundTasks?.() ?? [],
209
+ }), () => this.tui.terminal.rows, {
210
+ // Pane navigation only changes local presentation state. Reprojecting
211
+ // the live trace/Markdown here is both unnecessary and expensive.
212
+ onRender: () => this.tui.requestRender(),
213
+ onOpenWorkflow: (item) => this.openWorkflowInspector(item),
214
+ onOpenSubagent: (item) => this.openSubagentInspector(item),
215
+ onOpenTask: (item) => this.openTaskInspector(item),
216
+ onStopWorkflow: (id) => this.options.controller.stopWorkflow(id),
217
+ onStopSubagent: (id) => this.options.controller.stopSubagent(id),
218
+ onStopTask: (id) => this.options.controller.stopBackgroundTask(id),
219
+ onCopyTaskOutput: (id) => {
220
+ const output = this.options.controller.getBackgroundTaskOutput(id);
221
+ if (!output)
222
+ return;
223
+ void copyToClipboard(output).then(() => this.pushNotice("Copied task output."));
224
+ },
225
+ onEscape: () => {
226
+ this.tasksPane.focused = false;
227
+ this.tui.setFocus(this.editor);
228
+ this.tui.requestRender();
229
+ },
230
+ }, () => this.theme);
231
+ this.animationClock = new TuiAnimationClock((elapsedMs) => {
232
+ const streamingChanged = this.streamingMessage.advanceAnimationFrame(elapsedMs);
233
+ const tasksChanged = this.tasksPane.advanceAnimationFrame();
234
+ if (streamingChanged || tasksChanged)
235
+ this.tui.requestRender();
236
+ });
237
+ this.taskStatusBar = new TaskStatusBarComponent(this.tasksPane);
238
+ this.overlays = new OverlayRequestController({ questionController: options.questionController });
239
+ this.syncMetadataSubscription();
240
+ this.installBlockingHandlers();
241
+ this.buildLayout();
242
+ this.wireInput();
243
+ this.renderSnapshot();
244
+ void options.resolveGitBranch?.(process.cwd()).then((branch) => {
245
+ if (this.disposed)
246
+ return;
247
+ this.gitBranch = branch?.trim() || undefined;
248
+ this.renderSnapshot();
249
+ }).catch(() => undefined);
250
+ }
251
+ activeSessionManager() {
252
+ return this.options.controller.getSessionManager?.() ?? this.options.sessionManager;
253
+ }
254
+ syncMetadataSubscription() {
255
+ const manager = this.activeSessionManager();
256
+ if (manager === this.metadataManager)
257
+ return;
258
+ this.metadataUnsubscribe?.();
259
+ this.metadataManager = manager;
260
+ this.metadataUnsubscribe = typeof manager?.subscribeMetadata === "function"
261
+ ? manager.subscribeMetadata(() => this.renderSnapshot())
262
+ : null;
263
+ }
264
+ installBlockingHandlers() {
265
+ const { planHandlerRef, approvalHandlerRef, questionController } = this.options;
266
+ if (planHandlerRef) {
267
+ planHandlerRef.current = async (plan) => {
268
+ return this.planDialog(plan);
269
+ };
270
+ }
271
+ if (approvalHandlerRef) {
272
+ approvalHandlerRef.current = async (request) => {
273
+ const choice = await this.approvalDialog(request);
274
+ if (choice.kind === "approve_bash_prefix") {
275
+ if (request.type !== "bash" || !this.options.bashAllowlist) {
276
+ return {
277
+ action: "reject",
278
+ feedback: "This host cannot remember Bash approvals for the current session.",
279
+ };
280
+ }
281
+ // Session-scoped and command-scoped: PermissionAwareApprovalController
282
+ // consults this same instance before opening the next dialog.
283
+ this.options.bashAllowlist.add(choice.prefix);
284
+ return { action: "approve" };
285
+ }
286
+ return choice.kind === "approve_once"
287
+ ? { action: "approve" }
288
+ : { action: "reject", feedback: "User denied the tool call." };
289
+ };
290
+ }
291
+ if (questionController) {
292
+ this.questionUnsubscribe = questionController.subscribe((event) => {
293
+ if (event.type === "asked") {
294
+ this.questionQueue.push(event.request);
295
+ this.openNextQuestion();
296
+ return;
297
+ }
298
+ this.removeQueuedQuestion(event.request.id);
299
+ if (this.activeQuestion?.id === event.request.id) {
300
+ this.activeQuestion.close();
301
+ }
302
+ });
303
+ }
304
+ }
305
+ buildLayout() {
306
+ // Keep the live component permanently after the settled transcript. It
307
+ // renders zero rows while idle. Appending it per turn eventually placed
308
+ // the same component reference in the tree multiple times and let settled
309
+ // rows land after it, breaking spacing/order on later turns.
310
+ this.transcriptBox.addChild(this.settledTranscript);
311
+ this.transcriptBox.addChild(this.streamingMessage);
312
+ this.composerSlot.addChild(this.composerPreview);
313
+ this.composerSlot.addChild(this.editor);
314
+ if (isViewportTUI(this.tui)) {
315
+ // Fullscreen owns a bounded viewport. History scrolls while the composer
316
+ // and footer stay docked. The activity lane is a permanent one-row
317
+ // boundary: spinner text changes, its geometry never does.
318
+ const document = new VStack([this.welcome, this.transcriptBox]);
319
+ const scroll = new ScrollView(document, { follow: "end", primary: true });
320
+ this.viewportScroll = scroll;
321
+ this.tui.setLayoutRoot(new VStack([
322
+ { component: scroll, basis: 0, grow: 1, minSize: 0 },
323
+ { component: this.taskStatusBar, basis: "auto", shrink: 0 },
324
+ { component: this.tasksPane, basis: "auto", shrink: 0 },
325
+ { component: this.streamingMessage.activityLane, basis: "auto", shrink: 0 },
326
+ { component: this.composerSlot, basis: "auto", shrink: 0 },
327
+ { component: this.footer, basis: "auto", shrink: 0 },
328
+ ]));
329
+ }
330
+ else {
331
+ this.tui.addChild(this.welcome);
332
+ this.tui.addChild(this.transcriptBox);
333
+ this.tui.addChild(this.taskStatusBar);
334
+ this.tui.addChild(this.tasksPane);
335
+ this.tui.addChild(this.streamingMessage.activityLane);
336
+ this.tui.addChild(this.composerSlot);
337
+ this.tui.addChild(this.footer);
338
+ }
339
+ this.tui.setFocus(this.editor);
340
+ this.controllerUnsubscribe = this.options.controller.subscribe(() => {
341
+ this.syncMetadataSubscription();
342
+ this.composer.setScope({
343
+ sessionFile: this.activeSessionManager().getSessionFile(),
344
+ cwd: process.cwd(),
345
+ }, nextImageDisplayLabelStart(this.options.controller.getTranscript()));
346
+ this.renderSnapshot();
347
+ });
348
+ }
349
+ wireInput() {
350
+ let ctrlCArmed = false;
351
+ let rewindEscapeArmed = false;
352
+ this.tui.addInputListener((data) => {
353
+ // Global shortcuts act on key presses only. Kitty keyboard protocol also
354
+ // reports releases; allowing those through can trigger a shortcut twice.
355
+ if (isKeyRelease(data))
356
+ return { consume: true };
357
+ // Modal dialogs own their complete keyboard contract. In particular,
358
+ // an approval arrives while a run is active, so letting the global
359
+ // Escape/Ctrl+C handler run first would cancel the agent and strand the
360
+ // unresolved permission request instead of rejecting the dialog.
361
+ if (this.tui.hasOverlay())
362
+ return undefined;
363
+ // The inline credential phase owns all of its input. In particular,
364
+ // Escape returns to /provider instead of cancelling an unrelated run,
365
+ // and Shift+Tab must not change permissions while a key is being typed.
366
+ if (this.providerKeyPhase)
367
+ return undefined;
368
+ if (this.rewindPhase)
369
+ return undefined;
370
+ if (this.editor.focused
371
+ && (matchesKey(data, "ctrl+v") || matchesKey(data, "super+v"))) {
372
+ // A terminal can only bracket text that it receives from the clipboard.
373
+ // Image-only Cmd+V is therefore silent in some terminals; Ctrl+V (and
374
+ // Kitty's super+v when available) gives the composer an explicit,
375
+ // terminal-independent signal to probe the native image clipboard.
376
+ this.composer.requestClipboardImagePaste();
377
+ return { consume: true };
378
+ }
379
+ if (matchesKey(data, "ctrl+g")) {
380
+ if (this.tasksPane.isOpen() && this.tasksPane.focused) {
381
+ this.tasksPane.close();
382
+ this.tasksPane.focused = false;
383
+ this.tui.setFocus(this.editor);
384
+ }
385
+ else {
386
+ if (!this.tasksPane.isOpen())
387
+ this.tasksPane.toggle(true);
388
+ this.tasksPane.focused = true;
389
+ this.tui.setFocus(this.tasksPane);
390
+ }
391
+ this.renderSnapshot();
392
+ return { consume: true };
393
+ }
394
+ if (matchesKey(data, "ctrl+b") && this.options.controller.isRunning()) {
395
+ // Current Grok contract: Ctrl+B transfers a live Execute into Tasks;
396
+ // Ctrl+G remains dedicated to opening and closing the Tasks Pane.
397
+ this.options.controller.promoteActiveBash?.();
398
+ return { consume: true };
399
+ }
400
+ // A focused Tasks Pane owns Escape. It returns focus to the composer;
401
+ // it must never fall through and cancel the running parent Agent.
402
+ if (this.tasksPane.focused && matchesKey(data, "escape"))
403
+ return undefined;
404
+ if (matchesKey(data, "shift+tab")) {
405
+ this.options.agent.setMode(getNextPermissionMode(this.options.agent.mode));
406
+ this.renderSnapshot();
407
+ return { consume: true };
408
+ }
409
+ if (matchesKey(data, "escape") && this.options.controller.cancelActiveRun()) {
410
+ // Ink parity: Escape interrupts an active run. When idle, leave the
411
+ // key unconsumed so the editor/autocomplete layer can handle it.
412
+ return { consume: true };
413
+ }
414
+ if (matchesKey(data, "escape") && this.options.controller.isBusy?.()) {
415
+ // A second Escape while the provider is acknowledging Compact
416
+ // cancellation must not arm rewind or leak into the editor.
417
+ return { consume: true };
418
+ }
419
+ if (matchesKey(data, "escape") && !this.options.controller.isRunning() && !this.editor.getText()) {
420
+ if (rewindEscapeArmed) {
421
+ rewindEscapeArmed = false;
422
+ this.openRewindPicker();
423
+ return { consume: true };
424
+ }
425
+ rewindEscapeArmed = true;
426
+ setTimeout(() => { rewindEscapeArmed = false; }, 650);
427
+ return undefined;
428
+ }
429
+ rewindEscapeArmed = false;
430
+ if (matchesKey(data, "ctrl+c")) {
431
+ // Match Ink: the first Ctrl+C interrupts the active run. Requiring a
432
+ // second keypress here made cancellation feel like a quit gesture and
433
+ // left the tool trace running behind the composer.
434
+ if (this.options.controller.cancelActiveRun()) {
435
+ ctrlCArmed = false;
436
+ return { consume: true };
437
+ }
438
+ if (this.options.controller.isBusy?.())
439
+ return { consume: true };
440
+ if (ctrlCArmed) {
441
+ this.dispose();
442
+ return { consume: true };
443
+ }
444
+ ctrlCArmed = true;
445
+ setTimeout(() => {
446
+ ctrlCArmed = false;
447
+ }, 1_500);
448
+ return { consume: true };
449
+ }
450
+ if (data === "\x14") {
451
+ this.showReasoning = !this.showReasoning;
452
+ this.renderSnapshot();
453
+ return { consume: true };
454
+ }
455
+ if (data === "\x0f") {
456
+ this.verboseTrace = !this.verboseTrace;
457
+ this.renderSnapshot();
458
+ return { consume: true };
459
+ }
460
+ return undefined;
461
+ });
462
+ }
463
+ handleSubmit(input) {
464
+ const payload = typeof input === "string"
465
+ ? { text: input, images: [] }
466
+ : input;
467
+ const trimmed = payload.text.trim();
468
+ if (!trimmed && payload.images.length === 0)
469
+ return;
470
+ // Passive output preserves a user's manual history position, but an
471
+ // explicit Composer submission changes their point of attention to the
472
+ // new turn. Re-enable follow-end before appending the user row so the same
473
+ // layout pass that renders it also brings it into view.
474
+ this.viewportScroll?.scrollToEnd();
475
+ if (trimmed.startsWith("/")) {
476
+ void this.handleCommand(trimmed);
477
+ return;
478
+ }
479
+ const controller = this.options.controller;
480
+ if (controller.isRunning()) {
481
+ // AgentRunInputQueue is intentionally text-only. Image payloads retain
482
+ // their bytes by waiting for the next provider turn instead of degrading
483
+ // into a text steer.
484
+ if (payload.images.length > 0)
485
+ controller.queueInput(payload);
486
+ else
487
+ controller.steer(trimmed);
488
+ this.renderSnapshot();
489
+ }
490
+ else if (controller.queueAfterCommand?.(payload)) {
491
+ this.renderSnapshot();
492
+ }
493
+ else {
494
+ this.pushUserRow(payload);
495
+ const agentInput = payload.images.length > 0 ? payload : trimmed;
496
+ void controller.runTurn(agentInput, process.cwd()).finally(() => this.renderSnapshot());
497
+ }
498
+ }
499
+ async handleCommand(command) {
500
+ const [name, ...rest] = command.slice(1).split(/\s+/);
501
+ const args = rest.join(" ");
502
+ // pi-tui-local commands first (renderer-specific modes).
503
+ if (name === "fullscreen") {
504
+ if (this.fullscreen) {
505
+ this.exitFullscreen();
506
+ return;
507
+ }
508
+ if (this.tui.mode === "fullscreen") {
509
+ this.pushNotice("Already in fullscreen mode");
510
+ this.renderSnapshot();
511
+ return;
512
+ }
513
+ void this.enterFullscreen();
514
+ return;
515
+ }
516
+ const skillInvocation = this.options.skillRegistry
517
+ ? parseSkillInvocation(command, this.options.skillRegistry)
518
+ : undefined;
519
+ if (skillInvocation) {
520
+ this.pushUserRow({ text: command, images: [] });
521
+ const controller = this.options.controller;
522
+ if (controller.isRunning()) {
523
+ controller.steer(skillInvocation.actualPrompt);
524
+ this.renderSnapshot();
525
+ }
526
+ else {
527
+ void controller.runTurn(skillInvocation.actualPrompt, process.cwd()).finally(() => this.renderSnapshot());
528
+ }
529
+ return;
530
+ }
531
+ // Everything else routes through the shared registry (21 builtin
532
+ // commands + MCP dynamic prompts + skill fallback) — the same surface
533
+ // the Ink TUI exposed.
534
+ const compactCommand = name === "compact";
535
+ let compactSignal;
536
+ try {
537
+ if (compactCommand)
538
+ compactSignal = this.options.controller.beginCommandActivity?.("compact");
539
+ const ctx = this.buildSlashContext(compactSignal);
540
+ const outcome = await slashRegistry.execute(`/${name}${args ? ` ${args}` : ""}`, ctx);
541
+ if (compactCommand) {
542
+ const text = outcome.result ?? (outcome.handled ? undefined : `Unknown command: /${name}`);
543
+ const compactionSummary = outcome.detail?.kind === "compaction-summary"
544
+ ? outcome.detail.content.trim()
545
+ : "";
546
+ this.options.controller.finishCommandActivity?.("compact", text ? {
547
+ key: `compact-${Date.now()}`,
548
+ role: text.startsWith("Error:") ? "error" : "assistant",
549
+ content: text,
550
+ syntheticKind: text.startsWith("Error:")
551
+ ? undefined
552
+ : compactionSummary
553
+ ? "ui_compact_summary"
554
+ : "ui_notice",
555
+ compactionSummary: compactionSummary || undefined,
556
+ } : undefined);
557
+ void this.options.controller.drainQueuedInputs?.(process.cwd());
558
+ return;
559
+ }
560
+ if (outcome.inject) {
561
+ // Command produced model-facing input (e.g. /rewind restore).
562
+ void this.handleSubmit(outcome.inject);
563
+ }
564
+ else if (outcome.result) {
565
+ this.pushNotice(outcome.result);
566
+ }
567
+ else if (!outcome.handled) {
568
+ this.pushNotice(`Unknown command: /${name}`);
569
+ }
570
+ }
571
+ catch (error) {
572
+ const text = error instanceof Error ? error.message : String(error);
573
+ if (compactCommand && compactSignal) {
574
+ this.options.controller.finishCommandActivity?.("compact", {
575
+ key: `compact-error-${Date.now()}`,
576
+ role: "error",
577
+ content: text,
578
+ });
579
+ }
580
+ else {
581
+ this.pushNotice(text);
582
+ }
583
+ }
584
+ finally {
585
+ if (compactSignal && this.options.controller.getCommandActivity?.()?.kind === "compact") {
586
+ this.options.controller.finishCommandActivity?.("compact");
587
+ }
588
+ this.renderSnapshot();
589
+ }
590
+ }
591
+ buildSlashContext(compactionAbortSignal) {
592
+ const options = this.options;
593
+ const agent = options.agent;
594
+ const addMessage = (role, content) => {
595
+ options.controller.appendDisplayMessage({
596
+ key: `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
597
+ role,
598
+ content,
599
+ syntheticKind: role === "assistant" ? "ui_notice" : undefined,
600
+ });
601
+ };
602
+ return {
603
+ agent,
604
+ addMessage,
605
+ clearMessages: () => options.controller.clearTranscript(),
606
+ cwd: process.cwd(),
607
+ exit: () => this.dispose(),
608
+ // Embedded/test hosts created before session switching may only expose
609
+ // the constructor manager; production controllers provide the live one.
610
+ sessionManager: (options.controller.getSessionManager?.() ?? options.sessionManager),
611
+ createProvider: options.createProvider,
612
+ openPicker: (mode, providerId) => {
613
+ if (mode === "model" || mode === "provider" || mode === "theme") {
614
+ // A bare picker command can still arrive from a fast Enter or a
615
+ // non-composer command host. Keep pi-tui on the shared inline
616
+ // command surface instead of falling back to a centered overlay.
617
+ this.composer.replaceDraft(mode === "model" ? "/model " : mode === "provider" ? "/provider " : "/theme ");
618
+ this.editor.refreshAutocomplete();
619
+ }
620
+ else if (mode === "key" && providerId) {
621
+ this.openProviderKeyPhase(providerId);
622
+ }
623
+ else if (mode === "skill") {
624
+ this.openSkills();
625
+ }
626
+ else if (mode === "feishu-setup") {
627
+ this.openFeishuSetup();
628
+ }
629
+ },
630
+ registry: options.registry,
631
+ skillRegistry: options.skillRegistry,
632
+ bashAllowlist: options.bashAllowlist,
633
+ settingsManager: options.settingsManager,
634
+ hookController: options.hookController,
635
+ mcpManager: options.mcpManager,
636
+ lspService: options.lspService,
637
+ flushMemory: options.flushMemory,
638
+ runMemoryCompaction: options.runMemoryCompaction,
639
+ runMemorySummary: options.runMemorySummary,
640
+ runMemoryRefresh: options.runMemoryRefresh,
641
+ getThemeMode: () => this.themeMode,
642
+ getResolvedTheme: () => this.resolvedTheme(),
643
+ setThemeMode: (mode) => this.applyThemeMode(mode),
644
+ openFeedback: (initialDescription) => this.openFeedback(initialDescription),
645
+ openStats: () => this.openStats(),
646
+ ...(this.tui.mode === "fullscreen"
647
+ ? { openContextInfo: (snapshot) => this.openContextInfo(snapshot) }
648
+ : {}),
649
+ handleGoalCommand: (input) => options.controller.handleGoalCommand(input, process.cwd()),
650
+ openRewindPicker: () => this.openRewindPicker(),
651
+ fillComposer: (text) => {
652
+ this.composer.replaceDraft(text);
653
+ this.tui.setFocus(this.editor);
654
+ },
655
+ rebuildTranscript: () => options.controller.rebuildTranscriptFromAgent(),
656
+ openSessionPicker: () => this.openSessionPicker(),
657
+ compactionProgress: () => { },
658
+ compactionAbortSignal,
659
+ };
660
+ }
661
+ pushUserRow(payload) {
662
+ this.appendTranscriptRow({
663
+ key: `user-${Date.now()}`,
664
+ role: "user",
665
+ content: formatImageUserDisplayText(payload.displayText ?? payload.text, payload.images.length, payload.imageDisplayStart),
666
+ images: displayImagesFromPayload(payload),
667
+ });
668
+ }
669
+ resolvedTheme() {
670
+ return this.themeMode === "auto" ? this.detectedTheme : this.themeMode;
671
+ }
672
+ applyThemeMode(mode) {
673
+ if (this.themeMode === mode) {
674
+ // Persist an explicit selection even when it resolves to the palette
675
+ // already on screen (e.g. auto-dark -> forced dark).
676
+ this.options.callbacks.onThemeModeChange?.(mode);
677
+ return;
678
+ }
679
+ this.themeMode = mode;
680
+ // Preserve object identity so already-open panels that received this
681
+ // palette also repaint in place.
682
+ Object.assign(this.theme, resolveThemePalette(mode, this.detectedTheme, this.themeOverrides).palette);
683
+ this.options.callbacks.onThemeModeChange?.(mode);
684
+ this.tui.invalidate();
685
+ this.renderSnapshot();
686
+ }
687
+ pushNotice(text) {
688
+ this.appendTranscriptRow({
689
+ key: `notice-${Date.now()}`,
690
+ role: "assistant",
691
+ content: text,
692
+ syntheticKind: "ui_notice",
693
+ });
694
+ }
695
+ appendTranscriptRow(message) {
696
+ this.options.controller.appendDisplayMessage(message);
697
+ }
698
+ streamingMounted = false;
699
+ /** One display policy feeds both committed history and the live row pool. */
700
+ transcriptRenderOptions() {
701
+ return {
702
+ showReasoning: this.showReasoning,
703
+ verboseTrace: this.verboseTrace,
704
+ traceInteraction: this.traceInteraction,
705
+ dimFromMessageIndex: this.rewindPreviewMessageIndex,
706
+ theme: this.transcriptTheme,
707
+ // Stable identity lets the settled transcript cache survive composer-only
708
+ // frames. The stateful Markdown instance is still updated on cache misses.
709
+ markdownRenderer: this.markdownRenderer,
710
+ };
711
+ }
712
+ renderSnapshot() {
713
+ if (this.disposed)
714
+ return;
715
+ const columns = this.tui.terminal.columns || process.stdout.columns || 80;
716
+ this.updateStreamingRegion(columns);
717
+ this.tui.requestRender();
718
+ }
719
+ /**
720
+ * The streaming preview is a persistent row-pool component mounted as the
721
+ * transcript's LAST child (upstream AssistantMessageComponent pattern).
722
+ * Rows are only ever setText — re-creating components per frame is what
723
+ * leaked every intermediate prefix into scrollback (verified with a
724
+ * VirtualTerminal matrix). On turn end it collapses to zero rows in the
725
+ * same frame the settled message commits, so the full answer replaces the
726
+ * preview with no duplicate.
727
+ */
728
+ updateStreamingRegion(columns) {
729
+ const controller = this.options.controller;
730
+ const tail = controller.isRunning() ? controller.getStreamingTail() : null;
731
+ const commandActivity = controller.getCommandActivity?.() ?? null;
732
+ if (tail) {
733
+ if (!this.streamingMounted) {
734
+ this.streamingMounted = true;
735
+ this.streamingMessage.startSpinner();
736
+ }
737
+ this.streamingMessage.noteWidth(columns);
738
+ this.streamingMessage.update(tail, columns, this.transcriptRenderOptions());
739
+ }
740
+ else if (commandActivity) {
741
+ if (!this.streamingMounted) {
742
+ this.streamingMounted = true;
743
+ this.streamingMessage.startSpinner();
744
+ }
745
+ this.streamingMessage.noteWidth(columns);
746
+ this.streamingMessage.updateCommandActivity(commandActivity.kind === "compact" ? "Compacting" : commandActivity.kind, commandActivity.status === "cancelling", columns);
747
+ }
748
+ else if (this.streamingMounted) {
749
+ this.streamingMounted = false;
750
+ this.streamingMessage.clearToNothing();
751
+ }
752
+ this.syncAnimationClock();
753
+ }
754
+ syncAnimationClock() {
755
+ this.animationClock.setActive(this.fullscreen === null
756
+ && (this.streamingMessage.isAnimationActive() || this.tasksPane.isAnimationActive()));
757
+ }
758
+ // ---- Fullscreen mode ----------------------------------------------------
759
+ fullscreen = null;
760
+ /**
761
+ * Switch to the alternate-screen transcript view. The main screen stays
762
+ * mounted but paused (stopped renderer); both modes subscribe to the same
763
+ * controller so the transcript stays in sync. Escape / Ctrl+C / /fullscreen
764
+ * return here.
765
+ */
766
+ async enterFullscreen() {
767
+ if (this.fullscreen)
768
+ return;
769
+ this.animationClock.setActive(false);
770
+ this.tui.stop();
771
+ const { FullscreenApp } = await import("./fullscreen.js");
772
+ this.fullscreen = new FullscreenApp({
773
+ controller: this.options.controller,
774
+ agent: this.options.agent,
775
+ onExit: () => this.exitFullscreen(),
776
+ onCommand: (command) => {
777
+ // Pickers and dialogs are owned by the main-screen TUI. Return to it
778
+ // before executing so every slash command uses the same registry and
779
+ // never leaks into the model as ordinary fullscreen input.
780
+ this.exitFullscreen();
781
+ void this.handleCommand(command);
782
+ },
783
+ terminal: this.options.terminal,
784
+ traceInteraction: this.traceInteraction,
785
+ composer: this.composer,
786
+ getTheme: () => this.theme,
787
+ });
788
+ this.fullscreen.start();
789
+ }
790
+ exitFullscreen() {
791
+ if (!this.fullscreen)
792
+ return;
793
+ this.fullscreen.dispose();
794
+ this.fullscreen = null;
795
+ this.composer.attachEditor(this.editor);
796
+ this.composer.setOpenImageHandler((item) => this.openImageViewer(item));
797
+ this.tui.start();
798
+ this.renderSnapshot();
799
+ }
800
+ // ---- Overlays -----------------------------------------------------------
801
+ openImageViewer(image) {
802
+ let handle;
803
+ const close = () => {
804
+ handle?.hide();
805
+ this.tui.setFocus(this.editor);
806
+ this.renderSnapshot();
807
+ };
808
+ const component = new ImageViewerComponent(image, close, () => this.tui.terminal.rows, () => this.theme);
809
+ handle = this.tui.showOverlay(component, {
810
+ anchor: "center",
811
+ width: "70%",
812
+ minWidth: 36,
813
+ maxWidth: 110,
814
+ maxHeight: "85%",
815
+ margin: { top: 2, right: 1, bottom: 2, left: 1 },
816
+ dismissOnOutsideClick: true,
817
+ });
818
+ this.tui.setFocus(component);
819
+ }
820
+ openStats() {
821
+ let handle;
822
+ const component = new StatsPanelComponent(collectUsageStatsBundle(), {
823
+ getTerminalRows: () => this.tui.terminal.rows,
824
+ onClose: () => handle?.hide(),
825
+ onRender: () => this.renderSnapshot(),
826
+ theme: this.theme,
827
+ });
828
+ handle = this.tui.showOverlay(component, {
829
+ anchor: "center",
830
+ width: "65%",
831
+ minWidth: 44,
832
+ maxWidth: 100,
833
+ maxHeight: 30,
834
+ margin: { top: 2, right: 0, bottom: 2, left: 0 },
835
+ });
836
+ this.tui.setFocus(component);
837
+ }
838
+ openSkills() {
839
+ const skillRegistry = this.options.skillRegistry;
840
+ if (!skillRegistry) {
841
+ this.pushNotice("No skill registry is attached to this session.");
842
+ return;
843
+ }
844
+ let handle;
845
+ const component = new SkillsPanelComponent(skillRegistry, {
846
+ getTerminalRows: () => this.tui.terminal.rows,
847
+ onClose: () => handle?.hide(),
848
+ onRender: () => this.renderSnapshot(),
849
+ onSkillsChanged: () => {
850
+ this.options.agent.setSkillSummaries(skillRegistry.summaries());
851
+ this.editor.refreshAutocomplete();
852
+ this.renderSnapshot();
853
+ },
854
+ theme: this.theme,
855
+ });
856
+ handle = this.tui.showOverlay(component, {
857
+ anchor: "center",
858
+ width: "65%",
859
+ minWidth: 40,
860
+ maxWidth: 160,
861
+ maxHeight: 34,
862
+ margin: { top: 3, right: 0, bottom: 3, left: 0 },
863
+ dismissOnOutsideClick: true,
864
+ });
865
+ this.tui.setFocus(component);
866
+ }
867
+ openSessionPicker() {
868
+ if (this.options.controller.isBusy?.()) {
869
+ this.appendTranscriptRow({
870
+ key: `session-error-${Date.now()}`,
871
+ role: "error",
872
+ content: "Stop the current run before switching sessions.",
873
+ });
874
+ return;
875
+ }
876
+ const activeFile = this.activeSessionManager().getSessionFile();
877
+ const currentSessions = SessionManager.summarizeSessionsForCwd(process.cwd());
878
+ const allSessions = SessionManager.listAllSessions();
879
+ let handle;
880
+ const close = () => {
881
+ handle?.hide();
882
+ this.tui.setFocus(this.editor);
883
+ this.renderSnapshot();
884
+ };
885
+ const fail = (prefix, error) => {
886
+ close();
887
+ this.appendTranscriptRow({
888
+ key: `session-error-${Date.now()}`,
889
+ role: "error",
890
+ content: `${prefix}: ${error || "unknown error"}`,
891
+ });
892
+ };
893
+ const component = new SessionPickerComponent({
894
+ currentCwd: process.cwd(),
895
+ currentSessions,
896
+ allSessions,
897
+ activeFile,
898
+ getTerminalRows: () => this.tui.terminal.rows,
899
+ onClose: close,
900
+ onNewSession: () => {
901
+ const outcome = this.options.controller.createFreshSession(process.cwd());
902
+ if (!outcome.ok) {
903
+ fail("Failed to start a new session", outcome.error);
904
+ return;
905
+ }
906
+ this.composer.setScope({
907
+ sessionFile: this.activeSessionManager().getSessionFile(),
908
+ cwd: process.cwd(),
909
+ }, nextImageDisplayLabelStart(this.options.controller.getTranscript()));
910
+ this.viewportScroll?.scrollToEnd();
911
+ close();
912
+ },
913
+ onSelect: (file) => {
914
+ if (file === this.activeSessionManager().getSessionFile()) {
915
+ close();
916
+ return;
917
+ }
918
+ const summary = allSessions.find((session) => session.file === file);
919
+ const displayName = summary?.title || summary?.preview || sessionBasename(file) || "Session";
920
+ const outcome = this.options.controller.switchSession({
921
+ targetFile: file,
922
+ notice: `⤷ Resumed session: ${displayName}`,
923
+ });
924
+ if (!outcome.ok) {
925
+ fail("Failed to switch session", outcome.error);
926
+ return;
927
+ }
928
+ this.composer.setScope({
929
+ sessionFile: this.activeSessionManager().getSessionFile(),
930
+ cwd: process.cwd(),
931
+ }, nextImageDisplayLabelStart(this.options.controller.getTranscript()));
932
+ this.viewportScroll?.scrollToEnd();
933
+ close();
934
+ },
935
+ onRender: () => this.renderSnapshot(),
936
+ theme: this.theme,
937
+ });
938
+ handle = this.tui.showOverlay(component, {
939
+ anchor: "center",
940
+ width: "70%",
941
+ minWidth: 40,
942
+ maxWidth: 120,
943
+ maxHeight: 30,
944
+ margin: { top: 2, right: 0, bottom: 2, left: 0 },
945
+ dismissOnOutsideClick: true,
946
+ });
947
+ this.tui.setFocus(component);
948
+ }
949
+ openContextInfo(snapshot) {
950
+ let handle;
951
+ const messages = this.options.agent.messages;
952
+ const turnCount = messages.filter((message) => message.role === "user").length;
953
+ const toolCallCount = messages.reduce((count, message) => (count + (message.role === "assistant" ? message.toolCalls?.length ?? 0 : 0)), 0);
954
+ const compactionCount = this.options.agent.getCompactionStats().fired;
955
+ const mcpStates = this.options.mcpManager?.getStates?.() ?? [];
956
+ const sessionId = sessionBasename(this.activeSessionManager().getSessionFile());
957
+ const component = new ContextInfoComponent({
958
+ snapshot,
959
+ sessionId,
960
+ cwd: friendlyCwd(process.cwd()),
961
+ thinking: this.options.agent.thinking,
962
+ permissionMode: this.options.agent.mode,
963
+ turnCount,
964
+ toolCallCount,
965
+ compactionCount,
966
+ mcpServerCount: mcpStates.length,
967
+ }, {
968
+ getTerminalRows: () => this.tui.terminal.rows,
969
+ onClose: () => handle?.hide(),
970
+ onRender: () => this.renderSnapshot(),
971
+ ...(sessionId ? { copySessionId: () => copyToClipboard(sessionId) } : {}),
972
+ theme: this.theme,
973
+ });
974
+ handle = this.tui.showOverlay(component, {
975
+ anchor: "center",
976
+ width: "65%",
977
+ minWidth: 44,
978
+ maxWidth: 100,
979
+ maxHeight: 30,
980
+ margin: { top: 2, right: 0, bottom: 2, left: 0 },
981
+ });
982
+ this.tui.setFocus(component);
983
+ }
984
+ openProviderKeyPhase(providerId) {
985
+ const registry = this.options.registry;
986
+ const provider = registry?.getConfigured().find((candidate) => candidate.id === providerId);
987
+ if (!registry || !provider) {
988
+ this.pushNotice(`Provider ${providerId} could not be prepared for API key setup.`);
989
+ this.renderSnapshot();
990
+ return;
991
+ }
992
+ this.closeProviderKeyPhase(false);
993
+ const input = new Input({ prompt: "◆ ", mask: "•" });
994
+ const component = new ProviderKeyInputComponent(input, provider.name, () => this.theme);
995
+ const phase = { input, component, submitting: false };
996
+ this.providerKeyPhase = phase;
997
+ this.composerSlot.removeChild(this.editor);
998
+ this.composerSlot.addChild(component);
999
+ input.onEscape = () => this.closeProviderKeyPhase(true);
1000
+ input.onBackspaceAtStart = () => this.closeProviderKeyPhase(true);
1001
+ input.onSubmit = (rawKey) => {
1002
+ const key = rawKey.replace(/[\r\n\t]/g, "").trim();
1003
+ if (!key || phase.submitting || this.providerKeyPhase !== phase)
1004
+ return;
1005
+ phase.submitting = true;
1006
+ this.closeProviderKeyPhase(false);
1007
+ void this.saveProviderKey(providerId, key);
1008
+ };
1009
+ this.tui.setFocus(input);
1010
+ this.renderSnapshot();
1011
+ }
1012
+ closeProviderKeyPhase(returnToProviderPicker) {
1013
+ const phase = this.providerKeyPhase;
1014
+ if (!phase)
1015
+ return;
1016
+ // Erase the component-owned copy before releasing the phase. The caller
1017
+ // may retain the submitted value just long enough to persist it, but UI
1018
+ // state never keeps the raw credential after this point.
1019
+ phase.input.setValue("");
1020
+ phase.input.onSubmit = undefined;
1021
+ phase.input.onEscape = undefined;
1022
+ phase.input.onBackspaceAtStart = undefined;
1023
+ this.composerSlot.removeChild(phase.component);
1024
+ this.composerSlot.addChild(this.editor);
1025
+ this.providerKeyPhase = null;
1026
+ this.composer.replaceDraft(returnToProviderPicker ? "/provider " : "");
1027
+ if (returnToProviderPicker)
1028
+ this.editor.refreshAutocomplete();
1029
+ this.tui.setFocus(this.editor);
1030
+ this.renderSnapshot();
1031
+ }
1032
+ async saveProviderKey(providerId, key) {
1033
+ const registry = this.options.registry;
1034
+ if (!registry)
1035
+ return;
1036
+ try {
1037
+ registry.updateProviderKey(providerId, key);
1038
+ const provider = registry.getConfigured().find((candidate) => candidate.id === providerId);
1039
+ if (!provider)
1040
+ throw new Error("provider disappeared after saving credentials");
1041
+ // Saving credentials configures the provider; it must not partially
1042
+ // mutate the active route. Provider + model + thinking + prompt + session
1043
+ // move together only through the shared /model switch transaction.
1044
+ registry.setDefault(providerId);
1045
+ this.pushNotice(`API key updated for ${provider.name} to ${maskKey(key)}. Use /model to select a compatible model.`);
1046
+ }
1047
+ catch (error) {
1048
+ const message = (error instanceof Error ? error.message : String(error)).replaceAll(key, "[redacted]");
1049
+ this.pushNotice(`Failed to update API key for ${providerId}: ${message}`);
1050
+ }
1051
+ this.renderSnapshot();
1052
+ }
1053
+ openRewindPicker() {
1054
+ if (this.rewindPhase || this.providerKeyPhase || this.disposed)
1055
+ return;
1056
+ const controller = this.options.controller;
1057
+ const points = controller.isRunning() ? [] : this.collectRewindPoints();
1058
+ if (!controller.isRunning() && points.length === 0) {
1059
+ this.pushNotice("Nothing to rewind: no user messages in this session.");
1060
+ this.renderSnapshot();
1061
+ return;
1062
+ }
1063
+ const draft = this.composer.snapshotDraft();
1064
+ const component = new RewindPickerComponent(controller.isRunning() ? "cancel-offer" : "picker", points, {
1065
+ getTerminalRows: () => this.tui.terminal.rows,
1066
+ onPreview: (point, scope) => this.previewRewindPoint(point, scope),
1067
+ onScopeChange: (point, scope) => this.previewRewindPoint(point, scope),
1068
+ onCancel: () => this.closeRewindPicker(true),
1069
+ onCancelRun: () => void this.cancelRunForRewind(component),
1070
+ onConfirm: (point, scope) => void this.executeRewindFromPicker(component, point, scope),
1071
+ onRender: () => this.renderSnapshot(),
1072
+ theme: this.theme,
1073
+ });
1074
+ this.rewindPhase = {
1075
+ component,
1076
+ draft,
1077
+ previousScrollTop: this.viewportScroll?.scrollTop ?? 0,
1078
+ };
1079
+ this.composerSlot.removeChild(this.editor);
1080
+ this.composerSlot.addChild(component);
1081
+ this.tui.setFocus(component);
1082
+ const selected = component.getSelectedPoint();
1083
+ if (selected)
1084
+ this.previewRewindPoint(selected, "all");
1085
+ this.renderSnapshot();
1086
+ }
1087
+ collectRewindPoints() {
1088
+ const session = this.options.controller.getSessionManager();
1089
+ const turns = session.listUserTurns();
1090
+ const checkpoints = session.getCheckpoints();
1091
+ return turns.map((turn, turnIndex) => ({
1092
+ turn,
1093
+ turnIndex,
1094
+ fileCount: checkpoints.filesTouchedAt(turn.id).length,
1095
+ }));
1096
+ }
1097
+ async cancelRunForRewind(component) {
1098
+ if (this.rewindPhase?.component !== component || component.getPhase() !== "cancel-offer")
1099
+ return;
1100
+ component.showLoading();
1101
+ this.options.controller.cancelActiveRun("Cancelled before rewind");
1102
+ await this.waitForControllerIdle();
1103
+ if (this.rewindPhase?.component !== component || this.disposed)
1104
+ return;
1105
+ component.showPicker(this.collectRewindPoints());
1106
+ }
1107
+ waitForControllerIdle() {
1108
+ const controller = this.options.controller;
1109
+ if (!controller.isRunning())
1110
+ return Promise.resolve();
1111
+ return new Promise((resolve) => {
1112
+ const unsubscribe = controller.subscribe(() => {
1113
+ if (controller.isRunning())
1114
+ return;
1115
+ unsubscribe();
1116
+ resolve();
1117
+ });
1118
+ // The run can settle between the first check and subscription.
1119
+ if (!controller.isRunning()) {
1120
+ unsubscribe();
1121
+ resolve();
1122
+ }
1123
+ });
1124
+ }
1125
+ async executeRewindFromPicker(component, point, scope) {
1126
+ if (this.rewindPhase?.component !== component || component.getPhase() === "executing")
1127
+ return;
1128
+ component.showExecuting();
1129
+ try {
1130
+ const result = await this.options.controller.rewindToTurn(point.turn.id, scope);
1131
+ const targetText = result.target.text;
1132
+ // Swap the panel, final composer value, transcript dim, and focus before
1133
+ // requesting the success paint. This prevents an empty-composer frame
1134
+ // between "Rewinding..." and the restored prompt.
1135
+ this.closeRewindPicker(scope === "code", scope === "code" ? undefined : targetText, false);
1136
+ this.viewportScroll?.scrollToEnd();
1137
+ const fileCount = result.files.restored.length + result.files.deleted.length;
1138
+ const failed = result.files.failed.length;
1139
+ if (scope === "code") {
1140
+ this.pushNotice(failed > 0
1141
+ ? `Files restored with ${failed} failure${failed === 1 ? "" : "s"}.`
1142
+ : fileCount > 0
1143
+ ? `Restored ${fileCount} file${fileCount === 1 ? "" : "s"}.`
1144
+ : "No tracked file edits to undo.");
1145
+ }
1146
+ else {
1147
+ const fileSuffix = scope === "all"
1148
+ ? failed > 0
1149
+ ? ` · ${failed} file restore failure${failed === 1 ? "" : "s"}`
1150
+ : fileCount > 0
1151
+ ? ` · ${fileCount} file${fileCount === 1 ? "" : "s"} restored`
1152
+ : " · no tracked file edits"
1153
+ : "";
1154
+ this.pushNotice(`Reverted conversation${fileSuffix}`);
1155
+ }
1156
+ this.renderSnapshot();
1157
+ }
1158
+ catch (error) {
1159
+ if (this.rewindPhase?.component !== component)
1160
+ return;
1161
+ component.showError(error instanceof Error ? error.message : String(error));
1162
+ }
1163
+ }
1164
+ closeRewindPicker(restoreDraft, replacementText, render = true) {
1165
+ const phase = this.rewindPhase;
1166
+ if (!phase)
1167
+ return;
1168
+ this.composerSlot.removeChild(phase.component);
1169
+ this.composerSlot.addChild(this.editor);
1170
+ this.rewindPhase = null;
1171
+ this.rewindPreviewMessageIndex = undefined;
1172
+ if (restoreDraft)
1173
+ this.composer.restoreDraft(phase.draft);
1174
+ else if (replacementText !== undefined)
1175
+ this.composer.replaceDraft(replacementText);
1176
+ if (restoreDraft) {
1177
+ this.viewportScroll?.scrollTo(phase.previousScrollTop, { disableFollow: true });
1178
+ }
1179
+ this.tui.setFocus(this.editor);
1180
+ if (render)
1181
+ this.renderSnapshot();
1182
+ }
1183
+ previewRewindPoint(point, scope) {
1184
+ if (!point) {
1185
+ this.rewindPreviewMessageIndex = undefined;
1186
+ this.renderSnapshot();
1187
+ return;
1188
+ }
1189
+ const transcript = this.options.controller.getTranscript();
1190
+ const userIndexes = transcript
1191
+ .map((message, index) => message.role === "user" ? index : -1)
1192
+ .filter((index) => index >= 0);
1193
+ const turnCount = this.options.controller.getSessionManager().listUserTurns().length;
1194
+ const relevantUsers = userIndexes.slice(Math.max(0, userIndexes.length - turnCount));
1195
+ const messageIndex = relevantUsers[point.turnIndex];
1196
+ this.rewindPreviewMessageIndex = scope === "code" ? undefined : messageIndex;
1197
+ if (messageIndex !== undefined && this.viewportScroll) {
1198
+ const columns = this.tui.terminal.columns || process.stdout.columns || 80;
1199
+ const rowsBefore = this.welcome.render(columns).length + projectTranscript(transcript.slice(0, messageIndex), { ...this.transcriptRenderOptions(), columns, trailingSpacer: false, dimFromMessageIndex: undefined }).rows.length;
1200
+ const centeredTop = rowsBefore - Math.floor(this.viewportScroll.viewportHeight / 2);
1201
+ this.viewportScroll.scrollTo(centeredTop, { disableFollow: true });
1202
+ }
1203
+ this.renderSnapshot();
1204
+ }
1205
+ planDialog(plan) {
1206
+ return new Promise((resolve) => {
1207
+ const dialog = new PlanDialogComponent(this.tui, plan, () => this.tui.terminal.rows, () => this.theme);
1208
+ const handle = this.tui.showOverlay(dialog, {
1209
+ anchor: "bottom-center",
1210
+ width: "100%",
1211
+ margin: { left: 1, right: 1 },
1212
+ });
1213
+ let settled = false;
1214
+ const finish = (decision) => {
1215
+ if (settled)
1216
+ return;
1217
+ settled = true;
1218
+ handle.hide();
1219
+ resolve(decision);
1220
+ };
1221
+ dialog.onApprove = (finalPlan) => finish({ action: "approve", plan: finalPlan });
1222
+ dialog.onReject = () => finish({ action: "reject", reason: "User rejected the plan." });
1223
+ this.tui.setFocus(dialog);
1224
+ });
1225
+ }
1226
+ openFeedback(initialDescription) {
1227
+ const collected = collectFeedback(this.options.agent, { description: "" });
1228
+ const { description: _description, ...base } = collected;
1229
+ let handle;
1230
+ const close = () => {
1231
+ handle?.hide();
1232
+ this.tui.setFocus(this.editor);
1233
+ this.renderSnapshot();
1234
+ };
1235
+ const recordResult = (result) => {
1236
+ if (result.kind === "success") {
1237
+ this.pushNotice(`Feedback submitted: ${result.url}`);
1238
+ }
1239
+ else if (result.kind === "error") {
1240
+ this.appendTranscriptRow({
1241
+ key: `feedback-error-${Date.now()}`,
1242
+ role: "error",
1243
+ content: `Feedback failed: ${result.message}`,
1244
+ });
1245
+ }
1246
+ };
1247
+ const dialog = new FeedbackDialogComponent(this.tui, base, initialDescription, {
1248
+ getTerminalRows: () => this.tui.terminal.rows,
1249
+ getTheme: () => this.theme,
1250
+ submit: this.options.submitFeedback,
1251
+ onDismiss: close,
1252
+ onResult: recordResult,
1253
+ onRender: () => this.renderSnapshot(),
1254
+ });
1255
+ handle = this.tui.showOverlay(dialog, {
1256
+ anchor: "bottom-center",
1257
+ width: "100%",
1258
+ margin: { left: 1, right: 1 },
1259
+ });
1260
+ this.tui.setFocus(dialog);
1261
+ }
1262
+ openFeishuSetup() {
1263
+ if (this.feishuSetupPhase)
1264
+ return;
1265
+ let handle;
1266
+ let closed = false;
1267
+ const close = (result) => {
1268
+ if (closed)
1269
+ return;
1270
+ closed = true;
1271
+ handle?.hide();
1272
+ this.feishuSetupPhase = null;
1273
+ this.tui.setFocus(this.editor);
1274
+ if (result.kind === "completed") {
1275
+ this.appendTranscriptRow({
1276
+ key: `feishu-setup-${Date.now()}`,
1277
+ role: "assistant",
1278
+ content: result.summary,
1279
+ syntheticKind: "ui_notice",
1280
+ });
1281
+ }
1282
+ else if (result.kind === "error") {
1283
+ this.appendTranscriptRow({
1284
+ key: `feishu-setup-error-${Date.now()}`,
1285
+ role: "error",
1286
+ content: `Feishu setup failed: ${result.message}`,
1287
+ });
1288
+ }
1289
+ else {
1290
+ this.pushNotice("已取消 Feishu setup。");
1291
+ }
1292
+ this.renderSnapshot();
1293
+ };
1294
+ const dialog = new FeishuSetupDialogComponent({
1295
+ getTerminalRows: () => this.tui.terminal.rows,
1296
+ getTheme: () => this.theme,
1297
+ onResult: close,
1298
+ onRender: () => this.renderSnapshot(),
1299
+ });
1300
+ handle = this.tui.showOverlay(dialog, {
1301
+ anchor: "bottom-center",
1302
+ width: "100%",
1303
+ margin: { left: 1, right: 1 },
1304
+ });
1305
+ this.feishuSetupPhase = { component: dialog, handle };
1306
+ this.tui.setFocus(dialog);
1307
+ dialog.start();
1308
+ }
1309
+ openWorkflowInspector(item) {
1310
+ let handle;
1311
+ const getSnapshot = () => {
1312
+ const workflow = this.options.controller.getWorkflows().find((candidate) => candidate.runId === item.id);
1313
+ const group = this.options.controller.getSubagentGroups().find((candidate) => (candidate.kind === "workflow" && (candidate.runId === item.id || candidate.id === item.id)));
1314
+ const members = workflow?.snapshots.length
1315
+ ? workflow.snapshots.map((snapshot) => ({
1316
+ subAgentId: snapshot.agentId,
1317
+ agentName: snapshot.agentName,
1318
+ nickname: snapshot.nickname,
1319
+ status: snapshot.status,
1320
+ category: snapshot.category,
1321
+ phase: snapshot.phase,
1322
+ route: snapshot.route,
1323
+ task: snapshot.task,
1324
+ summary: snapshot.summary,
1325
+ toolNotes: snapshot.toolNotes,
1326
+ error: snapshot.error,
1327
+ usage: snapshot.usage,
1328
+ createdAt: snapshot.createdAt,
1329
+ updatedAt: snapshot.updatedAt,
1330
+ }))
1331
+ : group?.members ?? item.members;
1332
+ return {
1333
+ id: item.id,
1334
+ title: workflow?.title ?? group?.label ?? item.title,
1335
+ status: workflow?.status ?? item.status,
1336
+ members,
1337
+ createdAt: workflow?.createdAt ?? item.createdAt,
1338
+ updatedAt: workflow?.updatedAt ?? item.updatedAt,
1339
+ };
1340
+ };
1341
+ const component = new WorkflowInspectorComponent({
1342
+ getSnapshot,
1343
+ getTerminalRows: () => this.tui.terminal.rows,
1344
+ theme: this.theme,
1345
+ onClose: () => handle?.hide(),
1346
+ onOpenAgent: (agentId) => {
1347
+ handle?.hide();
1348
+ const member = getSnapshot().members.find((candidate) => candidate.subAgentId === agentId);
1349
+ if (!member)
1350
+ return;
1351
+ this.openSubagentInspector({
1352
+ kind: "subagent",
1353
+ id: agentId,
1354
+ title: member.nickname ?? member.agentName ?? "subagent",
1355
+ status: member.status ?? "running",
1356
+ member,
1357
+ }, () => this.openWorkflowInspector(item));
1358
+ },
1359
+ onStop: (runId) => this.options.controller.stopWorkflow(runId),
1360
+ onRender: () => this.renderSnapshot(),
1361
+ });
1362
+ handle = this.tui.showOverlay(component, {
1363
+ width: "100%",
1364
+ maxHeight: "100%",
1365
+ margin: 1,
1366
+ });
1367
+ this.tui.setFocus(component);
1368
+ }
1369
+ openSubagentInspector(item, onClose) {
1370
+ let handle;
1371
+ const allAgentIds = () => this.options.controller.getSubagentGroups()
1372
+ .flatMap((group) => group.members)
1373
+ .map((member) => member.subAgentId)
1374
+ .filter((id) => !!id);
1375
+ const close = () => {
1376
+ handle?.hide();
1377
+ onClose?.();
1378
+ };
1379
+ const component = new SubagentInspectorComponent({
1380
+ agentId: item.id,
1381
+ controller: this.options.controller,
1382
+ theme: this.theme,
1383
+ getMember: () => this.options.controller.getSubagentGroups()
1384
+ .flatMap((group) => group.members)
1385
+ .find((member) => member.subAgentId === item.id) ?? item.member,
1386
+ getTerminalRows: () => this.tui.terminal.rows,
1387
+ renderOptions: () => this.transcriptRenderOptions(),
1388
+ onClose: close,
1389
+ onNavigate: (direction) => {
1390
+ const ids = allAgentIds();
1391
+ const current = ids.indexOf(item.id);
1392
+ if (current < 0 || ids.length < 2)
1393
+ return;
1394
+ const nextId = ids[(current + direction + ids.length) % ids.length];
1395
+ const member = this.options.controller.getSubagentGroups().flatMap((group) => group.members).find((candidate) => candidate.subAgentId === nextId);
1396
+ if (!member)
1397
+ return;
1398
+ handle?.hide();
1399
+ this.openSubagentInspector({
1400
+ kind: "subagent",
1401
+ id: nextId,
1402
+ title: member.nickname ?? member.agentName ?? "subagent",
1403
+ status: member.status ?? "running",
1404
+ member,
1405
+ }, onClose);
1406
+ },
1407
+ onRender: () => this.renderSnapshot(),
1408
+ });
1409
+ handle = this.tui.showOverlay(component, {
1410
+ width: "100%",
1411
+ maxHeight: "100%",
1412
+ margin: 1,
1413
+ });
1414
+ this.tui.setFocus(component);
1415
+ }
1416
+ handleTraceAction(action) {
1417
+ if (action.kind === "open-image") {
1418
+ const message = this.options.controller.getTranscript().find((candidate) => candidate.key === action.messageKey);
1419
+ const image = message?.images?.find((candidate) => candidate.label === action.imageLabel);
1420
+ if (image)
1421
+ this.openImageViewer(image);
1422
+ return;
1423
+ }
1424
+ if (action.kind === "open-task") {
1425
+ const task = this.options.controller.getBackgroundTasks()
1426
+ .find((candidate) => candidate.id === action.taskId);
1427
+ if (task) {
1428
+ this.openTaskInspector({
1429
+ kind: "task",
1430
+ id: task.id,
1431
+ title: task.description || task.command,
1432
+ status: task.status,
1433
+ task,
1434
+ });
1435
+ }
1436
+ else {
1437
+ const tool = this.options.controller.getTranscript()
1438
+ .flatMap((message) => message.toolCalls ?? [])
1439
+ .find((candidate) => candidate.metadata?.taskId === action.taskId
1440
+ && candidate.metadata?.taskLifecycle !== undefined);
1441
+ if (tool) {
1442
+ const lifecycle = String(tool.metadata?.taskLifecycle ?? "completed");
1443
+ const command = String(tool.args.command ?? action.taskId);
1444
+ const description = typeof tool.args.description === "string" ? tool.args.description : undefined;
1445
+ const startedAt = tool.startedAt ?? Date.now();
1446
+ const endedAt = typeof tool.metadata?.endedAt === "number" ? tool.metadata.endedAt : startedAt;
1447
+ this.openTaskInspector({
1448
+ kind: "task",
1449
+ id: action.taskId,
1450
+ title: description || command,
1451
+ status: lifecycle,
1452
+ task: {
1453
+ kind: "task",
1454
+ id: action.taskId,
1455
+ command,
1456
+ description,
1457
+ cwd: process.cwd(),
1458
+ status: lifecycle === "failed" ? "failed" : lifecycle === "killed" ? "killed" : "completed",
1459
+ exitCode: typeof tool.metadata?.exitCode === "number" ? tool.metadata.exitCode : null,
1460
+ startedAt,
1461
+ endedAt,
1462
+ outputTruncated: false,
1463
+ outputLines: typeof tool.metadata?.outputLines === "number" ? tool.metadata.outputLines : 0,
1464
+ },
1465
+ }, tool.result ?? "");
1466
+ }
1467
+ }
1468
+ return;
1469
+ }
1470
+ if (action.kind !== "open-subagent")
1471
+ return;
1472
+ const member = this.options.controller.getSubagentGroups()
1473
+ .flatMap((group) => group.members)
1474
+ .find((candidate) => candidate.subAgentId === action.subAgentId);
1475
+ if (!member)
1476
+ return;
1477
+ this.openSubagentInspector({
1478
+ kind: "subagent",
1479
+ id: action.subAgentId,
1480
+ title: member.nickname ?? member.agentName ?? "subagent",
1481
+ status: member.status ?? "running",
1482
+ member,
1483
+ });
1484
+ }
1485
+ openTaskInspector(item, fallbackOutput = "") {
1486
+ let handle;
1487
+ const taskOutput = () => this.options.controller.getBackgroundTaskOutput(item.id) || fallbackOutput;
1488
+ const component = new TaskInspectorComponent({
1489
+ id: item.id,
1490
+ title: item.title,
1491
+ getStatus: () => this.options.controller.getBackgroundTasks().find((task) => task.id === item.id)?.status ?? item.status,
1492
+ getOutput: taskOutput,
1493
+ getTerminalRows: () => this.tui.terminal.rows,
1494
+ onClose: () => handle?.hide(),
1495
+ onStop: () => this.options.controller.stopBackgroundTask(item.id),
1496
+ onCopy: () => copyToClipboard(taskOutput()),
1497
+ onRender: () => this.renderSnapshot(),
1498
+ theme: this.theme,
1499
+ });
1500
+ handle = this.tui.showOverlay(component, {
1501
+ width: "100%",
1502
+ maxHeight: "100%",
1503
+ margin: 1,
1504
+ });
1505
+ this.tui.setFocus(component);
1506
+ }
1507
+ approvalDialog(request) {
1508
+ return new Promise((resolve) => {
1509
+ const dialog = new ApprovalDialogComponent(request, () => this.tui.terminal.rows, {
1510
+ allowBashPrefix: this.options.bashAllowlist !== undefined,
1511
+ getTheme: () => this.theme,
1512
+ });
1513
+ const handle = this.tui.showOverlay(dialog, {
1514
+ anchor: "bottom-center",
1515
+ width: "100%",
1516
+ margin: { left: 1, right: 1 },
1517
+ });
1518
+ const finish = (choice) => {
1519
+ handle.hide();
1520
+ resolve(choice);
1521
+ };
1522
+ dialog.onSelect = finish;
1523
+ dialog.onCancel = () => finish({ kind: "reject" });
1524
+ this.tui.setFocus(dialog);
1525
+ });
1526
+ }
1527
+ openNextQuestion() {
1528
+ if (this.disposed || this.activeQuestion)
1529
+ return;
1530
+ const request = this.questionQueue.shift();
1531
+ if (!request)
1532
+ return;
1533
+ const questionController = this.options.questionController;
1534
+ if (!questionController)
1535
+ return;
1536
+ const dialog = new QuestionDialogComponent(request, () => this.tui.terminal.rows, () => this.theme);
1537
+ const handle = this.tui.showOverlay(dialog, {
1538
+ anchor: "bottom-center",
1539
+ width: "100%",
1540
+ margin: { left: 1, right: 1 },
1541
+ });
1542
+ let closed = false;
1543
+ const close = () => {
1544
+ if (closed)
1545
+ return;
1546
+ closed = true;
1547
+ handle.hide();
1548
+ if (this.activeQuestion?.id === request.id)
1549
+ this.activeQuestion = null;
1550
+ this.openNextQuestion();
1551
+ };
1552
+ this.activeQuestion = { id: request.id, close };
1553
+ dialog.onSubmit = (answers) => {
1554
+ close();
1555
+ questionController.reply(request.id, answers);
1556
+ };
1557
+ dialog.onCancel = () => {
1558
+ close();
1559
+ questionController.reject(request.id);
1560
+ };
1561
+ this.tui.setFocus(dialog);
1562
+ }
1563
+ removeQueuedQuestion(id) {
1564
+ const index = this.questionQueue.findIndex((request) => request.id === id);
1565
+ if (index >= 0)
1566
+ this.questionQueue.splice(index, 1);
1567
+ }
1568
+ // ---- Lifecycle ----------------------------------------------------------
1569
+ start() {
1570
+ this.tui.start();
1571
+ this.renderSnapshot();
1572
+ this.watchUpdateNoticeRefresh();
1573
+ }
1574
+ watchUpdateNoticeRefresh() {
1575
+ if (this.updateNoticeRefreshStarted)
1576
+ return;
1577
+ this.updateNoticeRefreshStarted = true;
1578
+ const refresh = this.options.updateNoticeRefresh;
1579
+ if (!refresh)
1580
+ return;
1581
+ void refresh.then((notice) => {
1582
+ if (this.disposed || !notice || notice === this.currentUpdateNotice)
1583
+ return;
1584
+ this.currentUpdateNotice = notice;
1585
+ // The root fullscreen renderer includes the welcome card. The legacy
1586
+ // main-to-fullscreen transition does not, so it needs a transcript row
1587
+ // even before the first user turn.
1588
+ if (this.fullscreen || this.options.controller.getTranscript().length > 0) {
1589
+ this.pushNotice(notice);
1590
+ }
1591
+ else {
1592
+ this.renderSnapshot();
1593
+ }
1594
+ }).catch(() => {
1595
+ // Update checks are best-effort and must never disturb an active session.
1596
+ });
1597
+ }
1598
+ dispose() {
1599
+ if (this.disposed)
1600
+ return;
1601
+ this.disposed = true;
1602
+ this.controllerUnsubscribe?.();
1603
+ this.controllerUnsubscribe = null;
1604
+ this.metadataUnsubscribe?.();
1605
+ this.metadataUnsubscribe = null;
1606
+ this.metadataManager = null;
1607
+ this.questionUnsubscribe?.();
1608
+ this.questionUnsubscribe = null;
1609
+ this.activeQuestion?.close();
1610
+ this.activeQuestion = null;
1611
+ this.questionQueue.length = 0;
1612
+ this.feishuSetupPhase?.component.dispose();
1613
+ this.feishuSetupPhase?.handle.hide();
1614
+ this.feishuSetupPhase = null;
1615
+ if (this.providerKeyPhase) {
1616
+ this.providerKeyPhase.input.setValue("");
1617
+ this.providerKeyPhase = null;
1618
+ }
1619
+ this.options.questionController?.rejectAll();
1620
+ // Root lifecycle events (notably SIGTERM) can dispose the app while the
1621
+ // alternate screen is still open. Tear it down directly so its controller
1622
+ // listener, spinner timers, and terminal mode cannot outlive the root app.
1623
+ this.fullscreen?.dispose();
1624
+ this.fullscreen = null;
1625
+ this.animationClock.dispose();
1626
+ this.composer.dispose();
1627
+ this.streamingMessage.dispose();
1628
+ this.tasksPane.dispose();
1629
+ this.options.controller.shutdown("user-quit");
1630
+ this.overlays.dispose();
1631
+ this.options.callbacks.onExitRequest();
1632
+ // The fullscreen frame is transient application chrome. Reprinting it
1633
+ // into the restored main buffer leaves the docked composer above the
1634
+ // post-session summary. Exit the alternate screen without exporting that
1635
+ // frame; regular mode keeps its native scrollback behavior unchanged.
1636
+ this.tui.stop({ preserveScreen: this.tui.mode === "fullscreen" });
1637
+ }
1638
+ async waitUntilExit() {
1639
+ await new Promise((resolve) => {
1640
+ const original = this.dispose.bind(this);
1641
+ this.dispose = () => {
1642
+ original();
1643
+ resolve();
1644
+ };
1645
+ });
1646
+ }
1647
+ }