@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
@@ -1,2377 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { Box, Text, useApp, useInput, useStdout } from "ink";
4
- import { AgentAbortError, INTERRUPTED_ASSISTANT_CONTENT } from "../agent.js";
5
- import { SessionManager } from "../session.js";
6
- import { registry as slashRegistry } from "../slash-commands/index.js";
7
- import { UserConfig, maskKey } from "../config.js";
8
- import { InputBox, isCtrlCInput, isCtrlLetterInput, } from "./input-box.js";
9
- import { MessageList } from "./message-list.js";
10
- import { isMultiplexedTerminal } from "./terminal-env.js";
11
- import { appendTextPart, appendToolPart, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
12
- import { AgentRunInputQueue } from "../agent/input-controller.js";
13
- import { canvasBackgroundFor, paletteFor, ThemeProvider } from "./theme.js";
14
- import { ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
15
- import { FeishuSetupPicker } from "./feishu-setup-picker.js";
16
- import { BUILTIN_PROVIDERS, ProviderRegistry, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
17
- import { buildSystemPrompt } from "../system-prompt.js";
18
- import { getAvailableThinkingLevels, isThinkingOnlyLevels, isThinkingToggleModel, normalizeThinkingLevel } from "../provider-transform.js";
19
- import { FooterBar, buildFooterData } from "./footer.js";
20
- import { SkillRegistry } from "../skills/registry.js";
21
- import { parseSkillInvocation } from "../skills/invocation.js";
22
- import { useTerminalSize } from "./use-terminal-size.js";
23
- import { WelcomeBanner, shouldShowWelcomeBanner } from "./welcome.js";
24
- import { BubbleCodeWordmark } from "./wordmark.js";
25
- import { expandAtMentions } from "./file-mentions.js";
26
- import { CompactionProgressCard } from "./compaction-progress.js";
27
- import { PlanConfirm } from "./plan-confirm.js";
28
- import { ApprovalDialog } from "./approval/approval-dialog.js";
29
- import { getNextPermissionMode } from "../permission/mode.js";
30
- import { QuestionDialog } from "./question-dialog.js";
31
- import { FeedbackDialog } from "./feedback-dialog.js";
32
- import { collectFeedback } from "../feedback/collect.js";
33
- import { isKeyReleaseEvent } from "./key-events.js";
34
- import { errorMessage, formatModelSwitchError, modelSwitchTarget, switchAgentModel } from "../tui/model-switch.js";
35
- import { formatImageUserDisplayText, nextImageDisplayLabelStart } from "../tui/image-display.js";
36
- import { decideStartingSubmitFingerprint, submitPayloadFingerprint } from "./submit-dedupe.js";
37
- import { isQueuedInputForCurrentSession, queuedAndPendingDisplayKeys, } from "./input-queue.js";
38
- import { SessionPicker } from "./session-picker.js";
39
- import { SubagentInspector } from "./subagent-inspector.js";
40
- import { accumulateLiveSubagentUpdate, collectSubagentGroups, mergeToolMetadata, pruneSettledLiveSubagentTools, subagentSummary } from "./subagent-view.js";
41
- import { sessionDisplayName } from "../tui/session-display.js";
42
- import { parseGoalCommand } from "../goal/command.js";
43
- import { continuationPrompt, initialPrompt } from "../goal/prompts.js";
44
- import { shouldContinueGoal, stopReasonNotice } from "../goal/engine.js";
45
- import { goalCompleteNotice, goalIndicatorLine, goalSummaryText } from "../goal/format.js";
46
- import { tokenUsageTotal } from "../goal/usage.js";
47
- import { formatInternalContextBlock, isInternalBlockOnlyContent } from "../agent/internal-reminder-sanitizer.js";
48
- import { MAX_ACTIVE_LOOPS, decideLoopFiring, formatInterval, formatLoopList, parseLoopCommand, } from "../loop/engine.js";
49
- import { TaskWakeCoalescer, findDanglingTaskStarts, formatTaskWakeSummary, isPidAlive, shouldFireTaskWake, taskEligibleForWake, } from "../tasks/wake.js";
50
- import { STREAMING_FLUSH_INTERVAL_MS, TASK_WAKE_DEBOUNCE_MS, buildTips, formatContextUsageLabel, friendlyCwd, parsePartialArgs, sessionBasename, slashResultNoticeKind, taskRowSummary, truncate, withMessageKey, } from "./app-helpers.js";
51
- import { reconstructDisplayMessages } from "./display-reconstruct.js";
52
- export { reconstructDisplayMessages } from "./display-reconstruct.js";
53
- import { INK_LOCAL_SLASH_COMMANDS, CommandPalette, McpReconnectPicker, buildCommandPaletteItems, buildMcpReconnectItems, } from "./command-palette.js";
54
- export { INK_LOCAL_SLASH_COMMANDS } from "./command-palette.js";
55
- import { RewindPicker } from "./rewind-picker.js";
56
- import { StatsPanel } from "./stats-panel.js";
57
- import { WaitingIndicator } from "./waiting-indicator.js";
58
- import { collectUsageStatsBundle } from "../stats/usage.js";
59
- import { GrokRuntimeError } from "../external-runtime/grok-errors.js";
60
- import { GROK_LOCAL_SLASH_COMMANDS, classifyGrokInput, } from "../external-runtime/grok-input-policy.js";
61
- import { GROK_SUBSCRIPTION_PROVIDER_ID, isGrokSubscriptionProviderId, withGrokSubscriptionProvider, } from "../external-runtime/grok-provider.js";
62
- import { classifyExternalRuntimeBinding, stopExternalRuntimeForSessionSwitch, } from "../external-runtime/session-policy.js";
63
- import { rmSync } from "node:fs";
64
- import { execFile } from "node:child_process";
65
- // Canonical implementation lives in the sanitizer module so non-TUI surfaces
66
- // (session pickers, desktop host) share it; re-exported here for existing users.
67
- export { isInternalBlockOnlyContent };
68
- export function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, processManager, tasksAutoResume, promotionChannel, updateNotice, updateNoticeRefresh, hookController, externalRuntime, onExit }) {
69
- const [sessionManager, setSessionManager] = useState(initialSessionManager);
70
- const [externalRuntimeBinding, setExternalRuntimeBinding] = useState(() => initialSessionManager?.getMetadata().externalRuntime);
71
- const sessionManagerRef = useRef(sessionManager);
72
- sessionManagerRef.current = sessionManager;
73
- const externalRuntimeBindingRef = useRef(externalRuntimeBinding);
74
- externalRuntimeBindingRef.current = externalRuntimeBinding;
75
- const [themeMode, setThemeMode] = useState(initialThemeMode ?? "auto");
76
- // `detectedTheme` is captured once at startup in main.ts. We keep it in state
77
- // so future re-detection (e.g. if a user runs `/theme auto` after switching
78
- // their terminal) is possible without re-mounting the app. For now it never
79
- // changes after first render.
80
- const [autoResolved] = useState(detectedTheme ?? "dark");
81
- const palette = useMemo(() => {
82
- const resolved = themeMode === "auto" ? autoResolved : themeMode;
83
- const base = paletteFor(resolved, themeOverrides);
84
- // A user override wins; otherwise paint the canvas only when a forced
85
- // theme mismatches the terminal it is running in.
86
- if (base.background === undefined) {
87
- const canvas = canvasBackgroundFor(themeMode, resolved, autoResolved);
88
- if (canvas !== undefined)
89
- return { ...base, background: canvas };
90
- }
91
- return base;
92
- }, [themeMode, autoResolved, themeOverrides]);
93
- const applyThemeMode = useCallback((mode) => {
94
- setThemeMode(mode);
95
- onThemeModeChange?.(mode);
96
- }, [onThemeModeChange]);
97
- // Theme mode at the moment the /theme picker opened, so Esc can restore it
98
- // after live-previewing other themes while navigating the picker.
99
- const themeModeRef = useRef(themeMode);
100
- themeModeRef.current = themeMode;
101
- const themePickerRevertRef = useRef("auto");
102
- const themeResolved = themeMode === "auto" ? autoResolved : themeMode;
103
- const { exit } = useApp();
104
- const [messages, setMessages] = useState(() => reconstructDisplayMessages(agent.messages));
105
- const nextImageDisplayLabelStartRef = useRef(nextImageDisplayLabelStart(messages));
106
- const [isRunning, setIsRunning] = useState(false);
107
- const [streamingContent, setStreamingContent] = useState("");
108
- const [streamingReasoning, setStreamingReasoning] = useState("");
109
- const [streamingTools, setStreamingTools] = useState([]);
110
- const [streamingParts, setStreamingParts] = useState([]);
111
- // Background-child updates that outlived their launching tool call's
112
- // streaming round (toolCalls is cleared on every turn_start): keyed by the
113
- // launching call id, they feed the inspector as synthetic tool calls so
114
- // traces stay live e.g. while a wait_workflow blocks. The version counter
115
- // triggers recompute since the ref mutates in place.
116
- const liveSubagentToolsRef = useRef(new Map());
117
- const [liveSubagentVersion, setLiveSubagentVersion] = useState(0);
118
- // Transcript-reset paths (/clear, session switch, /rewind) must drop the
119
- // accumulator too, or ghost subagent groups from the wiped conversation keep
120
- // feeding the entry line and inspector for the rest of the process.
121
- const clearLiveSubagentTools = useCallback(() => {
122
- if (liveSubagentToolsRef.current.size === 0)
123
- return;
124
- liveSubagentToolsRef.current.clear();
125
- setLiveSubagentVersion((version) => version + 1);
126
- }, []);
127
- // Live subagent groups for the inspector opened from the subagent entry line;
128
- // recomputed each render so it reflects members as their events stream into the transcript.
129
- const subagentGroups = useMemo(() => collectSubagentGroups(messages, [...streamingTools, ...liveSubagentToolsRef.current.values()]),
130
- // eslint-disable-next-line react-hooks/exhaustive-deps
131
- [messages, streamingTools, liveSubagentVersion]);
132
- const subagentMembers = useMemo(() => subagentGroups.flatMap((g) => g.members), [subagentGroups]);
133
- // Down-arrow from the composer focuses the subagent entry line; Enter then
134
- // opens the inspector, Esc/Up returns to the composer (Claude Code parity).
135
- const [subagentEntryFocused, setSubagentEntryFocused] = useState(false);
136
- useEffect(() => {
137
- if (subagentMembers.length === 0 && subagentEntryFocused)
138
- setSubagentEntryFocused(false);
139
- }, [subagentMembers.length, subagentEntryFocused]);
140
- // Live progress for a manual `/compact` run (null when not compacting).
141
- const [compaction, setCompaction] = useState(null);
142
- // The reasoning level is persisted on the agent (agent.thinking); display it
143
- // directly. switchAgentModel re-normalizes on explicit model switches, so the
144
- // initial value needs no clamping against the (possibly not-yet-discovered)
145
- // model ladder.
146
- const [thinkingLevel, setThinkingLevel] = useState(agent.thinking);
147
- const [permissionMode, setPermissionMode] = useState(agent.mode);
148
- const [goalLine, setGoalLine] = useState("");
149
- const [branch, setBranch] = useState(undefined);
150
- const [contextUsage, setContextUsage] = useState("");
151
- useEffect(() => {
152
- let cancelled = false;
153
- execFile("git", ["-C", args.cwd, "branch", "--show-current"], { timeout: 3000 }, (error, stdout) => {
154
- if (cancelled)
155
- return;
156
- if (error)
157
- return;
158
- const name = stdout.trim();
159
- setBranch(name || undefined);
160
- });
161
- return () => { cancelled = true; };
162
- }, [args.cwd]);
163
- // Push-based sync: the agent notifies on every resident-context mutation
164
- // (message rewrites from /clear /compact /rewind and session switches,
165
- // per-message appends during runs, model/provider switches), so this single
166
- // subscription replaces the per-path manual refreshes that were easy to
167
- // forget (the /clear footer bug). Initial read covers agents mounted with
168
- // pre-existing history.
169
- //
170
- // Notifications are trailing-edge debounced: a snapshot run is O(all
171
- // resident messages) (tiktoken providers measured ~225ms at 400 messages),
172
- // and a tool-dense turn appends 5-15 messages synchronously — recomputing
173
- // per append would block the run loop 1-3s. The footer is a human-facing
174
- // readout; one recomputation 150ms after the last append is indistinguishable.
175
- useEffect(() => {
176
- let timer;
177
- const update = () => setContextUsage(formatContextUsageLabel(agent.getContextUsageSnapshot()));
178
- update();
179
- const off = agent.onContextChanged(() => {
180
- clearTimeout(timer);
181
- timer = setTimeout(update, 150);
182
- });
183
- return () => {
184
- clearTimeout(timer);
185
- off();
186
- };
187
- }, [agent]);
188
- const [currentUpdateNotice, setCurrentUpdateNotice] = useState(updateNotice);
189
- const [pendingPlan, setPendingPlan] = useState(null);
190
- const [pendingApproval, setPendingApproval] = useState(null);
191
- const [pendingQuestion, setPendingQuestion] = useState(null);
192
- const [pendingFeedback, setPendingFeedback] = useState(null);
193
- const [pickerMode, setPickerMode] = useState(null);
194
- const [grokModels, setGrokModels] = useState([]);
195
- const [statsPanel, setStatsPanel] = useState(null);
196
- const [cursorResetEpoch, setCursorResetEpoch] = useState(0);
197
- const [composerDraft, setComposerDraft] = useState(null);
198
- const [keyProviderId, setKeyProviderId] = useState(null);
199
- const [showThinking, setShowThinking] = useState(false);
200
- const [verboseTrace, setVerboseTrace] = useState(false);
201
- const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
202
- const { columns: terminalColumns, rows: terminalRows } = useTerminalSize();
203
- const showWelcome = shouldShowWelcomeBanner({
204
- messages,
205
- startedWithVisibleHistory: startedWithVisibleHistoryRef.current,
206
- });
207
- const showWelcomeRef = useRef(showWelcome);
208
- const activeAbortRef = useRef(null);
209
- const exitRequestedRef = useRef(false);
210
- const sessionStartRef = useRef(Date.now());
211
- // Bumped whenever the settled transcript is rebuilt non-monotonically
212
- // (/clear, /compact, /rewind, session switch). Used as the <Static> key in
213
- // MessageList so Ink discards its already-printed rows and re-prints the
214
- // rebuilt list onto a freshly-cleared screen instead of appending duplicates.
215
- const [staticGeneration, setStaticGeneration] = useState(0);
216
- // The clear must go through Ink's stdout hook, not a raw process.stdout
217
- // write: Ink replays its last dynamic frame after any external write
218
- // (restoreLastOutput), which is the only repaint guarantee when the
219
- // generation bump produces no new output — with zero settled rows (fresh
220
- // welcome screen) <Static> emits nothing and the dynamic frame diffs equal,
221
- // so Ink skips the write and a raw clear would leave the screen blank.
222
- const { write: writeThroughInk } = useStdout();
223
- const reprintTranscript = useCallback(() => {
224
- if (process.stdout.isTTY) {
225
- writeThroughInk("\x1b[0m\x1b[2J\x1b[3J\x1b[H");
226
- }
227
- setStaticGeneration((generation) => generation + 1);
228
- }, [writeThroughInk]);
229
- // Steer/queue while the agent runs:
230
- // Enter steers the current run via the agent's input controller; Tab (or an
231
- // ineligible input) queues for the next turn. Both render placeholder user
232
- // rows whose badge tracks the input's lifecycle.
233
- const inputControllerRef = useRef(null);
234
- const pendingSteersRef = useRef(new Map());
235
- const queuedInputsRef = useRef([]);
236
- const [pendingSteerCount, setPendingSteerCount] = useState(0);
237
- const [queuedCount, setQueuedCount] = useState(0);
238
- const startingSubmitFingerprintRef = useRef(null);
239
- const [startingSubmitFingerprint, setStartingSubmitFingerprint] = useState(null);
240
- const nextRunIdRef = useRef(0);
241
- // Invalidates late external-runtime events whenever the Bubble session or
242
- // runtime binding changes. Native Agent events retain their existing path.
243
- const externalRuntimeGenerationRef = useRef(0);
244
- // Set true the moment /quit is invoked so we can hide dynamic UI (composer,
245
- // waiting indicator, footer) before Ink snapshots its final frame into the
246
- // shell scrollback. Without this, the last visible "> " input row stays
247
- // glued to the bottom of the terminal after exit.
248
- const [isExiting, setIsExiting] = useState(false);
249
- // 1Hz tick keeps the composer activity indicator animated while the agent is
250
- // running without churning renders at idle.
251
- const [nowTick, setNowTick] = useState(() => Date.now());
252
- // Live background-task snapshot for the status row (design §2.5); refreshed
253
- // by the manager subscription effect below.
254
- const [taskSnapshot, setTaskSnapshot] = useState([]);
255
- const runningTasks = useMemo(() => taskSnapshot.filter((task) => task.status === "running"), [taskSnapshot]);
256
- // Timestamp of when the current agent run started. Used only for the final
257
- // per-task duration summary.
258
- const runStartRef = useRef(null);
259
- // Mark the moment the run started; flips back to null in the finally block.
260
- // Also ticks while background tasks run at idle so their elapsed counter
261
- // keeps moving (design §2.5).
262
- useEffect(() => {
263
- if (!isRunning && runningTasks.length === 0)
264
- return;
265
- setNowTick(Date.now());
266
- const t = setInterval(() => setNowTick(Date.now()), 1000);
267
- return () => clearInterval(t);
268
- }, [isRunning, runningTasks.length]);
269
- const userConfig = new UserConfig();
270
- const safeRegistry = registry ?? new ProviderRegistry(userConfig);
271
- const safeSkillRegistry = skillRegistry ?? new SkillRegistry({
272
- cwd: args.cwd,
273
- skillPaths: userConfig.getSkillPaths(),
274
- });
275
- const externalRuntimeBindingKind = classifyExternalRuntimeBinding(externalRuntimeBinding);
276
- const externalSessionBound = externalRuntimeBindingKind !== "none";
277
- const grokSessionBound = externalRuntimeBindingKind === "grok";
278
- const unsupportedExternalSessionBound = externalRuntimeBindingKind === "unsupported";
279
- const refreshExternalRuntimeBinding = useCallback((manager) => {
280
- const next = (manager ?? sessionManagerRef.current)?.getMetadata().externalRuntime;
281
- externalRuntimeBindingRef.current = next;
282
- setExternalRuntimeBinding(next);
283
- }, []);
284
- useEffect(() => {
285
- setCurrentUpdateNotice(updateNotice);
286
- }, [updateNotice]);
287
- useEffect(() => {
288
- showWelcomeRef.current = showWelcome;
289
- }, [showWelcome]);
290
- useEffect(() => {
291
- if (!goalStore)
292
- return;
293
- let persistSuspended = false;
294
- const persistGoal = (goal) => {
295
- if (!sessionManager)
296
- return;
297
- try {
298
- const metadata = sessionManager.getMetadata();
299
- sessionManager.setMetadata({ ...metadata, goal: goal ?? undefined });
300
- }
301
- catch {
302
- // Goal persistence is best-effort; never break the run loop over it.
303
- }
304
- };
305
- const unsubscribe = goalStore.onChange((goal) => {
306
- setGoalLine(goal ? goalIndicatorLine(goal) : "");
307
- if (!persistSuspended)
308
- persistGoal(goal);
309
- });
310
- const persisted = sessionManager?.getMetadata().goal;
311
- if (persisted) {
312
- persistSuspended = true;
313
- goalStore.loadFrom(persisted.status === "active" ? { ...persisted, status: "paused" } : persisted);
314
- persistSuspended = false;
315
- }
316
- else {
317
- persistSuspended = true;
318
- goalStore.loadFrom(undefined);
319
- persistSuspended = false;
320
- setGoalLine("");
321
- }
322
- return unsubscribe;
323
- }, [goalStore, sessionManager]);
324
- const requestExit = useCallback(() => {
325
- if (exitRequestedRef.current)
326
- return;
327
- exitRequestedRef.current = true;
328
- // Drop the composer / waiting indicator / footer from the React tree
329
- // *before* we tell Ink to exit, so Ink's final log-update snapshot
330
- // doesn't leave an empty "> " row behind in the shell scrollback.
331
- setIsExiting(true);
332
- // Cancel any in-flight agent run first so its tools / network calls
333
- // don't keep emitting text after Ink unmounts and corrupt the
334
- // restored shell prompt.
335
- if (activeAbortRef.current) {
336
- try {
337
- activeAbortRef.current.abort(new AgentAbortError("Exiting Bubble."));
338
- }
339
- catch {
340
- // ignore — abort is best effort during shutdown
341
- }
342
- activeAbortRef.current = null;
343
- }
344
- void (async () => {
345
- // Yield once so React can commit the `isExiting=true` render
346
- // (which strips the composer/footer) before we hand control to
347
- // Ink's teardown. Without this, on the no-flushMemory path the
348
- // exit() below races the next React commit and Ink snapshots the
349
- // pre-exit frame with the composer still visible.
350
- await new Promise((resolve) => setImmediate(resolve));
351
- let flushError = null;
352
- if (flushMemory) {
353
- // Bound the flush so a stuck LLM/network call cannot trap the TUI.
354
- let timer;
355
- try {
356
- await Promise.race([
357
- flushMemory(),
358
- new Promise((_, reject) => {
359
- timer = setTimeout(() => reject(new Error("flushMemory timed out after 3s")), 3000);
360
- }),
361
- ]);
362
- }
363
- catch (err) {
364
- flushError = err;
365
- }
366
- finally {
367
- if (timer)
368
- clearTimeout(timer);
369
- }
370
- }
371
- // Hand off to Ink. Ink's render instance owns TTY teardown (raw mode,
372
- // cursor, alt-screen); doing it ourselves here races with that and
373
- // leaves the terminal in odd states. run.tsx awaits waitUntilExit()
374
- // and then main.ts handles the rest.
375
- exit();
376
- // Surface flush failures *after* Ink has restored the screen so the
377
- // warning lands on the real shell instead of being clobbered.
378
- if (flushError) {
379
- const message = flushError instanceof Error ? flushError.message : String(flushError);
380
- process.nextTick(() => {
381
- process.stderr.write(`warning: failed to flush memory on exit: ${message}\n`);
382
- });
383
- }
384
- onExit?.({ wallMs: Date.now() - sessionStartRef.current });
385
- })();
386
- }, [exit, flushMemory, onExit]);
387
- useEffect(() => {
388
- if (!planHandlerRef)
389
- return;
390
- planHandlerRef.current = (plan) => new Promise((resolve) => {
391
- setPendingPlan({ plan, resolve });
392
- });
393
- return () => {
394
- if (planHandlerRef.current) {
395
- planHandlerRef.current = undefined;
396
- }
397
- };
398
- }, [planHandlerRef]);
399
- useEffect(() => {
400
- if (!approvalHandlerRef)
401
- return;
402
- approvalHandlerRef.current = (request) => new Promise((resolve) => {
403
- setPendingApproval({ request, resolve });
404
- });
405
- return () => {
406
- if (approvalHandlerRef.current) {
407
- approvalHandlerRef.current = undefined;
408
- }
409
- };
410
- }, [approvalHandlerRef]);
411
- useEffect(() => {
412
- if (!questionController)
413
- return;
414
- const syncFirstPending = () => {
415
- setPendingQuestion((current) => current ?? questionController.list()[0] ?? null);
416
- };
417
- const unsubscribe = questionController.subscribe((event) => {
418
- if (event.type === "asked") {
419
- setPendingQuestion(event.request);
420
- return;
421
- }
422
- setPendingQuestion((current) => current?.id === event.request.id ? null : current);
423
- setTimeout(syncFirstPending, 0);
424
- });
425
- syncFirstPending();
426
- return unsubscribe;
427
- }, [questionController]);
428
- const rebuildSystemPrompt = useCallback((overrides) => {
429
- const modelParts = agent.model.includes(":")
430
- ? agent.model.split(":")
431
- : [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
432
- const providerId = modelParts[0];
433
- agent.setSystemPrompt(buildSystemPrompt({
434
- agentName: "Bubble",
435
- configuredProvider: providerId,
436
- configuredModel: displayModel(agent.model),
437
- configuredModelId: agent.model,
438
- thinkingLevel: overrides?.thinkingLevel ?? agent.thinking,
439
- mode: overrides?.mode ?? agent.mode,
440
- workingDir: args.cwd,
441
- ...agent.getSystemPromptToolOptions(),
442
- }));
443
- }, [agent, args.cwd, safeRegistry, safeSkillRegistry]);
444
- useInput((input, key) => {
445
- if (isKeyReleaseEvent(key))
446
- return;
447
- if (isCtrlCInput(input, key)) {
448
- requestExit();
449
- return;
450
- }
451
- // Scrolling is the terminal's job now: settled rows live in native
452
- // scrollback (committed via <Static>), so the wheel, tmux copy-mode, and
453
- // PageUp/PageDown scroll the real terminal with no app involvement and no
454
- // flicker. Bubble no longer intercepts mouse reports or page keys, which
455
- // also frees the arrow keys entirely for composer history.
456
- if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback || statsPanel)
457
- return;
458
- // Subagent entry is focused (the composer is disabled): Enter opens the
459
- // inspector, Up/Esc returns to the composer. Other keys just return focus.
460
- if (subagentEntryFocused && !pickerMode) {
461
- if (key.return) {
462
- setSubagentEntryFocused(false);
463
- setStatsPanel(null);
464
- setPickerMode("agents");
465
- return;
466
- }
467
- if (key.escape || key.upArrow) {
468
- setSubagentEntryFocused(false);
469
- return;
470
- }
471
- if (key.downArrow)
472
- return; // stay focused
473
- setSubagentEntryFocused(false);
474
- return;
475
- }
476
- if (isCtrlLetterInput(input, key, "p") && !pickerMode && !activeAbortRef.current) {
477
- setStatsPanel(null);
478
- setPickerMode("slash");
479
- return;
480
- }
481
- // Ctrl+G: promote the running foreground bash command to a background
482
- // task (design §2.5). A no-op when nothing promotable is running.
483
- if (isCtrlLetterInput(input, key, "g") && !pickerMode && promotionChannel) {
484
- const liveTools = [
485
- ...streamingTools,
486
- ...streamingParts.flatMap((part) => (part.type === "tools" ? part.toolCalls : [])),
487
- ];
488
- const candidate = [...liveTools].reverse().find((tool) => tool.name === "bash" && !tool.result && promotionChannel.hasPromotable(tool.id));
489
- if (candidate) {
490
- const taskId = promotionChannel.requestPromotion(candidate.id);
491
- if (taskId) {
492
- addMessage("assistant", `Moved to background: ${taskId} — the command keeps running (Ctrl+G).`, "ui_notice");
493
- }
494
- }
495
- return;
496
- }
497
- if (key.ctrl && key.shift && input.toLowerCase() === "m" && !pickerMode) {
498
- if (externalSessionBound) {
499
- addMessage("assistant", "MCP is unavailable in external runtime sessions (no Bubble workspace access).");
500
- return;
501
- }
502
- if (!mcpManager || mcpManager.getStates().length === 0) {
503
- addMessage("assistant", "No MCP servers configured.");
504
- }
505
- else {
506
- setStatsPanel(null);
507
- setPickerMode("mcp-reconnect");
508
- }
509
- return;
510
- }
511
- if (isCtrlLetterInput(input, key, "t") && !pickerMode) {
512
- setShowThinking((current) => {
513
- const next = !current;
514
- addMessage("assistant", next ? "Thinking blocks visible" : "Thinking blocks hidden");
515
- return next;
516
- });
517
- return;
518
- }
519
- if (isCtrlLetterInput(input, key, "o") && !pickerMode) {
520
- setVerboseTrace((v) => !v);
521
- reprintTranscript();
522
- return;
523
- }
524
- // Ctrl+R: cycle thinking level (formerly Shift+Tab)
525
- if (isCtrlLetterInput(input, key, "r") && !pickerMode) {
526
- if (externalSessionBound) {
527
- if (!grokSessionBound || !externalRuntime) {
528
- addMessage("assistant", "The external runtime manages its own model and reasoning settings.");
529
- return;
530
- }
531
- const bound = externalRuntimeBindingRef.current;
532
- void (bound?.sessionId
533
- ? externalRuntime.hydrateSession(bound.sessionId, bound.modelId, bound.reasoningEffort)
534
- : Promise.resolve()).then(async () => await externalRuntime.listModels()).then(async (models) => {
535
- const current = externalRuntime.getModelSelection();
536
- const model = models.find((candidate) => candidate.id === current.modelId);
537
- if (!model || model.reasoningLevels.length < 2) {
538
- addMessage("assistant", `${current.modelId ?? "The current Grok model"} has no alternate reasoning effort.`);
539
- return;
540
- }
541
- const index = Math.max(0, model.reasoningLevels.indexOf(current.reasoningEffort));
542
- const next = model.reasoningLevels[(index + 1) % model.reasoningLevels.length];
543
- const selection = await externalRuntime.setModel(model.id, next);
544
- const manager = sessionManagerRef.current;
545
- const binding = manager?.getMetadata().externalRuntime;
546
- if (manager && binding && isGrokSubscriptionProviderId(binding.id)) {
547
- manager.updateMetadata({ externalRuntime: { ...binding, ...selection } });
548
- manager.appendMarker("thinking_level_switch", selection.reasoningEffort);
549
- refreshExternalRuntimeBinding(manager);
550
- }
551
- addMessage("assistant", `Grok reasoning effort set to ${selection.reasoningEffort}.`);
552
- }).catch((error) => addMessage("error", `Failed to switch Grok reasoning: ${errorMessage(error)}`));
553
- return;
554
- }
555
- const modelParts = agent.model.includes(":")
556
- ? agent.model.split(":")
557
- : [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
558
- const providerId = modelParts[0];
559
- const modelId = modelParts.slice(1).join(":");
560
- const availableLevels = getAvailableThinkingLevels(providerId, modelId);
561
- const currentLevel = normalizeThinkingLevel(agent.thinking, availableLevels);
562
- const currentIndex = availableLevels.indexOf(currentLevel);
563
- const nextLevel = availableLevels[(currentIndex + 1) % availableLevels.length];
564
- agent.thinking = nextLevel;
565
- rebuildSystemPrompt({ thinkingLevel: nextLevel });
566
- userConfig.setDefaultThinkingLevel(nextLevel);
567
- setThinkingLevel(nextLevel);
568
- sessionManager?.updateMetadata({ model: agent.model, thinkingLevel: nextLevel, reasoningEffort: nextLevel });
569
- sessionManager?.appendMarker("thinking_level_switch", nextLevel);
570
- return;
571
- }
572
- // Shift+Tab: cycle through permission modes (default → acceptEdits → plan
573
- // → [bypassPermissions if enabled] → default). Agent.setMode injects a
574
- // <system-reminder>, so we do not rebuild the cache-friendly system prompt here.
575
- if (key.tab && key.shift && !pickerMode) {
576
- const nextMode = getNextPermissionMode(agent.mode);
577
- // setMode fires onModeUpdate, which is the single writer of the
578
- // mode_switch marker — appending here too double-logs every switch.
579
- agent.setMode(nextMode);
580
- setPermissionMode(nextMode);
581
- return;
582
- }
583
- if (key.escape && !pickerMode) {
584
- if (isRunning && activeAbortRef.current) {
585
- activeAbortRef.current.abort(new AgentAbortError("Agent run cancelled by user."));
586
- return;
587
- }
588
- }
589
- });
590
- const updateDisplayMessages = useCallback((updater) => {
591
- setMessages((prev) => updater(prev).map(withMessageKey));
592
- }, []);
593
- // Non-append transcript rebuilds (/clear, /compact, /rewind, session switch)
594
- // replace the settled list rather than extending it. The rows already
595
- // committed to the terminal's native scrollback (via <Static>) cannot be
596
- // un-printed, so we wipe the screen + scrollback and bump the Static key:
597
- // Ink then re-prints the rebuilt list fresh instead of appending duplicates.
598
- const resetTranscript = useCallback((updater) => {
599
- reprintTranscript();
600
- updateDisplayMessages(updater);
601
- }, [reprintTranscript, updateDisplayMessages]);
602
- const addMessage = useCallback((role, content, syntheticKind) => {
603
- updateDisplayMessages((prev) => [...prev, withMessageKey(syntheticKind ? { role, content, syntheticKind } : { role, content })]);
604
- }, [updateDisplayMessages]);
605
- // Reflow on terminal resize. ink 7.0.3 only clears its dynamic frame when the
606
- // terminal NARROWS (see its resized() handler); on widen / tmux split the
607
- // stale frame is left behind and the working trace duplicates into
608
- // scrollback. Dedicated scrollback renderers (pi-tui) handle this by doing a
609
- // full clear + re-print on ANY width/height change so content rewraps
610
- // cleanly — resetTranscript does exactly that here. Debounced so a drag
611
- // coalesces into one reflow instead of flashing on every resize event.
612
- const didMountSizeRef = useRef(false);
613
- useEffect(() => {
614
- if (!didMountSizeRef.current) {
615
- didMountSizeRef.current = true;
616
- return;
617
- }
618
- const timer = setTimeout(() => {
619
- resetTranscript((prev) => prev);
620
- }, 80);
621
- return () => clearTimeout(timer);
622
- }, [terminalColumns, terminalRows, resetTranscript]);
623
- useEffect(() => {
624
- if (!updateNoticeRefresh)
625
- return;
626
- let cancelled = false;
627
- updateNoticeRefresh.then((notice) => {
628
- if (cancelled || !notice)
629
- return;
630
- setCurrentUpdateNotice(notice);
631
- if (!showWelcomeRef.current)
632
- addMessage("assistant", notice);
633
- }).catch(() => {
634
- // Best-effort update checks should never disturb the session.
635
- });
636
- return () => {
637
- cancelled = true;
638
- };
639
- }, [addMessage, updateNoticeRefresh]);
640
- const clearMessages = useCallback(() => {
641
- // Settled rows live in the terminal's native scrollback now (committed via
642
- // <Static>), so clearing React state is not enough — resetTranscript wipes
643
- // the screen + scrollback and re-prints the (now empty) transcript.
644
- resetTranscript(() => []);
645
- clearLiveSubagentTools();
646
- }, [resetTranscript, agent, clearLiveSubagentTools]);
647
- // Render a placeholder user row for input waiting to enter the run.
648
- const addStatusUserMessage = useCallback((content, status) => {
649
- const key = nextDisplayMessageKey("user");
650
- updateDisplayMessages((prev) => [...prev, { key, role: "user", content, inputStatus: status }]);
651
- return key;
652
- }, [updateDisplayMessages]);
653
- const prepareSubmitDisplay = useCallback((payload) => {
654
- if (payload.images.length === 0)
655
- return payload;
656
- if (payload.imageDisplayStart !== undefined) {
657
- nextImageDisplayLabelStartRef.current = Math.max(nextImageDisplayLabelStartRef.current, payload.imageDisplayStart + payload.images.length);
658
- return payload;
659
- }
660
- const imageDisplayStart = nextImageDisplayLabelStartRef.current;
661
- nextImageDisplayLabelStartRef.current += payload.images.length;
662
- return { ...payload, imageDisplayStart };
663
- }, []);
664
- const submitDisplayText = useCallback((payload) => (formatImageUserDisplayText(payload.displayText ?? payload.text, payload.images.length, payload.imageDisplayStart)), []);
665
- const currentSessionFile = useCallback(() => sessionManagerRef.current?.getSessionFile(), []);
666
- const queueInput = useCallback((payload) => {
667
- const preparedPayload = prepareSubmitDisplay(payload);
668
- const displayKey = addStatusUserMessage(submitDisplayText(preparedPayload), "queued");
669
- queuedInputsRef.current.push({ payload: preparedPayload, displayKey, sessionFile: currentSessionFile() });
670
- setQueuedCount(queuedInputsRef.current.length);
671
- }, [addStatusUserMessage, currentSessionFile, prepareSubmitDisplay, submitDisplayText]);
672
- const submitSteer = useCallback((payload) => {
673
- const controller = inputControllerRef.current;
674
- if (!controller) {
675
- queueInput(payload);
676
- return;
677
- }
678
- const preparedPayload = prepareSubmitDisplay(payload);
679
- const displayKey = addStatusUserMessage(submitDisplayText(preparedPayload), "pending_steer");
680
- const pending = controller.enqueue(preparedPayload.text);
681
- pendingSteersRef.current.set(pending.id, { displayKey, sessionFile: currentSessionFile() });
682
- setPendingSteerCount(pendingSteersRef.current.size);
683
- }, [addStatusUserMessage, currentSessionFile, prepareSubmitDisplay, queueInput, submitDisplayText]);
684
- const openPicker = useCallback((mode, providerId) => {
685
- if (mode === "model" && grokSessionBound && externalRuntime) {
686
- setStatsPanel(null);
687
- setGrokModels([]);
688
- setPickerMode("grok-model");
689
- const bound = externalRuntimeBindingRef.current;
690
- void (bound?.sessionId
691
- ? externalRuntime.hydrateSession(bound.sessionId, bound.modelId, bound.reasoningEffort)
692
- : Promise.resolve()).then(async () => await externalRuntime.listModels()).then(setGrokModels).catch((error) => {
693
- addMessage("error", `Failed to load Grok models: ${errorMessage(error)}`);
694
- setPickerMode(null);
695
- });
696
- return;
697
- }
698
- if (mode === "key") {
699
- setKeyProviderId(providerId ?? null);
700
- }
701
- if (mode === "theme") {
702
- themePickerRevertRef.current = themeModeRef.current;
703
- }
704
- setStatsPanel(null);
705
- setPickerMode(mode);
706
- }, [addMessage, externalRuntime, grokSessionBound]);
707
- const closePicker = useCallback(() => {
708
- setPickerMode(null);
709
- setCursorResetEpoch((epoch) => epoch + 1);
710
- }, []);
711
- const handleGrokModelSelect = useCallback((encoded) => {
712
- if (!externalRuntime)
713
- return;
714
- const run = async () => {
715
- const parsed = JSON.parse(encoded);
716
- const boundSessionId = externalRuntimeBindingRef.current?.sessionId;
717
- if (boundSessionId)
718
- await externalRuntime.hydrateSession(boundSessionId);
719
- const selection = await externalRuntime.setModel(parsed.modelId, parsed.reasoningEffort);
720
- const manager = sessionManagerRef.current;
721
- const binding = manager?.getMetadata().externalRuntime;
722
- if (manager && binding && isGrokSubscriptionProviderId(binding.id)) {
723
- manager.updateMetadata({ externalRuntime: { ...binding, ...selection } });
724
- manager.appendMarker("model_switch", selection.modelId ?? parsed.modelId);
725
- manager.appendMarker("thinking_level_switch", selection.reasoningEffort);
726
- refreshExternalRuntimeBinding(manager);
727
- }
728
- addMessage("assistant", `Grok model switched to ${selection.modelId ?? parsed.modelId}${selection.reasoningEffort !== "off" ? ` (${selection.reasoningEffort})` : ""}.`, "ui_notice");
729
- closePicker();
730
- };
731
- void run().catch((error) => {
732
- addMessage("error", `Failed to switch Grok model: ${errorMessage(error)}`);
733
- closePicker();
734
- });
735
- }, [addMessage, closePicker, externalRuntime, refreshExternalRuntimeBinding]);
736
- const fillComposer = useCallback((text) => {
737
- setComposerDraft((current) => ({
738
- text,
739
- epoch: (current?.epoch ?? 0) + 1,
740
- }));
741
- }, []);
742
- const clearComposerDraft = useCallback(() => {
743
- setComposerDraft(null);
744
- }, []);
745
- const setStartingSubmit = useCallback((fingerprint) => {
746
- startingSubmitFingerprintRef.current = fingerprint;
747
- setStartingSubmitFingerprint(fingerprint);
748
- }, []);
749
- const openFeedback = useCallback((initialDescription) => {
750
- const base = collectFeedback(agent, { description: "" });
751
- const { description: _drop, ...rest } = base;
752
- setPendingFeedback({ base: rest, initialDescription });
753
- }, [agent]);
754
- const openSessionPicker = useCallback(() => {
755
- if (activeAbortRef.current) {
756
- addMessage("error", "Stop the current run before switching sessions.");
757
- return;
758
- }
759
- setStatsPanel(null);
760
- setPickerMode("session");
761
- }, [addMessage]);
762
- const openRewindPicker = useCallback(() => {
763
- if (!sessionManager) {
764
- addMessage("error", "Rewind requires an active session.");
765
- return;
766
- }
767
- if (activeAbortRef.current) {
768
- addMessage("error", "Stop the current run before rewinding.");
769
- return;
770
- }
771
- setStatsPanel(null);
772
- setPickerMode("rewind");
773
- }, [addMessage, sessionManager]);
774
- const openStatsPanel = useCallback(() => {
775
- setPickerMode(null);
776
- setStatsPanel({
777
- range: "30d",
778
- bundle: collectUsageStatsBundle(),
779
- });
780
- }, []);
781
- const closeStatsPanel = useCallback(() => {
782
- setStatsPanel(null);
783
- setCursorResetEpoch((epoch) => epoch + 1);
784
- }, []);
785
- const applySessionSwitch = useCallback((result, notice) => {
786
- externalRuntimeGenerationRef.current += 1;
787
- const queuedDisplayKeys = queuedAndPendingDisplayKeys(queuedInputsRef.current, pendingSteersRef.current.values());
788
- queuedInputsRef.current = [];
789
- pendingSteersRef.current.clear();
790
- inputControllerRef.current = null;
791
- setQueuedCount(0);
792
- setPendingSteerCount(0);
793
- setStartingSubmit(null);
794
- clearComposerDraft();
795
- sessionManagerRef.current = result.manager;
796
- setSessionManager(result.manager);
797
- // Keep ownerSessionId on newly spawned tasks/servers accurate (§2.2c).
798
- agent.setSessionID(result.manager.getSessionFile());
799
- const nextExternalBinding = result.manager.getMetadata().externalRuntime;
800
- externalRuntimeBindingRef.current = nextExternalBinding;
801
- setExternalRuntimeBinding(nextExternalBinding);
802
- clearLiveSubagentTools();
803
- resetTranscript(() => [
804
- ...reconstructDisplayMessages(agent.messages).filter((message) => !queuedDisplayKeys.has(message.key ?? "")),
805
- ...(notice ? [withMessageKey({ role: "assistant", content: notice })] : []),
806
- ]);
807
- closePicker();
808
- return result.manager;
809
- }, [agent, clearComposerDraft, clearLiveSubagentTools, closePicker, resetTranscript, setStartingSubmit]);
810
- const prepareFreshSession = useCallback((metadata) => {
811
- const fresh = SessionManager.createFresh(args.cwd);
812
- try {
813
- if (metadata && Object.keys(metadata).length > 0) {
814
- fresh.updateMetadata(metadata);
815
- }
816
- return fresh;
817
- }
818
- catch (error) {
819
- rmSync(fresh.getSessionFile(), { force: true });
820
- throw error;
821
- }
822
- }, [args.cwd]);
823
- const commitFreshSession = useCallback((fresh) => {
824
- if (!switchSession) {
825
- rmSync(fresh.getSessionFile(), { force: true });
826
- throw new Error("Starting a fresh session is not available in this mode.");
827
- }
828
- const result = switchSession(fresh.getSessionFile());
829
- if ("error" in result) {
830
- rmSync(fresh.getSessionFile(), { force: true });
831
- throw new Error(result.error);
832
- }
833
- return applySessionSwitch(result);
834
- }, [applySessionSwitch, switchSession]);
835
- const startFreshSession = useCallback(async (metadata) => {
836
- return commitFreshSession(prepareFreshSession(metadata));
837
- }, [commitFreshSession, prepareFreshSession]);
838
- const transitionToNative = useCallback(async () => {
839
- if (!externalSessionBound)
840
- return sessionManagerRef.current;
841
- const candidate = prepareFreshSession();
842
- externalRuntimeGenerationRef.current += 1;
843
- const boundSessionId = externalRuntimeBinding?.sessionId;
844
- try {
845
- // Validate/prepare the native provider before calling this function.
846
- // The external sidecar must be fully stopped before the new Bubble
847
- // session is committed. If commit then fails, the old binding remains
848
- // authoritative and the reusable manager will lazy-load it next time.
849
- await stopExternalRuntimeForSessionSwitch(externalRuntime, boundSessionId);
850
- return commitFreshSession(candidate);
851
- }
852
- catch (error) {
853
- rmSync(candidate.getSessionFile(), { force: true });
854
- throw error;
855
- }
856
- }, [commitFreshSession, externalRuntime, externalRuntimeBinding?.sessionId, externalSessionBound, prepareFreshSession]);
857
- const handleSessionSelect = useCallback((sessionFile) => {
858
- const run = async () => {
859
- if (!switchSession) {
860
- addMessage("error", "Session switching is not available in this mode.");
861
- closePicker();
862
- return;
863
- }
864
- if (activeAbortRef.current) {
865
- addMessage("error", "Stop the current run before switching sessions.");
866
- closePicker();
867
- return;
868
- }
869
- if (externalSessionBound) {
870
- externalRuntimeGenerationRef.current += 1;
871
- const boundSessionId = externalRuntimeBinding?.sessionId;
872
- await stopExternalRuntimeForSessionSwitch(externalRuntime, boundSessionId);
873
- }
874
- const result = switchSession(sessionFile);
875
- if ("error" in result) {
876
- addMessage("error", `Failed to switch session: ${result.error}`);
877
- closePicker();
878
- return;
879
- }
880
- applySessionSwitch(result, `⤷ Resumed session: ${sessionDisplayName(result.manager)}`);
881
- };
882
- void run().catch((error) => {
883
- addMessage("error", `Failed to switch session: ${errorMessage(error)}`);
884
- closePicker();
885
- });
886
- }, [activeAbortRef, addMessage, applySessionSwitch, closePicker, externalRuntime, externalRuntimeBinding?.sessionId, externalSessionBound, switchSession]);
887
- const handleModelSelect = useCallback((model, selectedThinkingLevel) => {
888
- const run = async () => {
889
- const target = modelSwitchTarget(model, agent.providerId || safeRegistry.getDefault()?.id);
890
- await safeRegistry.prepareProvider(target.providerId);
891
- const targetProvider = safeRegistry.getConfigured().find((item) => item.id === target.providerId);
892
- if (!targetProvider?.apiKey || !createProvider) {
893
- throw new Error(`Provider ${target.providerId} is not configured or has no active credentials.`);
894
- }
895
- // Construct once as a side-effect-free preflight before stopping Grok.
896
- // switchAgentModel constructs the committed instance after the fresh
897
- // native session has been installed.
898
- const preparedProvider = createProvider(target.providerId, targetProvider.apiKey, targetProvider.baseURL);
899
- const targetSessionManager = await transitionToNative();
900
- const nextThinkingLevel = await switchAgentModel({
901
- model,
902
- agent,
903
- registry: safeRegistry,
904
- createProvider,
905
- preparedProvider,
906
- workingDir: args.cwd,
907
- systemPromptOptions: agent.getSystemPromptToolOptions(),
908
- thinkingLevel: selectedThinkingLevel,
909
- rememberModel: (nextModel) => userConfig.pushRecentModel(nextModel),
910
- setThinkingLevel,
911
- sessionManager: targetSessionManager,
912
- });
913
- // Persist the resolved effort alongside the model so a fresh session
914
- // restores both. Without this, the picker path only remembered the model
915
- // and new sessions fell back to a stale config.json defaultThinkingLevel
916
- // (e.g. picking glm-5.3 + max still restored "high" on next launch).
917
- userConfig.setDefaultThinkingLevel(nextThinkingLevel);
918
- // Binary thinking toggles and thinking-only models use internal level
919
- // placeholders; do not present those placeholders as real effort grades.
920
- const decodedModel = decodeModel(model);
921
- const switchedProviderId = decodedModel.providerId || agent.providerId || safeRegistry.getDefault()?.id || "openai";
922
- const isThinkingToggle = isThinkingToggleModel(switchedProviderId, decodedModel.modelId);
923
- const isThinkingOnly = isThinkingOnlyLevels(getAvailableThinkingLevels(switchedProviderId, decodedModel.modelId));
924
- const effortNote = nextThinkingLevel && nextThinkingLevel !== "off"
925
- ? (isThinkingToggle || isThinkingOnly ? " in thinking mode" : ` with ${nextThinkingLevel} effort`)
926
- : "";
927
- addMessage("assistant", `Model switched to ${displayModel(model)}${effortNote}.`, "ui_notice");
928
- closePicker();
929
- return nextThinkingLevel;
930
- };
931
- void run().catch((error) => {
932
- addMessage("error", formatModelSwitchError(model, error));
933
- closePicker();
934
- });
935
- }, [agent, addMessage, closePicker, sessionManager, transitionToNative, userConfig, safeRegistry, createProvider]);
936
- const handleThemeHighlight = useCallback((mode) => {
937
- setThemeMode(mode);
938
- }, []);
939
- const handleThemeSelect = useCallback((mode) => {
940
- applyThemeMode(mode);
941
- const resolvedNote = mode === "auto" ? ` (resolved to ${autoResolved})` : "";
942
- addMessage("assistant", `Theme set to ${mode}${resolvedNote}.`);
943
- closePicker();
944
- }, [addMessage, applyThemeMode, autoResolved, closePicker]);
945
- const handleThemeCancel = useCallback(() => {
946
- setThemeMode(themePickerRevertRef.current);
947
- closePicker();
948
- }, [closePicker]);
949
- const executeProviderCommand = useCallback(async (command) => {
950
- closePicker();
951
- const { handled, result } = await slashRegistry.execute(command, {
952
- agent,
953
- addMessage,
954
- clearMessages,
955
- cwd: args.cwd,
956
- exit: requestExit,
957
- sessionManager: sessionManagerRef.current,
958
- createProvider: createProvider ?? (() => {
959
- throw new Error("Provider creation not available");
960
- }),
961
- openPicker,
962
- openSessionPicker,
963
- openRewindPicker,
964
- openFeedback,
965
- fillComposer,
966
- registry: safeRegistry,
967
- skillRegistry: safeSkillRegistry,
968
- bashAllowlist,
969
- settingsManager,
970
- lspService,
971
- mcpManager,
972
- hookController,
973
- flushMemory,
974
- runMemoryCompaction,
975
- runMemorySummary,
976
- runMemoryRefresh,
977
- getThemeMode: () => themeMode,
978
- getResolvedTheme: () => themeResolved,
979
- setThemeMode: applyThemeMode,
980
- openStats: openStatsPanel,
981
- externalRuntime,
982
- startFreshSession,
983
- transitionToNative,
984
- onExternalRuntimeChange: refreshExternalRuntimeBinding,
985
- });
986
- if (handled) {
987
- if (result)
988
- addMessage("assistant", result, slashResultNoticeKind(result));
989
- }
990
- }, [
991
- addMessage,
992
- agent,
993
- applyThemeMode,
994
- args.cwd,
995
- bashAllowlist,
996
- clearMessages,
997
- closePicker,
998
- createProvider,
999
- externalRuntime,
1000
- fillComposer,
1001
- flushMemory,
1002
- hookController,
1003
- lspService,
1004
- mcpManager,
1005
- openFeedback,
1006
- openPicker,
1007
- openRewindPicker,
1008
- openSessionPicker,
1009
- openStatsPanel,
1010
- refreshExternalRuntimeBinding,
1011
- requestExit,
1012
- runMemoryCompaction,
1013
- runMemoryRefresh,
1014
- runMemorySummary,
1015
- safeRegistry,
1016
- safeSkillRegistry,
1017
- settingsManager,
1018
- startFreshSession,
1019
- themeMode,
1020
- themeResolved,
1021
- transitionToNative,
1022
- ]);
1023
- const handleProviderSelect = useCallback((providerId) => {
1024
- const run = async () => {
1025
- if (isGrokSubscriptionProviderId(providerId)) {
1026
- if (grokSessionBound) {
1027
- closePicker();
1028
- return;
1029
- }
1030
- await executeProviderCommand(`/provider --set ${GROK_SUBSCRIPTION_PROVIDER_ID}`);
1031
- return;
1032
- }
1033
- await safeRegistry.prepareProvider(providerId);
1034
- const configured = safeRegistry.getConfigured();
1035
- const p = configured.find((x) => x.id === providerId);
1036
- const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
1037
- if (!p && !builtin) {
1038
- addMessage("error", `Provider ${providerId} not found.`);
1039
- closePicker();
1040
- return;
1041
- }
1042
- if (!p?.apiKey) {
1043
- if (!p && builtin) {
1044
- safeRegistry.addProvider(providerId, "");
1045
- }
1046
- setKeyProviderId(providerId);
1047
- setPickerMode("key");
1048
- return;
1049
- }
1050
- if (!createProvider)
1051
- throw new Error("Provider creation not available");
1052
- // Construct the target before stopping a Grok sidecar. A bad provider
1053
- // configuration must leave the current external session untouched.
1054
- const nextProvider = createProvider(providerId, p.apiKey, p.baseURL);
1055
- await transitionToNative();
1056
- agent.setProvider(nextProvider);
1057
- agent.providerId = providerId;
1058
- try {
1059
- safeRegistry.setDefault(providerId);
1060
- }
1061
- catch (error) {
1062
- addMessage("error", `Switched providers, but could not remember the default: ${errorMessage(error)}`);
1063
- }
1064
- addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
1065
- closePicker();
1066
- };
1067
- void run().catch((error) => {
1068
- addMessage("error", `Failed to switch provider ${providerId}: ${errorMessage(error)}`);
1069
- closePicker();
1070
- });
1071
- }, [addMessage, agent, closePicker, createProvider, executeProviderCommand, grokSessionBound, safeRegistry, transitionToNative]);
1072
- const handleProviderAddSelect = useCallback((providerId) => {
1073
- const run = async () => {
1074
- if (isGrokSubscriptionProviderId(providerId)) {
1075
- await executeProviderCommand(`/provider --add ${GROK_SUBSCRIPTION_PROVIDER_ID}`);
1076
- return;
1077
- }
1078
- const ok = safeRegistry.addProvider(providerId, "");
1079
- if (!ok) {
1080
- addMessage("error", `Provider ${providerId} could not be added.`);
1081
- closePicker();
1082
- return;
1083
- }
1084
- setKeyProviderId(providerId);
1085
- setPickerMode("key");
1086
- };
1087
- void run().catch((error) => {
1088
- addMessage("error", `Failed to add provider ${providerId}: ${errorMessage(error)}`);
1089
- closePicker();
1090
- });
1091
- }, [addMessage, closePicker, executeProviderCommand, safeRegistry]);
1092
- const handleLoginProviderSelect = useCallback((providerId) => {
1093
- void executeProviderCommand(`/login ${providerId}`).catch((error) => {
1094
- addMessage("error", `Failed to log in to ${providerId}: ${errorMessage(error)}`);
1095
- });
1096
- }, [addMessage, executeProviderCommand]);
1097
- const handleLogoutProviderSelect = useCallback((providerId) => {
1098
- void executeProviderCommand(`/logout ${providerId}`).catch((error) => {
1099
- addMessage("error", `Failed to log out of ${providerId}: ${errorMessage(error)}`);
1100
- });
1101
- }, [addMessage, executeProviderCommand]);
1102
- const handleKeySubmit = useCallback((key) => {
1103
- const run = async () => {
1104
- const targetId = keyProviderId || safeRegistry.getDefault()?.id;
1105
- if (!targetId) {
1106
- addMessage("error", "No provider selected.");
1107
- closePicker();
1108
- setKeyProviderId(null);
1109
- return;
1110
- }
1111
- safeRegistry.updateProviderKey(targetId, key);
1112
- const p = safeRegistry.getConfigured().find((x) => x.id === targetId);
1113
- if (p && createProvider) {
1114
- const nextProvider = createProvider(targetId, key, p.baseURL);
1115
- await transitionToNative();
1116
- agent.setProvider(nextProvider);
1117
- agent.providerId = targetId;
1118
- try {
1119
- safeRegistry.setDefault(targetId);
1120
- }
1121
- catch (error) {
1122
- addMessage("error", `API key saved, but could not remember the default: ${errorMessage(error)}`);
1123
- }
1124
- }
1125
- addMessage("assistant", `API key updated for ${p?.name || targetId} to ${maskKey(key)}.`);
1126
- closePicker();
1127
- setKeyProviderId(null);
1128
- };
1129
- void run().catch((error) => {
1130
- addMessage("error", `Failed to update provider key: ${errorMessage(error)}`);
1131
- closePicker();
1132
- });
1133
- }, [addMessage, agent, closePicker, createProvider, keyProviderId, safeRegistry, transitionToNative]);
1134
- // /loop handler lives below handleSubmit (it needs addMessage only); the
1135
- // ref breaks the declaration cycle so handleSubmit can route to it.
1136
- const handleLoopCommandRef = useRef(null);
1137
- const handleSubmit = useCallback(async (payload) => {
1138
- const initialPayload = typeof payload === "string" ? { text: payload, images: [] } : payload;
1139
- const input = initialPayload.text;
1140
- const displayInput = initialPayload.displayText ?? input;
1141
- const images = initialPayload.images;
1142
- if (!input.trim() && images.length === 0)
1143
- return;
1144
- // Agent already running: route the submit into the live run instead of
1145
- // starting a new one. Plain prose steers the current turn; slash
1146
- // commands, @-mentions and image payloads queue for the next turn.
1147
- if (activeAbortRef.current) {
1148
- if (/^\/(?:quit|exit)\s*$/.test(input.trim())) {
1149
- requestExit();
1150
- return;
1151
- }
1152
- if (externalSessionBound) {
1153
- // External ACP turns do not support Bubble's boundary steer
1154
- // protocol. Preserve ordering by sending every additional input to
1155
- // the next turn instead.
1156
- queueInput(initialPayload);
1157
- return;
1158
- }
1159
- const steerEligible = !displayInput.trim().startsWith("/") &&
1160
- !input.includes("@") &&
1161
- images.length === 0;
1162
- if (steerEligible) {
1163
- submitSteer(initialPayload);
1164
- }
1165
- else {
1166
- queueInput(initialPayload);
1167
- }
1168
- return;
1169
- }
1170
- const grokInputDecision = externalSessionBound
1171
- ? classifyGrokInput({ text: input, imageCount: images.length })
1172
- : undefined;
1173
- if (grokInputDecision?.kind === "blocked") {
1174
- addMessage("error", grokInputDecision.message);
1175
- return;
1176
- }
1177
- if (grokInputDecision?.kind === "prompt" && unsupportedExternalSessionBound) {
1178
- addMessage("error", "This session has an unsupported external runtime binding. Native tools and providers are disabled; use /session, /provider, or /model to enter a fresh native session.");
1179
- return;
1180
- }
1181
- if (grokInputDecision?.kind === "prompt" && grokSessionBound && !externalRuntime) {
1182
- addMessage("error", "This session belongs to Grok Subscription, but its runtime is unavailable in this TUI. Use /model, /provider, or /session to enter a fresh native session.");
1183
- return;
1184
- }
1185
- const submitFingerprint = submitPayloadFingerprint(initialPayload);
1186
- const startingDecision = decideStartingSubmitFingerprint(startingSubmitFingerprintRef.current, submitFingerprint);
1187
- if (startingDecision === "ignore")
1188
- return;
1189
- if (startingDecision === "queue") {
1190
- queueInput(initialPayload);
1191
- return;
1192
- }
1193
- const normalized = prepareSubmitDisplay(initialPayload);
1194
- setStartingSubmit(submitFingerprint);
1195
- try {
1196
- const runAgentInput = async (actualInput, displayInput, attachedImages = [], runOptions = {}) => {
1197
- const runSessionFile = currentSessionFile();
1198
- const runSessionManager = sessionManagerRef.current;
1199
- const runExternalRuntime = isGrokSubscriptionProviderId(externalRuntimeBindingRef.current?.id);
1200
- const externalSessionId = runExternalRuntime
1201
- ? externalRuntimeBindingRef.current?.sessionId
1202
- : undefined;
1203
- const externalModelId = runExternalRuntime
1204
- ? externalRuntimeBindingRef.current?.modelId ?? "subscription-default"
1205
- : undefined;
1206
- const externalGeneration = runExternalRuntime
1207
- ? ++externalRuntimeGenerationRef.current
1208
- : undefined;
1209
- const isCurrentExternalRun = () => !runExternalRuntime || (externalRuntimeGenerationRef.current === externalGeneration
1210
- && currentSessionFile() === runSessionFile
1211
- && isGrokSubscriptionProviderId(externalRuntimeBindingRef.current?.id)
1212
- && externalRuntimeBindingRef.current?.sessionId === externalSessionId);
1213
- if (runExternalRuntime) {
1214
- if (!externalRuntime || typeof actualInput !== "string" || !externalSessionId || !runSessionManager) {
1215
- addMessage("error", "Grok subscription session binding is unavailable. Start a fresh session with /login grok.");
1216
- return;
1217
- }
1218
- const boundModelId = externalRuntimeBindingRef.current?.modelId;
1219
- if (boundModelId) {
1220
- await externalRuntime.hydrateSession(externalSessionId, boundModelId, externalRuntimeBindingRef.current?.reasoningEffort);
1221
- }
1222
- }
1223
- else {
1224
- const activeProviderId = agent.providerId || safeRegistry.getDefault()?.id;
1225
- const hasActiveProvider = !!activeProviderId && safeRegistry.getEnabled().some((provider) => provider.id === activeProviderId);
1226
- if (!hasActiveProvider) {
1227
- addMessage("error", "No provider configured. Use /login for ChatGPT or /provider --add <id> before sending a prompt.");
1228
- if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
1229
- goalStore.pause();
1230
- addMessage("assistant", stopReasonNotice("error"));
1231
- }
1232
- return;
1233
- }
1234
- if (!agent.model) {
1235
- addMessage("error", "No model selected. Use /model after /login or provider setup.");
1236
- if (runOptions.goalRun && goalStore?.snapshot()?.status === "active") {
1237
- goalStore.pause();
1238
- addMessage("assistant", stopReasonNotice("error"));
1239
- }
1240
- return;
1241
- }
1242
- }
1243
- const displayContent = formatImageUserDisplayText(displayInput, attachedImages.length, runOptions.imageDisplayStart);
1244
- if (!runOptions.hidden) {
1245
- updateDisplayMessages((prev) => [
1246
- ...prev,
1247
- withMessageKey({ role: "user", content: displayContent }),
1248
- ]);
1249
- // The new user row commits to native scrollback; the terminal keeps
1250
- // the prompt in view, so there is no app-side "snap to bottom" to do.
1251
- }
1252
- if (runExternalRuntime) {
1253
- runSessionManager.appendMessage({ role: "user", content: actualInput });
1254
- }
1255
- setIsRunning(true);
1256
- runStartRef.current = Date.now();
1257
- setStreamingContent("");
1258
- setStreamingReasoning("");
1259
- setStreamingTools([]);
1260
- setStreamingParts([]);
1261
- let assistantContent = "";
1262
- let assistantReasoning = "";
1263
- let goalRunTokens = 0;
1264
- let goalRunUsageReported = false;
1265
- let runCancelled = false;
1266
- let runErrored = false;
1267
- const toolCalls = [];
1268
- const assistantParts = [];
1269
- let assistantSystemFingerprint;
1270
- // Entries whose members all reached a final status are now covered by
1271
- // the settled transcript; drop them so the accumulator stays bounded
1272
- // across a long session. Running entries survive — for a workflow
1273
- // spanning turns they are the only live view of its members.
1274
- if (pruneSettledLiveSubagentTools(liveSubagentToolsRef.current)) {
1275
- setLiveSubagentVersion((version) => version + 1);
1276
- }
1277
- const abortController = new AbortController();
1278
- activeAbortRef.current = abortController;
1279
- setStartingSubmit(null);
1280
- const inputController = new AgentRunInputQueue(`run-${++nextRunIdRef.current}`);
1281
- inputControllerRef.current = inputController;
1282
- const syncStreamingParts = () => {
1283
- setStreamingParts(snapshotDisplayParts(assistantParts));
1284
- };
1285
- // Text/reasoning deltas arrive far faster than the screen needs to
1286
- // update; batch them so the full-frame re-render runs at most every
1287
- // STREAMING_FLUSH_INTERVAL_MS. Tool events stay immediate.
1288
- let streamingFlushTimer = null;
1289
- const cancelStreamingFlush = () => {
1290
- if (streamingFlushTimer !== null) {
1291
- clearTimeout(streamingFlushTimer);
1292
- streamingFlushTimer = null;
1293
- }
1294
- };
1295
- const scheduleStreamingFlush = () => {
1296
- if (streamingFlushTimer !== null)
1297
- return;
1298
- streamingFlushTimer = setTimeout(() => {
1299
- streamingFlushTimer = null;
1300
- setStreamingContent(assistantContent);
1301
- setStreamingReasoning(assistantReasoning);
1302
- syncStreamingParts();
1303
- }, STREAMING_FLUSH_INTERVAL_MS);
1304
- };
1305
- const hasAssistantOutput = () => (!!assistantContent ||
1306
- !!assistantReasoning ||
1307
- toolCalls.length > 0 ||
1308
- assistantParts.length > 0);
1309
- const commitAssistantMessage = (taskElapsedMs) => {
1310
- if (!hasAssistantOutput() || !isCurrentExternalRun())
1311
- return;
1312
- const currentParts = snapshotDisplayParts(assistantParts);
1313
- const currentToolCalls = [...toolCalls];
1314
- const partContent = assistantContent || contentFromParts(currentParts);
1315
- const partToolCalls = currentToolCalls.length > 0
1316
- ? currentToolCalls
1317
- : toolCallsFromParts(currentParts);
1318
- const msg = {
1319
- key: nextDisplayMessageKey("asst"),
1320
- role: "assistant",
1321
- content: partContent,
1322
- };
1323
- if (assistantReasoning) {
1324
- msg.reasoning = assistantReasoning;
1325
- }
1326
- if (partToolCalls.length > 0) {
1327
- msg.toolCalls = partToolCalls;
1328
- }
1329
- if (currentParts.length > 0) {
1330
- msg.parts = currentParts;
1331
- }
1332
- if (taskElapsedMs !== undefined && Number.isFinite(taskElapsedMs) && taskElapsedMs > 0) {
1333
- msg.taskElapsedMs = taskElapsedMs;
1334
- }
1335
- if (assistantSystemFingerprint) {
1336
- msg.systemFingerprint = assistantSystemFingerprint;
1337
- }
1338
- updateDisplayMessages((prev) => [...prev, msg]);
1339
- if (runExternalRuntime) {
1340
- runSessionManager.appendMessage({
1341
- role: "assistant",
1342
- content: partContent,
1343
- ...(assistantReasoning ? { reasoning: assistantReasoning } : {}),
1344
- providerId: "grok",
1345
- model: "grok-subscription",
1346
- modelId: externalModelId,
1347
- });
1348
- }
1349
- };
1350
- const clearAssistantStream = () => {
1351
- // A timer firing after this reset would resurrect the just-committed
1352
- // text as a phantom streaming block — cancel before clearing.
1353
- cancelStreamingFlush();
1354
- setStreamingContent("");
1355
- setStreamingReasoning("");
1356
- setStreamingTools([]);
1357
- setStreamingParts([]);
1358
- assistantContent = "";
1359
- assistantReasoning = "";
1360
- assistantSystemFingerprint = undefined;
1361
- toolCalls.length = 0;
1362
- assistantParts.length = 0;
1363
- };
1364
- try {
1365
- const eventStream = runExternalRuntime
1366
- ? externalRuntime.run(actualInput, {
1367
- sessionId: externalSessionId,
1368
- signal: abortController.signal,
1369
- generation: externalGeneration,
1370
- })
1371
- : agent.run(actualInput, args.cwd, {
1372
- abortSignal: abortController.signal,
1373
- inputController,
1374
- });
1375
- for await (const event of eventStream) {
1376
- if (runExternalRuntime) {
1377
- if (!isCurrentExternalRun()) {
1378
- abortController.abort(new AgentAbortError("Stale Grok runtime generation."));
1379
- break;
1380
- }
1381
- if (event.type !== "turn_start"
1382
- && event.type !== "text_delta"
1383
- && event.type !== "reasoning_delta"
1384
- && event.type !== "tool_start"
1385
- && event.type !== "tool_update"
1386
- && event.type !== "tool_end"
1387
- && event.type !== "turn_end") {
1388
- await externalRuntime.cancel(externalSessionId).catch(() => undefined);
1389
- throw new GrokRuntimeError("policy_violation", "Grok emitted an unsupported external runtime event.");
1390
- }
1391
- }
1392
- switch (event.type) {
1393
- case "turn_start":
1394
- // A fresh provider call is starting. Everything worth keeping
1395
- // was committed at the preceding turn_end, so leftovers here
1396
- // can only be a half-built attempt the agent discarded (its
1397
- // stream-interruption retry re-issues the whole request and
1398
- // never appends the partial message — see agent.ts). Drop the
1399
- // stale buffer, or the retry re-streams the same opening text
1400
- // on top of it and the answer duplicates on screen.
1401
- clearAssistantStream();
1402
- break;
1403
- case "text_delta":
1404
- assistantContent += event.content;
1405
- appendTextPart(assistantParts, event.content);
1406
- scheduleStreamingFlush();
1407
- break;
1408
- case "reasoning_delta":
1409
- assistantReasoning += event.content;
1410
- scheduleStreamingFlush();
1411
- break;
1412
- case "tool_call_start": {
1413
- // The LLM has begun emitting this tool call. Args are still
1414
- // streaming — render an empty-args placeholder so the user
1415
- // sees the tool the moment it appears in the assistant
1416
- // response, not after the full arg payload finishes.
1417
- if (!toolCalls.some((t) => t.id === event.id)) {
1418
- const toolCall = {
1419
- id: event.id,
1420
- name: event.name,
1421
- args: {},
1422
- startedAt: Date.now(),
1423
- };
1424
- toolCalls.push(toolCall);
1425
- appendToolPart(assistantParts, toolCall);
1426
- setStreamingTools([...toolCalls]);
1427
- syncStreamingParts();
1428
- }
1429
- break;
1430
- }
1431
- case "tool_call_delta": {
1432
- // Best-effort parse of the partial argument JSON to extract
1433
- // identifying fields (path, command, content, …). The buffer
1434
- // is incomplete JSON during streaming, so fall back to regex
1435
- // peeks on common string fields.
1436
- const tc = toolCalls.find((t) => t.id === event.id);
1437
- if (tc) {
1438
- tc.args = parsePartialArgs(event.arguments, tc.args);
1439
- setStreamingTools([...toolCalls]);
1440
- syncStreamingParts();
1441
- }
1442
- break;
1443
- }
1444
- case "tool_call_end": {
1445
- // Provider signaled args streaming is complete; agent will
1446
- // emit tool_start next. We don't need to do anything visual
1447
- // here — the placeholder is already in place and tool_start
1448
- // will refresh it with the canonical parsed args.
1449
- break;
1450
- }
1451
- case "tool_start": {
1452
- // Tool is about to execute. Upgrade the placeholder created
1453
- // by tool_call_start (or append if upstream skipped the
1454
- // streaming path).
1455
- const existing = toolCalls.find((t) => t.id === event.id);
1456
- if (existing) {
1457
- existing.args = event.args;
1458
- existing.startedAt = existing.startedAt ?? Date.now();
1459
- }
1460
- else {
1461
- const toolCall = {
1462
- id: event.id,
1463
- name: event.name,
1464
- args: event.args,
1465
- startedAt: Date.now(),
1466
- };
1467
- toolCalls.push(toolCall);
1468
- appendToolPart(assistantParts, toolCall);
1469
- }
1470
- setStreamingTools([...toolCalls]);
1471
- syncStreamingParts();
1472
- break;
1473
- }
1474
- case "tool_end": {
1475
- const tc = toolCalls.find((t) => t.id === event.id);
1476
- if (tc) {
1477
- tc.result = event.result.content;
1478
- tc.isError = event.result.isError;
1479
- tc.metadata = event.result.metadata;
1480
- setStreamingTools([...toolCalls]);
1481
- syncStreamingParts();
1482
- }
1483
- break;
1484
- }
1485
- case "tool_update": {
1486
- const tc = toolCalls.find((t) => t.id === event.id);
1487
- if (tc) {
1488
- tc.metadata = mergeToolMetadata(tc.metadata, event.update.metadata);
1489
- if (event.update.message) {
1490
- tc.result = event.update.message;
1491
- }
1492
- tc.isError = event.update.status === "failed"
1493
- || event.update.status === "blocked"
1494
- || event.update.status === "cancelled";
1495
- setStreamingTools([...toolCalls]);
1496
- syncStreamingParts();
1497
- }
1498
- else if (accumulateLiveSubagentUpdate(liveSubagentToolsRef.current, {
1499
- id: event.id,
1500
- name: event.name,
1501
- metadata: event.update.metadata,
1502
- })) {
1503
- // The launching call already settled out of this round's
1504
- // toolCalls; absorbed into the live accumulator instead.
1505
- setLiveSubagentVersion((version) => version + 1);
1506
- }
1507
- break;
1508
- }
1509
- case "mode_changed": {
1510
- // Marker already persisted by onModeUpdate at setMode time.
1511
- setPermissionMode(event.mode);
1512
- break;
1513
- }
1514
- case "input_applied": {
1515
- // The steer joined the current turn at the next model-call
1516
- // boundary. Move it after the just-finished tool/assistant
1517
- // turn instead of clearing the badge in its original
1518
- // placeholder position.
1519
- //
1520
- // This move pulls the pending-steer block out of the live
1521
- // (dynamic) region and re-commits it elsewhere in <Static>, so
1522
- // the live frame SHRINKS and the block's old rows are vacated
1523
- // with nothing taking their place. Ink's in-place redraw leaves
1524
- // those rows behind under tmux (its cursor-up clear can't reach
1525
- // a frame that has scrolled), which is the blank gap users see
1526
- // after steering. A full reprint (resetTranscript) rewrites the
1527
- // transcript cleanly with no leftover — the same fix the resize
1528
- // path uses. Unlike a turn settling (content moves in place),
1529
- // this reorder is rare, so the reprint cost is acceptable.
1530
- const steer = pendingSteersRef.current.get(event.id);
1531
- if (steer) {
1532
- pendingSteersRef.current.delete(event.id);
1533
- setPendingSteerCount(pendingSteersRef.current.size);
1534
- // Moving the steer placeholder out of the live region into
1535
- // <Static> is a pure append (it was never in the settled list,
1536
- // only the dynamic block). Off a multiplexer Ink erases the
1537
- // vacated live rows in place, so a plain append avoids the
1538
- // full-screen reprint flash. Under tmux/screen the in-place
1539
- // erase can't reach scrolled rows, so keep the clean reprint.
1540
- const commit = isMultiplexedTerminal() ? resetTranscript : updateDisplayMessages;
1541
- commit((prev) => moveStatusMessageToEnd(prev, steer.displayKey));
1542
- }
1543
- break;
1544
- }
1545
- case "input_rejected": {
1546
- // No model continuation left in this run: the steer moves to
1547
- // the next turn's queue, badge flips to QUEUED.
1548
- const steer = pendingSteersRef.current.get(event.id);
1549
- if (steer) {
1550
- pendingSteersRef.current.delete(event.id);
1551
- setPendingSteerCount(pendingSteersRef.current.size);
1552
- updateDisplayMessages((prev) => prev.map((message) => message.key === steer.displayKey ? setUserInputStatus(message, "queued") : message));
1553
- queuedInputsRef.current.push({
1554
- payload: { text: event.content, images: [] },
1555
- displayKey: steer.displayKey,
1556
- sessionFile: steer.sessionFile ?? runSessionFile,
1557
- });
1558
- setQueuedCount(queuedInputsRef.current.length);
1559
- }
1560
- break;
1561
- }
1562
- case "input_pending_changed": {
1563
- if (event.pending === 0 && pendingSteersRef.current.size > 0) {
1564
- pendingSteersRef.current.clear();
1565
- }
1566
- setPendingSteerCount(event.pending === 0 ? 0 : event.pending);
1567
- break;
1568
- }
1569
- case "turn_end": {
1570
- if (event.usage) {
1571
- goalRunUsageReported = true;
1572
- goalRunTokens += tokenUsageTotal(event.usage);
1573
- }
1574
- assistantSystemFingerprint = event.systemFingerprint;
1575
- if (event.willContinue) {
1576
- commitAssistantMessage();
1577
- clearAssistantStream();
1578
- break;
1579
- }
1580
- commitAssistantMessage(runStartRef.current ? Date.now() - runStartRef.current : undefined);
1581
- clearAssistantStream();
1582
- break;
1583
- }
1584
- }
1585
- }
1586
- }
1587
- catch (err) {
1588
- const staleExternalRun = runExternalRuntime && !isCurrentExternalRun();
1589
- if (!staleExternalRun)
1590
- commitAssistantMessage();
1591
- const cancelledError = err instanceof AgentAbortError
1592
- || err?.name === "AbortError"
1593
- || (err instanceof GrokRuntimeError && err.code === "cancelled");
1594
- if (staleExternalRun) {
1595
- runCancelled = true;
1596
- }
1597
- else if (cancelledError) {
1598
- runCancelled = true;
1599
- // commitAssistantMessage already appended the partial answer; the
1600
- // interrupt is otherwise a pure append (the partial + a "Interrupted"
1601
- // row). Off a multiplexer, append just the interrupt row so settled
1602
- // history is never reprinted — no flash. Under tmux/screen, fall back
1603
- // to the full reprint that rebuilds from the canonical agent.messages.
1604
- if (!runExternalRuntime && isMultiplexedTerminal()) {
1605
- resetTranscript(() => reconstructDisplayMessages(agent.messages));
1606
- }
1607
- else {
1608
- updateDisplayMessages((prev) => [
1609
- ...prev,
1610
- withMessageKey({
1611
- role: "assistant",
1612
- content: "Interrupted by user",
1613
- syntheticKind: "ui_interrupt",
1614
- }),
1615
- ]);
1616
- }
1617
- }
1618
- else {
1619
- runErrored = true;
1620
- updateDisplayMessages((prev) => [
1621
- ...prev,
1622
- withMessageKey({ role: "error", content: errorMessage(err) }),
1623
- ]);
1624
- }
1625
- }
1626
- finally {
1627
- cancelStreamingFlush();
1628
- // Leftover steers that never reached a model-call boundary: drop
1629
- // them on cancel (the user asked the run to stop); requeue them for
1630
- // the next turn on a normal end.
1631
- const cancelled = abortController.signal.aborted;
1632
- if (cancelled)
1633
- runCancelled = true;
1634
- if (runExternalRuntime && runCancelled) {
1635
- // External ACP owns its own conversation state, so Bubble persists
1636
- // an explicit boundary for its local transcript. Without this,
1637
- // a partial response looks successfully completed after resume.
1638
- runSessionManager.appendMessage({
1639
- role: "assistant",
1640
- content: INTERRUPTED_ASSISTANT_CONTENT,
1641
- error: {
1642
- name: "MessageAbortedError",
1643
- message: "Assistant response was interrupted by the user.",
1644
- aborted: true,
1645
- },
1646
- providerId: "grok",
1647
- model: "grok-subscription",
1648
- modelId: externalModelId,
1649
- });
1650
- }
1651
- for (const leftover of inputController.clear()) {
1652
- const steer = pendingSteersRef.current.get(leftover.id);
1653
- pendingSteersRef.current.delete(leftover.id);
1654
- if (cancelled) {
1655
- if (steer) {
1656
- updateDisplayMessages((prev) => prev.filter((message) => message.key !== steer.displayKey));
1657
- }
1658
- continue;
1659
- }
1660
- if (steer) {
1661
- updateDisplayMessages((prev) => prev.map((message) => message.key === steer.displayKey ? setUserInputStatus(message, "queued") : message));
1662
- }
1663
- queuedInputsRef.current.push({
1664
- payload: { text: leftover.content, images: [] },
1665
- displayKey: steer?.displayKey,
1666
- sessionFile: steer?.sessionFile ?? runSessionFile,
1667
- });
1668
- }
1669
- const ownsCurrentUiGeneration = !runExternalRuntime || isCurrentExternalRun();
1670
- if (ownsCurrentUiGeneration) {
1671
- setPendingSteerCount(0);
1672
- setQueuedCount(queuedInputsRef.current.length);
1673
- }
1674
- if (inputControllerRef.current === inputController)
1675
- inputControllerRef.current = null;
1676
- if (activeAbortRef.current === abortController)
1677
- activeAbortRef.current = null;
1678
- if (ownsCurrentUiGeneration) {
1679
- setIsRunning(false);
1680
- runStartRef.current = null;
1681
- setStreamingContent("");
1682
- setStreamingReasoning("");
1683
- setStreamingTools([]);
1684
- setStreamingParts([]);
1685
- }
1686
- if (!runExternalRuntime) {
1687
- maybeContinueGoal({
1688
- runCancelled,
1689
- runErrored,
1690
- isGoalRun: !!runOptions.goalRun,
1691
- runTokens: goalRunTokens,
1692
- usageReported: goalRunUsageReported,
1693
- });
1694
- }
1695
- }
1696
- };
1697
- const kickGoalTurn = (prompt, visibleInput) => {
1698
- if (activeAbortRef.current)
1699
- return;
1700
- queueMicrotask(() => {
1701
- void runAgentInput(prompt, visibleInput ?? "", [], {
1702
- hidden: visibleInput === undefined,
1703
- goalRun: true,
1704
- });
1705
- });
1706
- };
1707
- function maybeContinueGoal(input) {
1708
- if (!goalStore || exitRequestedRef.current)
1709
- return;
1710
- const current = goalStore.snapshot();
1711
- if (!current)
1712
- return;
1713
- if (input.runCancelled || input.runErrored) {
1714
- if (current.status === "active") {
1715
- goalStore.pause();
1716
- addMessage("assistant", stopReasonNotice(input.runErrored ? "error" : "cancelled"));
1717
- }
1718
- return;
1719
- }
1720
- if (input.isGoalRun) {
1721
- if (input.usageReported) {
1722
- if (input.runTokens > 0)
1723
- goalStore.addTokens(input.runTokens);
1724
- }
1725
- else {
1726
- goalStore.markTokenUsageUnavailable();
1727
- }
1728
- goalStore.incrementTurn();
1729
- }
1730
- const goal = goalStore.snapshot();
1731
- const decision = shouldContinueGoal({
1732
- goal,
1733
- queuedInputs: queuedInputsRef.current.length,
1734
- });
1735
- if (decision.continue) {
1736
- kickGoalTurn(formatInternalContextBlock("goal", continuationPrompt(goal)));
1737
- return;
1738
- }
1739
- if (decision.reason === "budget" && goal.status === "active") {
1740
- goalStore.markBudgetLimited();
1741
- }
1742
- if (decision.reason === "complete") {
1743
- addMessage("assistant", goalCompleteNotice(goal));
1744
- return;
1745
- }
1746
- const note = stopReasonNotice(decision.reason);
1747
- if (note)
1748
- addMessage("assistant", note);
1749
- }
1750
- const handleGoalCommand = async (goalInput) => {
1751
- if (!goalStore) {
1752
- addMessage("error", "Goals are not available in this session.");
1753
- return;
1754
- }
1755
- const command = parseGoalCommand(goalInput);
1756
- if (command.error) {
1757
- addMessage("error", command.error);
1758
- return;
1759
- }
1760
- const existing = goalStore.snapshot();
1761
- switch (command.kind) {
1762
- case "show": {
1763
- addMessage("assistant", existing ? goalSummaryText(existing) : "No active goal. Set one with /goal <objective>");
1764
- return;
1765
- }
1766
- case "clear": {
1767
- if (!existing) {
1768
- addMessage("assistant", "No active goal to clear");
1769
- return;
1770
- }
1771
- goalStore.clear();
1772
- addMessage("assistant", "Goal cleared");
1773
- return;
1774
- }
1775
- case "pause": {
1776
- if (!existing) {
1777
- addMessage("assistant", "No active goal to pause");
1778
- return;
1779
- }
1780
- goalStore.pause();
1781
- addMessage("assistant", "Goal paused — /goal resume to continue");
1782
- return;
1783
- }
1784
- case "resume": {
1785
- if (!existing) {
1786
- addMessage("assistant", "No goal to resume. Set one with /goal <objective>");
1787
- return;
1788
- }
1789
- const resumed = goalStore.resume();
1790
- if (resumed?.status === "active") {
1791
- addMessage("assistant", "Goal resumed");
1792
- kickGoalTurn(formatInternalContextBlock("goal", continuationPrompt(resumed)));
1793
- }
1794
- else {
1795
- addMessage("assistant", "Goal cannot be resumed (already complete)");
1796
- }
1797
- return;
1798
- }
1799
- case "edit": {
1800
- if (!existing) {
1801
- addMessage("assistant", "No active goal to edit. Set one with /goal <objective>");
1802
- return;
1803
- }
1804
- goalStore.edit(command.objective);
1805
- if (command.tokenBudget !== undefined)
1806
- goalStore.setBudget(command.tokenBudget);
1807
- addMessage("assistant", `Goal updated: ${truncate(goalStore.snapshot().objective, 60)}`);
1808
- return;
1809
- }
1810
- case "set": {
1811
- const goal = goalStore.set(command.objective, { tokenBudget: command.tokenBudget });
1812
- const budgetNote = goal.tokenBudget !== undefined ? ` (budget ${goal.tokenBudget} tok)` : "";
1813
- addMessage("assistant", `Goal set${budgetNote} — working autonomously. /goal pause to stop.`);
1814
- kickGoalTurn(formatInternalContextBlock("goal", initialPrompt(goal)), goalInput.trim());
1815
- return;
1816
- }
1817
- }
1818
- };
1819
- // Background-task wake (design §2.3b): an internal continuation, not a
1820
- // user message — hidden from the transcript (the ✦ notice row is the
1821
- // user-facing record) and wrapped as internal context for the model.
1822
- if (initialPayload.internal === "task_wake") {
1823
- await runAgentInput(input, "", [], { hidden: true });
1824
- return;
1825
- }
1826
- // Slash commands and skill invocations drop any attached images —
1827
- // they're meant for pure command routing.
1828
- if (input.trimStart().startsWith("/")) {
1829
- // Fast-path `/quit` and `/exit` before slash-registry / skill
1830
- // resolution. This guarantees a literal "/quit" always exits even if
1831
- // a skill or alias of the same name is later registered. The
1832
- // canonical handler still lives in slash-commands/commands.ts so
1833
- // `/help` and the slash menu can list it; both paths end up calling
1834
- // requestExit().
1835
- if (/^\/(?:quit|exit)\s*$/.test(input.trim())) {
1836
- requestExit();
1837
- return;
1838
- }
1839
- if (!externalSessionBound) {
1840
- if (/^\/goal(?:\s|$)/.test(input.trim())) {
1841
- await handleGoalCommand(input);
1842
- return;
1843
- }
1844
- if (/^\/loop(?:\s|$)/.test(input.trim())) {
1845
- handleLoopCommandRef.current?.(input);
1846
- return;
1847
- }
1848
- const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
1849
- if (skillInvocation) {
1850
- await runAgentInput(skillInvocation.actualPrompt, displayInput);
1851
- return;
1852
- }
1853
- }
1854
- const { handled, result, inject } = await slashRegistry.execute(input, {
1855
- agent,
1856
- addMessage,
1857
- clearMessages,
1858
- cwd: args.cwd,
1859
- exit: () => { requestExit(); },
1860
- sessionManager,
1861
- createProvider: createProvider ?? (() => {
1862
- throw new Error("Provider creation not available");
1863
- }),
1864
- openPicker,
1865
- openSessionPicker,
1866
- openRewindPicker,
1867
- openFeedback,
1868
- fillComposer,
1869
- registry: safeRegistry,
1870
- skillRegistry: safeSkillRegistry,
1871
- bashAllowlist,
1872
- settingsManager,
1873
- lspService,
1874
- mcpManager,
1875
- hookController,
1876
- flushMemory,
1877
- runMemoryCompaction,
1878
- runMemorySummary,
1879
- runMemoryRefresh,
1880
- getThemeMode: () => themeMode,
1881
- getResolvedTheme: () => themeResolved,
1882
- setThemeMode: applyThemeMode,
1883
- openStats: openStatsPanel,
1884
- compactionProgress: setCompaction,
1885
- externalRuntime,
1886
- startFreshSession,
1887
- transitionToNative,
1888
- onExternalRuntimeChange: refreshExternalRuntimeBinding,
1889
- });
1890
- if (handled) {
1891
- if (agent.mode !== permissionMode) {
1892
- setPermissionMode(agent.mode);
1893
- }
1894
- if (result) {
1895
- // `/compact` rewrites agent.messages, so the Ink transcript needs to
1896
- // be rebuilt from the new agent state before appending the summary
1897
- // card; otherwise the pre-compaction history would keep rendering.
1898
- if (result.startsWith("✓ Compaction complete")) {
1899
- const summary = latestCompactionSummary(agent.messages);
1900
- resetTranscript(() => [
1901
- ...reconstructDisplayMessages(agent.messages),
1902
- {
1903
- role: "assistant",
1904
- content: result,
1905
- syntheticKind: "ui_compact_summary",
1906
- compactionSummary: summary,
1907
- },
1908
- ]);
1909
- }
1910
- else if (result.startsWith("⏪")) {
1911
- // /rewind truncated agent.messages — rebuild the transcript from
1912
- // the rewound state before appending the summary. Subagents
1913
- // rewound out of history must not linger in the live accumulator.
1914
- clearLiveSubagentTools();
1915
- resetTranscript(() => [
1916
- ...reconstructDisplayMessages(agent.messages),
1917
- { role: "assistant", content: result },
1918
- ]);
1919
- }
1920
- else {
1921
- addMessage("assistant", result, slashResultNoticeKind(result));
1922
- }
1923
- }
1924
- if (inject) {
1925
- await runAgentInput(inject, displayInput);
1926
- }
1927
- return;
1928
- }
1929
- if (externalSessionBound) {
1930
- addMessage("error", "That command is unavailable in this external runtime session.");
1931
- return;
1932
- }
1933
- }
1934
- if (grokSessionBound) {
1935
- await runAgentInput(input, displayInput);
1936
- return;
1937
- }
1938
- const expansion = await expandAtMentions(input, args.cwd);
1939
- if (expansion.missing.length > 0) {
1940
- addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
1941
- }
1942
- for (const skip of expansion.skipped) {
1943
- if (skip.reason !== "too large")
1944
- addMessage("error", `Skipped @${skip.path}: ${skip.reason}`);
1945
- }
1946
- const agentInput = images.length > 0
1947
- ? [
1948
- ...(expansion.text ? [{ type: "text", text: expansion.text }] : []),
1949
- ...images.map((img) => ({
1950
- type: "image_url",
1951
- image_url: { url: img.dataUrl },
1952
- })),
1953
- ]
1954
- : expansion.text;
1955
- await runAgentInput(agentInput, displayInput, images.map((img) => ({ filename: img.filename, bytes: img.bytes })), { imageDisplayStart: normalized.imageDisplayStart });
1956
- }
1957
- finally {
1958
- if (startingSubmitFingerprintRef.current === submitFingerprint) {
1959
- setStartingSubmit(null);
1960
- }
1961
- }
1962
- }, [addMessage, agent, args.cwd, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, createProvider, currentSessionFile, externalRuntime, externalRuntimeBinding, externalSessionBound, fillComposer, grokSessionBound, prepareSubmitDisplay, refreshExternalRuntimeBinding, safeRegistry, safeSkillRegistry, startFreshSession, transitionToNative, unsupportedExternalSessionBound, updateDisplayMessages, queueInput, submitSteer, requestExit, setStartingSubmit]);
1963
- // --- Background tasks: markers, notices, auto-resume (design §2.3b) ---
1964
- // Owner SessionManager captured at spawn so markers always land in the
1965
- // owner session's jsonl, regardless of what is bound when the task ends.
1966
- const taskOwnerSessionsRef = useRef(new Map());
1967
- // Completions for non-current sessions, held until switch-back (§2.2c).
1968
- const pendingTaskCompletionsRef = useRef(new Map());
1969
- const taskWakeCoalescerRef = useRef(null);
1970
- const announceTaskCompletion = useCallback((task) => {
1971
- const elapsed = Math.round(((task.endedAt ?? Date.now()) - task.startedAt) / 1000);
1972
- addMessage("assistant", `Background task ${task.id}${task.description ? ` (${task.description})` : ""} ${task.status}${task.exitCode != null ? ` — exit ${task.exitCode}` : ""} in ${elapsed}s.`, "ui_notice");
1973
- }, [addMessage]);
1974
- const fireTaskWake = useCallback((tasks) => {
1975
- if (!processManager)
1976
- return;
1977
- // Gate at FIRE time (after the debounce): a turn that started meanwhile
1978
- // or queued user input suppresses the wake — the state-change reminder
1979
- // carries the completion into the next turn instead.
1980
- if (!shouldFireTaskWake({
1981
- autoResume: tasksAutoResume !== false,
1982
- turnRunning: !!activeAbortRef.current,
1983
- queuedInputs: queuedInputsRef.current.length,
1984
- exiting: exitRequestedRef.current,
1985
- })) {
1986
- return;
1987
- }
1988
- const summary = formatTaskWakeSummary(tasks, (id) => processManager.taskOutputTail(id, 2000));
1989
- for (const task of tasks)
1990
- processManager.markTaskDelivered(task.id);
1991
- void handleSubmit({
1992
- text: formatInternalContextBlock("task-finished", summary),
1993
- images: [],
1994
- internal: "task_wake",
1995
- });
1996
- }, [handleSubmit, processManager, tasksAutoResume]);
1997
- const refreshTaskSnapshot = useCallback(() => {
1998
- if (!processManager)
1999
- return;
2000
- setTaskSnapshot(processManager.listTasks(sessionManagerRef.current?.getSessionFile()));
2001
- }, [processManager]);
2002
- useEffect(() => {
2003
- refreshTaskSnapshot();
2004
- }, [refreshTaskSnapshot, sessionManager]);
2005
- useEffect(() => {
2006
- if (!processManager)
2007
- return;
2008
- taskWakeCoalescerRef.current = new TaskWakeCoalescer(TASK_WAKE_DEBOUNCE_MS, fireTaskWake);
2009
- const unsubSnapshot = processManager.onChange(() => refreshTaskSnapshot());
2010
- const unsubChange = processManager.onChange((task) => {
2011
- if (task.status !== "running" || taskOwnerSessionsRef.current.has(task.id))
2012
- return;
2013
- const owner = sessionManagerRef.current;
2014
- if (!owner || task.ownerSessionId !== owner.getSessionFile())
2015
- return;
2016
- taskOwnerSessionsRef.current.set(task.id, owner);
2017
- owner.appendMarker("task_started", JSON.stringify({
2018
- id: task.id,
2019
- pid: task.pid,
2020
- startedAt: task.startedAt,
2021
- command: task.command,
2022
- description: task.description,
2023
- }));
2024
- });
2025
- const unsubFinish = processManager.onTaskFinished((task) => {
2026
- taskOwnerSessionsRef.current.get(task.id)?.appendMarker(task.status === "killed" ? "task_killed" : "task_finished", JSON.stringify({ id: task.id, status: task.status, exitCode: task.exitCode ?? null, endedAt: task.endedAt }));
2027
- const currentFile = sessionManagerRef.current?.getSessionFile();
2028
- if (task.ownerSessionId !== currentFile) {
2029
- if (taskEligibleForWake(task) && task.ownerSessionId) {
2030
- const held = pendingTaskCompletionsRef.current.get(task.ownerSessionId) ?? [];
2031
- held.push(task);
2032
- pendingTaskCompletionsRef.current.set(task.ownerSessionId, held);
2033
- }
2034
- return;
2035
- }
2036
- announceTaskCompletion(task);
2037
- if (taskEligibleForWake(task))
2038
- taskWakeCoalescerRef.current?.add(task);
2039
- });
2040
- return () => {
2041
- unsubSnapshot();
2042
- unsubChange();
2043
- unsubFinish();
2044
- taskWakeCoalescerRef.current?.cancel();
2045
- taskWakeCoalescerRef.current = null;
2046
- };
2047
- }, [announceTaskCompletion, fireTaskWake, processManager, refreshTaskSnapshot]);
2048
- // Switch-back sweep: completions held for this session fire now (§2.2c).
2049
- useEffect(() => {
2050
- if (!processManager)
2051
- return;
2052
- const file = sessionManager?.getSessionFile();
2053
- if (!file)
2054
- return;
2055
- const held = pendingTaskCompletionsRef.current.get(file);
2056
- if (!held?.length)
2057
- return;
2058
- pendingTaskCompletionsRef.current.delete(file);
2059
- for (const task of held) {
2060
- announceTaskCompletion(task);
2061
- taskWakeCoalescerRef.current?.add(task);
2062
- }
2063
- }, [announceTaskCompletion, processManager, sessionManager]);
2064
- // Resume-time orphan report: dangling task_started markers from a previous
2065
- // process, probed for liveness (§2.2c). Runs once per mount.
2066
- const orphanProbeDoneRef = useRef(false);
2067
- useEffect(() => {
2068
- if (!processManager || orphanProbeDoneRef.current)
2069
- return;
2070
- orphanProbeDoneRef.current = true;
2071
- const manager = sessionManagerRef.current;
2072
- if (!manager)
2073
- return;
2074
- let entries;
2075
- try {
2076
- entries = manager.getEntries();
2077
- }
2078
- catch {
2079
- return;
2080
- }
2081
- for (const orphan of findDanglingTaskStarts(entries)) {
2082
- const alive = isPidAlive(orphan.pid);
2083
- addMessage("assistant", alive
2084
- ? `Background task ${orphan.id}${orphan.description ? ` (${orphan.description})` : ""} from a previous process is still running (pid ${orphan.pid}). Kill it manually if unwanted: kill ${orphan.pid}`
2085
- : `Background task ${orphan.id}${orphan.description ? ` (${orphan.description})` : ""} was orphaned by a previous process and is no longer running.`, "ui_notice");
2086
- }
2087
- }, [addMessage, processManager]);
2088
- // --- /loop: recurring prompts (design §2.6) ---
2089
- const loopsRef = useRef([]);
2090
- const nextLoopIdRef = useRef(1);
2091
- const handleSubmitRef = useRef(handleSubmit);
2092
- handleSubmitRef.current = handleSubmit;
2093
- const handleLoopCommand = useCallback((raw) => {
2094
- const parsed = parseLoopCommand(raw);
2095
- if (parsed.action === "help" || parsed.error) {
2096
- addMessage("assistant", parsed.error ?? "Usage: /loop <interval> <prompt> (e.g. /loop 5m check CI status). Also: /loop list, /loop stop [id].");
2097
- return;
2098
- }
2099
- if (parsed.action === "list") {
2100
- addMessage("assistant", formatLoopList(loopsRef.current, Date.now()));
2101
- return;
2102
- }
2103
- if (parsed.action === "stop") {
2104
- if (parsed.stopId === undefined) {
2105
- const count = loopsRef.current.length;
2106
- loopsRef.current = [];
2107
- addMessage("assistant", count > 0 ? `Stopped ${count} loop${count === 1 ? "" : "s"}.` : "No active loops.");
2108
- return;
2109
- }
2110
- const before = loopsRef.current.length;
2111
- loopsRef.current = loopsRef.current.filter((loop) => loop.id !== parsed.stopId);
2112
- addMessage("assistant", loopsRef.current.length < before ? `Stopped loop #${parsed.stopId}.` : `No loop #${parsed.stopId}.`);
2113
- return;
2114
- }
2115
- if (loopsRef.current.length >= MAX_ACTIVE_LOOPS) {
2116
- addMessage("assistant", `Loop limit reached (${MAX_ACTIVE_LOOPS}). Stop one with /loop stop [id] first.`);
2117
- return;
2118
- }
2119
- const loop = {
2120
- id: nextLoopIdRef.current++,
2121
- prompt: parsed.prompt,
2122
- intervalMs: parsed.intervalMs,
2123
- // Fires immediately on creation (next idle tick), then repeats.
2124
- nextFireAt: Date.now(),
2125
- fires: 0,
2126
- };
2127
- loopsRef.current = [...loopsRef.current, loop];
2128
- addMessage("assistant", `Loop #${loop.id} set: every ${formatInterval(loop.intervalMs)} — ${loop.prompt}\nSession-scoped; /loop stop ${loop.id} to cancel.`, "ui_notice");
2129
- }, [addMessage]);
2130
- handleLoopCommandRef.current = handleLoopCommand;
2131
- useEffect(() => {
2132
- const timer = setInterval(() => {
2133
- if (loopsRef.current.length === 0 || exitRequestedRef.current)
2134
- return;
2135
- const now = Date.now();
2136
- for (const loop of loopsRef.current) {
2137
- const decision = decideLoopFiring(loop, now, !!activeAbortRef.current);
2138
- if (decision === "wait")
2139
- continue;
2140
- loop.nextFireAt = now + loop.intervalMs;
2141
- if (decision === "defer") {
2142
- // Defer-not-stack (design §2.6): the previous turn is still running.
2143
- addMessage("assistant", `Loop #${loop.id} skipped this interval (a turn is still running); next in ${formatInterval(loop.intervalMs)}.`, "ui_notice");
2144
- continue;
2145
- }
2146
- loop.fires += 1;
2147
- void handleSubmitRef.current({
2148
- text: loop.prompt,
2149
- displayText: `⟳ loop #${loop.id}: ${loop.prompt}`,
2150
- images: [],
2151
- });
2152
- }
2153
- }, 1000);
2154
- return () => clearInterval(timer);
2155
- }, [addMessage]);
2156
- // Drain the queue once the run ends and no modal needs the user first.
2157
- // The placeholder row is removed right before resubmitting — handleSubmit
2158
- // renders the message again as a regular user row.
2159
- const drainQueuedInput = useCallback(() => {
2160
- if (activeAbortRef.current)
2161
- return;
2162
- if (startingSubmitFingerprintRef.current)
2163
- return;
2164
- if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback || pickerMode || statsPanel)
2165
- return;
2166
- const next = queuedInputsRef.current.shift();
2167
- if (!next)
2168
- return;
2169
- setQueuedCount(queuedInputsRef.current.length);
2170
- if (next.displayKey) {
2171
- updateDisplayMessages((prev) => prev.filter((message) => message.key !== next.displayKey));
2172
- }
2173
- if (!isQueuedInputForCurrentSession(next, currentSessionFile()))
2174
- return;
2175
- void handleSubmit(next.payload);
2176
- }, [pendingPlan, pendingApproval, pendingQuestion, pendingFeedback, pickerMode, statsPanel, currentSessionFile, updateDisplayMessages, handleSubmit]);
2177
- useEffect(() => {
2178
- if (isRunning || queuedCount === 0)
2179
- return;
2180
- if (startingSubmitFingerprint)
2181
- return;
2182
- if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback || pickerMode || statsPanel)
2183
- return;
2184
- const timer = setTimeout(drainQueuedInput, 0);
2185
- return () => clearTimeout(timer);
2186
- }, [isRunning, queuedCount, startingSubmitFingerprint, pendingPlan, pendingApproval, pendingQuestion, pendingFeedback, pickerMode, statsPanel, drainQueuedInput]);
2187
- const currentProviderId = externalSessionBound
2188
- ? (grokSessionBound ? GROK_SUBSCRIPTION_PROVIDER_ID : undefined)
2189
- : agent.providerId || safeRegistry.getDefault()?.id;
2190
- const keyTarget = keyProviderId
2191
- ? safeRegistry.getConfigured().find((p) => p.id === keyProviderId)
2192
- : safeRegistry.getDefault();
2193
- // Surface a pending approval as an inline badge on the matching tool row.
2194
- // ApprovalRequest does not carry a toolCallId today; matching is loose by
2195
- // type + the most identifying arg (path/command).
2196
- const approvalHint = pendingApproval
2197
- ? (() => {
2198
- const r = pendingApproval.request;
2199
- if (r.type === "bash")
2200
- return { toolName: "bash", command: r.command };
2201
- if (r.type === "edit")
2202
- return { toolName: "edit", path: r.path };
2203
- if (r.type === "write")
2204
- return { toolName: "write", path: r.path };
2205
- return null;
2206
- })()
2207
- : null;
2208
- // Binary thinking toggles have only off/on, so the graded ">2 levels" gate would hide their label;
2209
- // surface it too (rendered as "thinking mode" by formatModelLine).
2210
- const isThinkingToggle = isThinkingToggleModel(agent.providerId, agent.apiModel);
2211
- const availableThinkingLevels = getAvailableThinkingLevels(agent.providerId, agent.apiModel);
2212
- const isThinkingOnly = isThinkingOnlyLevels(availableThinkingLevels);
2213
- const showThinkingLabel = Boolean(thinkingLevel)
2214
- && thinkingLevel !== "off"
2215
- && (isThinkingToggle || isThinkingOnly || availableThinkingLevels.length > 2);
2216
- // The banner's model row stays live until the first settled row commits it
2217
- // into <Static> scrollback (so a /model switch on the fresh launch screen
2218
- // updates in place); after that the accent-colored transcript notices carry
2219
- // model changes. External runtimes surface theirs via the footer runtimeLabel.
2220
- const bannerTips = buildTips(agent, safeRegistry, grokSessionBound);
2221
- const bannerProviderId = grokSessionBound ? "grok" : externalSessionBound ? undefined : agent.providerId || safeRegistry.getDefault()?.id;
2222
- const bannerModelId = grokSessionBound ? externalRuntimeBinding?.modelId : externalSessionBound ? undefined : agent.apiModel;
2223
- const bannerModelLabel = externalSessionBound
2224
- ? (grokSessionBound
2225
- ? `Grok Subscription${externalRuntimeBinding?.modelId ? ` · ${externalRuntimeBinding.modelId}` : ""}`
2226
- : "Unsupported external runtime · recovery-only mode")
2227
- : agent.model ? displayModel(agent.model) : undefined;
2228
- const bannerThinkingLabel = grokSessionBound
2229
- ? externalRuntimeBinding?.reasoningEffort
2230
- : !externalSessionBound && showThinkingLabel ? thinkingLevel : undefined;
2231
- // Footer surfaces the graded effort level too (grok/DeepSeek/OpenAI ladders),
2232
- // but skips on/off toggles and thinking-only placeholders ("medium").
2233
- const footerThinkingLabel = !externalSessionBound
2234
- && thinkingLevel
2235
- && thinkingLevel !== "off"
2236
- && !isThinkingToggle
2237
- && !isThinkingOnly
2238
- ? thinkingLevel
2239
- : undefined;
2240
- const welcomeBannerNode = showWelcome ? (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(BubbleCodeWordmark, { width: terminalColumns }), _jsx(WelcomeBanner, { terminalColumns: terminalColumns, tips: bannerTips, updateNotice: currentUpdateNotice, cwd: friendlyCwd(args.cwd), sessionLabel: sessionBasename(currentSessionFile()), providerId: bannerProviderId, modelId: bannerModelId, modelLabel: bannerModelLabel, thinkingLabel: bannerThinkingLabel })] })) : null;
2241
- const commandPaletteItems = useMemo(() => buildCommandPaletteItems(safeSkillRegistry, externalSessionBound), [externalSessionBound, safeSkillRegistry]);
2242
- const mcpReconnectItems = useMemo(() => buildMcpReconnectItems(mcpManager), [mcpManager]);
2243
- // No fixed-height frame: settled rows flow into the terminal's native
2244
- // scrollback via <Static>, and only the dynamic bottom stack (streaming
2245
- // tail, pickers, composer, footer) occupies the live region. Letting it size
2246
- // to its content keeps the composer pinned just below the latest output the
2247
- // way ordinary shell programs do.
2248
- const mainWidth = Math.max(40, terminalColumns);
2249
- return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", width: mainWidth, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: mainWidth, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode, staticGeneration: staticGeneration, paddingX: 1, maxStreamRows: Math.max(6, terminalRows - 10) }), pickerMode === "grok-model" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: grokModels.length === 0 ? (_jsx(Text, { children: "Loading Grok subscription models\u2026" })) : (_jsx(ProviderPicker, { title: "Select Grok Model and Reasoning", providers: grokModels.flatMap((model) => model.reasoningLevels.map((effort) => ({
2250
- id: JSON.stringify({ modelId: model.id, reasoningEffort: effort }),
2251
- name: `${model.name}${effort === "off" ? "" : ` · ${effort}`}`,
2252
- enabled: true,
2253
- }))), current: JSON.stringify({
2254
- modelId: externalRuntimeBinding?.modelId,
2255
- reasoningEffort: externalRuntimeBinding?.reasoningEffort ?? "high",
2256
- }), onSelect: handleGrokModelSelect, onCancel: closePicker })) })), pickerMode === "model" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ModelPicker, { registry: safeRegistry, current: agent.model, currentThinkingLevel: thinkingLevel, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: closePicker }) })), pickerMode === "provider" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: withGrokSubscriptionProvider(BUILTIN_PROVIDERS
2257
- .filter((p) => isUserVisibleProvider(p.id))
2258
- .map((p) => {
2259
- const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
2260
- const configuredLabel = configured?.apiKey ? "configured" : "needs key";
2261
- return {
2262
- id: p.id,
2263
- name: `${p.name} [${configuredLabel}]`,
2264
- enabled: true,
2265
- };
2266
- })), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker }) })), pickerMode === "provider-add" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: withGrokSubscriptionProvider(BUILTIN_PROVIDERS
2267
- .filter((p) => isUserVisibleProvider(p.id))
2268
- .map((p) => ({ id: p.id, name: p.name, enabled: true }))), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: closePicker, title: "Add Provider" }) })), pickerMode === "login" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers:
2269
- // The native grok provider is OAuth-capable, so it appears via
2270
- // supportsOAuth like OpenAI — no extra hardcoded row.
2271
- BUILTIN_PROVIDERS
2272
- .filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
2273
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: closePicker, title: "Select Login Provider" }) })), pickerMode === "logout" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: [
2274
- ...safeRegistry.getConfigured()
2275
- .filter((p) => safeRegistry.getAuthStorage().has(p.id))
2276
- .map((p) => ({ id: p.id, name: p.name, enabled: true })),
2277
- ...(grokSessionBound
2278
- ? [{ id: GROK_SUBSCRIPTION_PROVIDER_ID, name: "Grok Subscription [local login]", enabled: true }]
2279
- : []),
2280
- ], current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: closePicker, title: "Select Logout Provider" }) })), pickerMode === "key" && keyTarget && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
2281
- closePicker();
2282
- setKeyProviderId(null);
2283
- } }) })), pickerMode === "skill" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
2284
- fillComposer(`/${name} `);
2285
- closePicker();
2286
- }, onCancel: closePicker }) })), pickerMode === "theme" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { title: "Select Theme", providers: [
2287
- { id: "auto", name: `Auto — match terminal (${autoResolved})`, enabled: true },
2288
- { id: "light", name: "Light", enabled: true },
2289
- { id: "dark", name: "Dark", enabled: true },
2290
- ], current: themePickerRevertRef.current, onSelect: handleThemeSelect, onHighlight: handleThemeHighlight, onCancel: handleThemeCancel }) })), pickerMode === "slash" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(CommandPalette, { items: commandPaletteItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
2291
- closePicker();
2292
- if (item.action === "insert-skill") {
2293
- fillComposer(`/${item.value} `);
2294
- }
2295
- else {
2296
- void handleSubmit(item.command);
2297
- }
2298
- }, onCancel: closePicker }) })), pickerMode === "mcp-reconnect" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(McpReconnectPicker, { items: mcpReconnectItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
2299
- closePicker();
2300
- void handleSubmit(item.command);
2301
- }, onCancel: closePicker }) })), pickerMode === "session" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SessionPicker, { currentCwd: args.cwd, currentSessions: SessionManager.summarizeSessionsForCwd(args.cwd), allSessions: SessionManager.listAllSessions(), onSelect: handleSessionSelect, onCancel: closePicker }) })), pickerMode === "agents" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SubagentInspector, { groups: subagentGroups, onCancel: closePicker, tasks: taskSnapshot, onKillTask: (taskId) => { void processManager?.killTask(taskId); } }) })), pickerMode === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
2302
- closePicker();
2303
- void handleSubmit(command);
2304
- }, onCancel: closePicker }) })), pickerMode === "feishu-setup" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeishuSetupPicker, { onComplete: (summary) => {
2305
- closePicker();
2306
- addMessage("assistant", summary);
2307
- }, onCancel: () => {
2308
- closePicker();
2309
- addMessage("assistant", "已取消 Feishu setup。");
2310
- } }) })), statsPanel && !pickerMode && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(StatsPanel, { panel: statsPanel, terminalColumns: mainWidth, terminalRows: terminalRows, onRangeChange: (range) => setStatsPanel((current) => current ? { ...current, range } : current), onCancel: closeStatsPanel }) })), pendingPlan && !pickerMode && !statsPanel && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
2311
- const resolve = pendingPlan.resolve;
2312
- setPendingPlan(null);
2313
- resolve({ action: "approve", plan: finalPlan });
2314
- }, onReject: (reason) => {
2315
- const resolve = pendingPlan.resolve;
2316
- setPendingPlan(null);
2317
- resolve({ action: "reject", reason });
2318
- } }) })), pendingApproval && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ApprovalDialog, { request: pendingApproval.request, onDecision: (decision) => {
2319
- const resolve = pendingApproval.resolve;
2320
- setPendingApproval(null);
2321
- resolve(decision);
2322
- }, onAllowBashPrefix: (prefix) => {
2323
- bashAllowlist?.add(prefix);
2324
- } }) })), pendingQuestion && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingFeedback && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
2325
- questionController?.reply(pendingQuestion.id, answers);
2326
- setPendingQuestion(null);
2327
- }, onCancel: () => {
2328
- questionController?.reject(pendingQuestion.id);
2329
- setPendingQuestion(null);
2330
- } }) })), pendingFeedback && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeedbackDialog, { base: pendingFeedback.base, initialDescription: pendingFeedback.initialDescription, onDismiss: () => setPendingFeedback(null), onResult: (result) => {
2331
- if (result.kind === "success") {
2332
- addMessage("assistant", `Feedback submitted: ${result.url}`);
2333
- }
2334
- else if (result.kind === "error") {
2335
- addMessage("error", `Feedback failed: ${result.message}`);
2336
- }
2337
- } }) })), !isExiting && compaction && (_jsx(Box, { flexShrink: 0, backgroundColor: palette.background, children: _jsx(CompactionProgressCard, { progress: compaction }) })), !isExiting && isRunning && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (
2338
- // The streaming tail supplies its own bottom margin once it has
2339
- // visible output, but right after a prompt is sent the tail renders
2340
- // nothing and the spinner would hug the user's message bubble — give
2341
- // it a top gap only in that empty-tail state to avoid double spacing.
2342
- _jsx(Box, { paddingX: 1, paddingBottom: 1, paddingTop: streamingParts.length === 0
2343
- && streamingContent.length === 0
2344
- && streamingTools.length === 0
2345
- && (streamingReasoning.length === 0 || !(showThinking || verboseTrace))
2346
- ? 1
2347
- : 0, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined, onArrowDownAtBottom: () => {
2348
- if (subagentMembers.length > 0 && !pickerMode)
2349
- setSubagentEntryFocused(true);
2350
- }, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: externalSessionBound ? undefined : safeSkillRegistry, localSlashCommands: externalSessionBound ? [] : [...INK_LOCAL_SLASH_COMMANDS], allowedSlashCommands: unsupportedExternalSessionBound ? GROK_LOCAL_SLASH_COMMANDS : undefined, allowWorkspaceMentions: !unsupportedExternalSessionBound, allowImageAttachments: !externalSessionBound, terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (subagentMembers.length > 0 || runningTasks.length > 0) && (_jsxs(Box, { paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [[
2351
- subagentMembers.length > 0
2352
- ? `${subagentMembers.length} subagent${subagentMembers.length === 1 ? "" : "s"} · ${subagentSummary(subagentMembers)}`
2353
- : undefined,
2354
- runningTasks.length > 0
2355
- ? `${runningTasks.length} task${runningTasks.length === 1 ? "" : "s"} · ${taskRowSummary(runningTasks, nowTick)}`
2356
- : undefined,
2357
- ].filter(Boolean).join(" · "), " \u00B7 "] }), subagentMembers.length > 0
2358
- ? _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })
2359
- : _jsx(Text, { color: palette.accent, children: "task_output to check \u00B7 kill_task to stop" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({
2360
- mode: permissionMode,
2361
- goalLine: externalSessionBound ? undefined : goalLine,
2362
- runtimeLabel: externalSessionBound
2363
- ? (grokSessionBound
2364
- ? `Grok Subscription${externalRuntimeBinding?.modelId ? ` · ${externalRuntimeBinding.modelId}` : ""}${externalRuntimeBinding?.reasoningEffort && externalRuntimeBinding.reasoningEffort !== "off" ? ` · ${externalRuntimeBinding.reasoningEffort}` : ""} · workspace`
2365
- : "Unsupported external runtime · recovery-only")
2366
- : undefined,
2367
- cwd: friendlyCwd(args.cwd),
2368
- branch,
2369
- model: externalSessionBound
2370
- ? undefined
2371
- : (agent.model
2372
- ? `${displayModel(agent.model)}${footerThinkingLabel ? ` · ${footerThinkingLabel}` : ""}`
2373
- : undefined),
2374
- sessionTitle: externalSessionBound ? undefined : (sessionManager?.getMetadata().title?.trim() || undefined),
2375
- contextUsage: externalSessionBound ? undefined : (contextUsage || undefined),
2376
- }) }) }))] }) }));
2377
- }