@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,1279 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- import { Box, Text, useCursor, useInput, usePaste, useStdout } from "ink";
4
- import { visualWidth, graphemeWidth } from "./width.js";
5
- import { appendFileSync } from "node:fs";
6
- import { registry as slashRegistry } from "../slash-commands/index.js";
7
- import { useTheme } from "./theme.js";
8
- import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
9
- import { bareImageFilenameFromPaste, extractImagePathTokens, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
10
- import { appendHistoryEntry, loadHistoryEntriesSync, pushHistoryEntry, stepHistory, } from "./input-history.js";
11
- import { cloneComposerBuffer, createComposerBuffer, deleteComposerBackward, deleteComposerForward, expandComposerBuffer, insertComposerPaste, insertComposerText, moveComposerCursor, replaceComposerRange, } from "./composer-buffer.js";
12
- import { isKeyReleaseEvent } from "./key-events.js";
13
- import { PasteOperationTracker } from "./paste-operation-tracker.js";
14
- import { stripTerminalMouseSequences } from "./terminal-mouse.js";
15
- import { submitPayloadFingerprint } from "./submit-dedupe.js";
16
- export { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
17
- import { shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
18
- import { imageDisplayLabel, stripInlineImageLabels } from "../tui/image-display.js";
19
- export function resolveComposerAtContext(text, cursor, isSlashContext, allowWorkspaceMentions = true) {
20
- return !allowWorkspaceMentions || isSlashContext ? null : findAtContext(text, cursor);
21
- }
22
- export function createComposerDraftSnapshot(input) {
23
- return {
24
- buffer: cloneComposerBuffer(input.buffer),
25
- attachments: input.attachments.map((attachment) => ({ ...attachment })),
26
- imageLabelStartOverride: input.imageLabelStartOverride,
27
- };
28
- }
29
- /**
30
- * Rebuild a composer buffer from a persisted history entry. Text long enough to
31
- * have been collapsed at paste time is collapsed again into a `[Pasted text #N …]`
32
- * marker, but its full content is preserved in the buffer's paste spans so the
33
- * composer stays compact, no content is lost, and the next submit re-expands it.
34
- */
35
- function bufferFromHistoryText(text) {
36
- if (shouldCollapsePastedContent(text)) {
37
- return insertComposerPaste(createComposerBuffer(), text);
38
- }
39
- return createComposerBuffer(text);
40
- }
41
- /**
42
- * Browse persisted history without turning marker-shaped text into live paste
43
- * tokens, while preserving the exact unsent draft in memory for the final
44
- * Down transition back out of history.
45
- */
46
- export function stepComposerHistory(state, history, direction) {
47
- const plainDraft = state.draftSnapshot
48
- ? {
49
- text: state.draftSnapshot.buffer.text,
50
- images: state.draftSnapshot.attachments,
51
- ...(state.draftSnapshot.imageLabelStartOverride !== null
52
- ? { imageDisplayStart: state.draftSnapshot.imageLabelStartOverride }
53
- : {}),
54
- }
55
- : { text: "", images: [] };
56
- const result = stepHistory({ history, index: state.historyIndex, draft: plainDraft }, direction, {
57
- text: state.buffer.text,
58
- images: state.attachments,
59
- ...(state.imageLabelStartOverride !== null
60
- ? { imageDisplayStart: state.imageLabelStartOverride }
61
- : {}),
62
- });
63
- if (!result.changed)
64
- return { ...state, changed: false };
65
- let draftSnapshot = state.draftSnapshot;
66
- if (state.historyIndex === null && result.index !== null && draftSnapshot === null) {
67
- draftSnapshot = createComposerDraftSnapshot({
68
- buffer: state.buffer,
69
- attachments: state.attachments,
70
- imageLabelStartOverride: state.imageLabelStartOverride,
71
- });
72
- }
73
- if (result.index === null && draftSnapshot) {
74
- const restored = createComposerDraftSnapshot(draftSnapshot);
75
- return {
76
- ...restored,
77
- historyIndex: null,
78
- draftSnapshot: null,
79
- changed: true,
80
- };
81
- }
82
- return {
83
- buffer: bufferFromHistoryText(result.text),
84
- attachments: (result.images ?? []).map((attachment) => ({ ...attachment })),
85
- imageLabelStartOverride: result.imageDisplayStart ?? null,
86
- historyIndex: result.index,
87
- draftSnapshot: result.index === null ? null : draftSnapshot,
88
- changed: true,
89
- };
90
- }
91
- const MAX_VISIBLE_LINES = 6;
92
- const PADDING_X = 1;
93
- const PROMPT = " > ";
94
- const MAX_VISIBLE_SUGGESTIONS = 8;
95
- export function needsCursorRowCompensation(nextOutputHeight, viewportRows, previousOutputHeight) {
96
- const hadPreviousFrame = previousOutputHeight !== null && previousOutputHeight > 0;
97
- const isFullscreen = nextOutputHeight >= viewportRows;
98
- const wasFullscreen = hadPreviousFrame && previousOutputHeight >= viewportRows;
99
- const wasOverflowing = hadPreviousFrame && previousOutputHeight > viewportRows;
100
- const isOverflowing = nextOutputHeight > viewportRows;
101
- const isLeavingFullscreen = wasFullscreen && nextOutputHeight < viewportRows;
102
- // Ink omits the trailing newline in two cases that matter for cursor math:
103
- // the normal fullscreen frame, and the clear/sync frame used when leaving an
104
- // overflowing viewport. buildCursorSuffix still assumes the cursor starts one
105
- // line below the output, so pass y+1 in those cases.
106
- return isFullscreen || wasOverflowing || (isOverflowing && hadPreviousFrame) || isLeavingFullscreen;
107
- }
108
- export function resolveCursorRowCompensation(input) {
109
- if (input.sameRenderedFrame)
110
- return input.previousRowCompensation;
111
- return needsCursorRowCompensation(input.nextOutputHeight, input.viewportRows, input.previousOutputHeight) ? 1 : 0;
112
- }
113
- export function isCtrlLetterInput(input, key, letter) {
114
- const normalized = letter.toLowerCase();
115
- if (!/^[a-z]$/.test(normalized))
116
- return false;
117
- const rawControlInput = String.fromCharCode(normalized.charCodeAt(0) - 96);
118
- return input === rawControlInput || (key.ctrl === true && input.toLowerCase() === normalized);
119
- }
120
- export function isCtrlCInput(input, key) {
121
- return isCtrlLetterInput(input, key, "c");
122
- }
123
- export function shouldUseHardwareComposerCursor(env = process.env) {
124
- return env.BUBBLE_HARDWARE_CURSOR === "1";
125
- }
126
- export function composerVerticalArrowDirection(key) {
127
- if (key.upArrow)
128
- return "up";
129
- if (key.downArrow)
130
- return "down";
131
- return undefined;
132
- }
133
- export function resolveSoftwareCursorCellStyle(input) {
134
- // Inverse video swaps the cell's real foreground/background at the terminal
135
- // level, so the cursor block stays visible on any terminal palette without
136
- // guessing a contrasting pair.
137
- return { inverse: input.visible, color: input.textColor };
138
- }
139
- /**
140
- * Split a composer line around the cursor so the cell under it can render as
141
- * an inverse-video software cursor. The visible cursor must not depend on the
142
- * real terminal cursor: Ink only re-arms its one-shot cursor escape when the
143
- * component owning useCursor re-commits, so frames produced by other
144
- * components' local state (the waiting spinner, viewport scrolling) hide the
145
- * hardware cursor for most of an agent run. Drawing and blinking the cell
146
- * ourselves keeps it visible while preserving normal typing feedback; the real
147
- * cursor is still positioned for IME anchoring.
148
- */
149
- export function splitLineAtCursor(lineText, charOffset) {
150
- const offset = Math.max(0, Math.min(charOffset, lineText.length));
151
- if (offset >= lineText.length) {
152
- return { before: lineText, at: " ", after: "" };
153
- }
154
- const codePoint = lineText.codePointAt(offset);
155
- const length = codePoint > 0xffff ? 2 : 1;
156
- return {
157
- before: lineText.slice(0, offset),
158
- at: lineText.slice(offset, offset + length),
159
- after: lineText.slice(offset + length),
160
- };
161
- }
162
- // Break a logical line into segments that each fit within `maxWidth` display
163
- // columns. Uses the shared terminal-aware width (./width.js) so CJK, emoji and
164
- // ambiguous-width chars wrap exactly as the terminal renders them; empty lines
165
- // still produce one empty segment so cursors on blank lines render.
166
- function wrapLineByWidth(line, maxWidth) {
167
- if (line.length === 0)
168
- return [""];
169
- const out = [];
170
- let current = "";
171
- let currentWidth = 0;
172
- for (const ch of line) {
173
- const w = graphemeWidth(ch);
174
- if (currentWidth + w > maxWidth && current.length > 0) {
175
- out.push(current);
176
- current = "";
177
- currentWidth = 0;
178
- }
179
- current += ch;
180
- currentWidth += w;
181
- }
182
- if (current.length > 0 || out.length === 0)
183
- out.push(current);
184
- return out;
185
- }
186
- function computeVisualLines(text, maxWidth) {
187
- const logical = text.split("\n");
188
- const out = [];
189
- let abs = 0;
190
- for (let lIdx = 0; lIdx < logical.length; lIdx++) {
191
- const line = logical[lIdx];
192
- const segments = wrapLineByWidth(line, maxWidth);
193
- let offset = 0;
194
- for (const seg of segments) {
195
- out.push({ text: seg, absStart: abs + offset, logicalLineIndex: lIdx });
196
- offset += seg.length;
197
- }
198
- abs += line.length + 1; // consume the "\n"
199
- }
200
- return out;
201
- }
202
- // Map a source-text cursor index to its (visualRow, visualCol) coordinates.
203
- function cursorToVisual(visualLines, cursor) {
204
- if (visualLines.length === 0)
205
- return { row: 0, col: 0 };
206
- let row = 0;
207
- for (let i = 0; i < visualLines.length; i++) {
208
- if (visualLines[i].absStart <= cursor)
209
- row = i;
210
- else
211
- break;
212
- }
213
- const vl = visualLines[row];
214
- const charOffset = Math.max(0, cursor - vl.absStart);
215
- return { row, col: visualWidth(vl.text.slice(0, charOffset)) };
216
- }
217
- // Map a (visualRow, visualCol) target back to a source-text cursor index.
218
- // Used by up/down arrows to preserve the visual column when jumping rows.
219
- function visualToCursor(visualLines, row, col) {
220
- if (visualLines.length === 0)
221
- return 0;
222
- const clamped = Math.max(0, Math.min(visualLines.length - 1, row));
223
- const vl = visualLines[clamped];
224
- let width = 0;
225
- let charOffset = 0;
226
- for (const ch of vl.text) {
227
- const w = graphemeWidth(ch);
228
- if (width + w > col)
229
- break;
230
- width += w;
231
- charOffset += ch.length;
232
- }
233
- return vl.absStart + charOffset;
234
- }
235
- export function resolveSlashCommandHighlightRange(input, commandNames) {
236
- if (!input.startsWith("/"))
237
- return null;
238
- const match = /^\/([^\s]+)/.exec(input);
239
- if (!match)
240
- return null;
241
- const commandName = match[1]?.toLowerCase();
242
- if (!commandName)
243
- return null;
244
- for (const name of commandNames) {
245
- if (name.toLowerCase() === commandName) {
246
- return { start: 0, end: match[0].length };
247
- }
248
- }
249
- return null;
250
- }
251
- function splitHighlightedText(text, absStart, highlight) {
252
- if (!text)
253
- return [];
254
- if (!highlight)
255
- return [{ kind: "normal", text }];
256
- const start = Math.max(0, highlight.start - absStart);
257
- const end = Math.min(text.length, highlight.end - absStart);
258
- if (start >= end)
259
- return [{ kind: "normal", text }];
260
- const segments = [];
261
- if (start > 0)
262
- segments.push({ kind: "normal", text: text.slice(0, start) });
263
- segments.push({ kind: "command", text: text.slice(start, end) });
264
- if (end < text.length)
265
- segments.push({ kind: "normal", text: text.slice(end) });
266
- return segments;
267
- }
268
- export function splitComposerTextSegments(input) {
269
- if (input.cursorOffset === undefined) {
270
- return splitHighlightedText(input.text, input.absStart, input.highlight);
271
- }
272
- const cursorOffset = Math.max(0, Math.min(input.text.length, input.cursorOffset));
273
- const cursorSegments = splitLineAtCursor(input.text, cursorOffset);
274
- const cursorConsumesSource = cursorOffset < input.text.length;
275
- return [
276
- ...splitHighlightedText(cursorSegments.before, input.absStart, input.highlight),
277
- { kind: "cursor", text: cursorSegments.at },
278
- ...splitHighlightedText(cursorSegments.after, input.absStart + cursorOffset + (cursorConsumesSource ? cursorSegments.at.length : 0), input.highlight),
279
- ];
280
- }
281
- export function shouldSubmitExactSlashSuggestion(input, suggestionName) {
282
- if (!suggestionName)
283
- return false;
284
- return input.trim() === `/${suggestionName}`;
285
- }
286
- export function resolveSlashEnterAction(input, suggestions, selectedIndex) {
287
- if (suggestions.some((item) => shouldSubmitExactSlashSuggestion(input, item.name))) {
288
- return { kind: "submit" };
289
- }
290
- const suggestion = suggestions[selectedIndex];
291
- return suggestion ? { kind: "complete", text: `/${suggestion.name} ` } : { kind: "none" };
292
- }
293
- const KITTY_RETURN_PRIVATE_USE = String.fromCodePoint(57345);
294
- export function isInkModifiedEnterInput(input) {
295
- const normalized = input.startsWith("\x1b") ? input.slice(1) : input;
296
- return normalized === KITTY_RETURN_PRIVATE_USE
297
- || /^\[(?:13|57345)(?::\d+){0,2};[2-9]\d*(?::[12])?u$/.test(normalized)
298
- || /^\[27;[2-9]\d*(?::[12])?;(?:13|57345)~$/.test(normalized);
299
- }
300
- export function resolveInkEnterIntent(input, key) {
301
- if (key.eventType === "release")
302
- return "none";
303
- const hasReturnInput = !!input && /[\r\n]/.test(input);
304
- if (isInkModifiedEnterInput(input))
305
- return "newline";
306
- const isEnter = hasReturnInput || !!key.return;
307
- if (!isEnter)
308
- return "none";
309
- if (key.shift || key.ctrl || key.meta)
310
- return "newline";
311
- return "submit";
312
- }
313
- export function shouldDeferComposerCommitWhilePastePending(hasPendingPaste, enterIntent, key) {
314
- return hasPendingPaste && (enterIntent === "submit" || key.tab === true);
315
- }
316
- export function insertNewlineAtCursor(text, cursor) {
317
- const clampedCursor = Math.max(0, Math.min(text.length, cursor));
318
- return {
319
- text: `${text.slice(0, clampedCursor)}\n${text.slice(clampedCursor)}`,
320
- cursor: clampedCursor + 1,
321
- };
322
- }
323
- export function previousWordBoundary(text, cursor) {
324
- const clampedCursor = Math.max(0, Math.min(text.length, cursor));
325
- if (clampedCursor === 0)
326
- return 0;
327
- let index = clampedCursor - 1;
328
- while (index > 0 && /\s/.test(text[index]))
329
- index--;
330
- while (index > 0 && !/\s/.test(text[index - 1]))
331
- index--;
332
- return index;
333
- }
334
- export function nextWordBoundary(text, cursor) {
335
- const clampedCursor = Math.max(0, Math.min(text.length, cursor));
336
- if (clampedCursor === text.length)
337
- return text.length;
338
- let index = clampedCursor;
339
- while (index < text.length && /\s/.test(text[index]))
340
- index++;
341
- while (index < text.length && !/\s/.test(text[index]))
342
- index++;
343
- return index;
344
- }
345
- export function lineStartBoundary(text, cursor) {
346
- const clampedCursor = Math.max(0, Math.min(text.length, cursor));
347
- return text.lastIndexOf("\n", clampedCursor - 1) + 1;
348
- }
349
- export function lineEndBoundary(text, cursor) {
350
- const clampedCursor = Math.max(0, Math.min(text.length, cursor));
351
- const lineEnd = text.indexOf("\n", clampedCursor);
352
- return lineEnd === -1 ? text.length : lineEnd;
353
- }
354
- export function deleteToLineStart(text, cursor) {
355
- const clampedCursor = Math.max(0, Math.min(text.length, cursor));
356
- const lineStart = lineStartBoundary(text, clampedCursor);
357
- return {
358
- text: text.slice(0, lineStart) + text.slice(clampedCursor),
359
- cursor: lineStart,
360
- };
361
- }
362
- export function deleteToLineEnd(text, cursor) {
363
- const clampedCursor = Math.max(0, Math.min(text.length, cursor));
364
- const lineEnd = lineEndBoundary(text, clampedCursor);
365
- return {
366
- text: text.slice(0, clampedCursor) + text.slice(lineEnd),
367
- cursor: clampedCursor,
368
- };
369
- }
370
- export function deleteAtCursor(text, cursor) {
371
- const clampedCursor = Math.max(0, Math.min(text.length, cursor));
372
- if (clampedCursor >= text.length)
373
- return { text, cursor: clampedCursor };
374
- return {
375
- text: text.slice(0, clampedCursor) + text.slice(clampedCursor + 1),
376
- cursor: clampedCursor,
377
- };
378
- }
379
- export function resolveComposerEditAction(input, key) {
380
- if (key.home)
381
- return "line-start";
382
- if (key.end)
383
- return "line-end";
384
- const wordModifier = key.ctrl || key.meta;
385
- if (wordModifier && key.leftArrow)
386
- return "word-left";
387
- if (wordModifier && key.rightArrow)
388
- return "word-right";
389
- const lowerInput = input.toLowerCase();
390
- if ((key.ctrl && lowerInput === "a") || input === "\x01")
391
- return "line-start";
392
- if ((key.ctrl && lowerInput === "e") || input === "\x05")
393
- return "line-end";
394
- if ((key.ctrl && lowerInput === "u") || input === "\x15")
395
- return "delete-line-start";
396
- if ((key.ctrl && lowerInput === "k") || input === "\x0b")
397
- return "delete-line-end";
398
- return null;
399
- }
400
- export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDownAtBottom, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, localSlashCommands = [], allowedSlashCommands, allowWorkspaceMentions = true, allowImageAttachments = true, terminalColumns, cwd, sessionFile, nextImageLabelStart = 1, }) {
401
- const theme = useTheme();
402
- const width = terminalColumns;
403
- const historyScope = useMemo(() => ({ sessionFile, cwd }), [sessionFile, cwd]);
404
- const hardwareCursorEnabled = shouldUseHardwareComposerCursor();
405
- const [composerBuffer, setComposerBuffer] = useState(() => createComposerBuffer());
406
- const composerBufferRef = useRef(composerBuffer);
407
- composerBufferRef.current = composerBuffer;
408
- const updateComposerBuffer = React.useCallback((transition) => {
409
- const next = transition(composerBufferRef.current);
410
- composerBufferRef.current = next;
411
- setComposerBuffer(next);
412
- return next;
413
- }, []);
414
- const { text, cursor } = composerBuffer;
415
- const [softwareCursorVisible, setSoftwareCursorVisible] = useState(true);
416
- const [selectedIndex, setSelectedIndex] = useState(0);
417
- const [projectFiles, setProjectFiles] = useState(null);
418
- const [attachments, setAttachments] = useState([]);
419
- const [imageLabelStartOverride, setImageLabelStartOverride] = useState(null);
420
- const [history, setHistory] = useState(() => loadHistoryEntriesSync({ scope: historyScope }));
421
- const [historyIndex, setHistoryIndex] = useState(null);
422
- const historyDraftRef = useRef(null);
423
- const historyScopeRef = useRef(historyScope);
424
- const historyScopeKey = `${historyScope.cwd}\0${historyScope.sessionFile ?? ""}`;
425
- const previousHistoryScopeKeyRef = useRef(historyScopeKey);
426
- const draftIdentityEpochRef = useRef(0);
427
- const pasteOperationsRef = useRef(new PasteOperationTracker());
428
- const submittedPayloadFingerprintRef = useRef(null);
429
- const loadingFilesRef = useRef(false);
430
- // Kept equal to attachments.length so a synchronous multi-image paste loop
431
- // assigns each image its correct (distinct) inline label index.
432
- const attachmentCountRef = useRef(0);
433
- historyScopeRef.current = historyScope;
434
- const invalidateDraftAsyncWork = React.useCallback(() => {
435
- draftIdentityEpochRef.current += 1;
436
- pasteOperationsRef.current.invalidateAll();
437
- }, []);
438
- const ensureImageLabelStart = React.useCallback(() => {
439
- setImageLabelStartOverride((current) => current ?? nextImageLabelStart);
440
- }, [nextImageLabelStart]);
441
- useEffect(() => {
442
- const scopeChanged = previousHistoryScopeKeyRef.current !== historyScopeKey;
443
- previousHistoryScopeKeyRef.current = historyScopeKey;
444
- setHistory(loadHistoryEntriesSync({ scope: historyScope }));
445
- setHistoryIndex(null);
446
- setImageLabelStartOverride(null);
447
- historyDraftRef.current = null;
448
- if (scopeChanged) {
449
- updateComposerBuffer(() => createComposerBuffer());
450
- setAttachments([]);
451
- attachmentCountRef.current = 0;
452
- setProjectFiles(null);
453
- loadingFilesRef.current = false;
454
- }
455
- invalidateDraftAsyncWork();
456
- }, [historyScope, historyScopeKey, invalidateDraftAsyncWork, updateComposerBuffer]);
457
- useEffect(() => () => {
458
- invalidateDraftAsyncWork();
459
- }, [invalidateDraftAsyncWork]);
460
- const isSlashContext = text.startsWith("/") && cursor > 0 && !text.includes("\n");
461
- const slashPrefix = isSlashContext ? text.slice(1).toLowerCase() : "";
462
- const atContext = useMemo(() => resolveComposerAtContext(text, cursor, isSlashContext, allowWorkspaceMentions), [allowWorkspaceMentions, text, cursor, isSlashContext]);
463
- useEffect(() => {
464
- if (!atContext || projectFiles !== null || loadingFilesRef.current)
465
- return;
466
- loadingFilesRef.current = true;
467
- listProjectFiles(cwd).then((files) => setProjectFiles(files), () => setProjectFiles([]));
468
- }, [atContext, cwd, projectFiles]);
469
- // The rendered inverse-video cell below is the visible cursor. Keep Ink's
470
- // terminal cursor hidden by default so it can't race the software cursor; the
471
- // hardware cursor can be enabled for IME diagnostics with BUBBLE_HARDWARE_CURSOR=1.
472
- useEffect(() => {
473
- if (!hardwareCursorEnabled)
474
- return;
475
- if (!process.stdout.isTTY)
476
- return;
477
- process.stdout.write("\x1b[1 q"); // blinking block
478
- return () => {
479
- process.stdout.write("\x1b[0 q"); // reset to terminal default
480
- };
481
- }, [hardwareCursorEnabled]);
482
- const slashSuggestions = useMemo(() => {
483
- if (!isSlashContext)
484
- return [];
485
- const commands = new Map();
486
- for (const command of localSlashCommands) {
487
- commands.set(command.name, command);
488
- }
489
- for (const command of slashRegistry.list()) {
490
- if (allowedSlashCommands && !allowedSlashCommands.has(command.name))
491
- continue;
492
- if (!commands.has(command.name))
493
- commands.set(command.name, command);
494
- }
495
- const commandSuggestions = [...commands.values()].map((command) => ({
496
- type: "command",
497
- name: command.name,
498
- description: command.description,
499
- }));
500
- const skillSuggestions = (allowedSlashCommands ? [] : (skillRegistry?.summaries() ?? [])).map((skill) => ({
501
- type: "skill",
502
- name: skill.name,
503
- description: skill.description,
504
- }));
505
- const all = [...commandSuggestions, ...skillSuggestions];
506
- return all.filter((item) => item.name.toLowerCase().startsWith(slashPrefix));
507
- }, [allowedSlashCommands, isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
508
- const knownSlashCommandNames = useMemo(() => {
509
- const names = new Set();
510
- for (const command of localSlashCommands)
511
- names.add(command.name);
512
- for (const command of slashRegistry.list()) {
513
- if (!allowedSlashCommands || allowedSlashCommands.has(command.name))
514
- names.add(command.name);
515
- }
516
- if (!allowedSlashCommands) {
517
- for (const skill of skillRegistry?.summaries() ?? [])
518
- names.add(skill.name);
519
- }
520
- return names;
521
- }, [allowedSlashCommands, skillRegistry, localSlashCommands]);
522
- const slashCommandHighlight = useMemo(() => resolveSlashCommandHighlightRange(text, knownSlashCommandNames), [text, knownSlashCommandNames]);
523
- const fileSuggestions = useMemo(() => {
524
- if (!atContext || !projectFiles)
525
- return [];
526
- return filterFileSuggestions(projectFiles, atContext.query, MAX_VISIBLE_SUGGESTIONS * 3);
527
- }, [atContext, projectFiles]);
528
- const mode = slashSuggestions.length > 0
529
- ? "slash"
530
- : atContext
531
- ? "file"
532
- : null;
533
- const activeCount = mode === "slash" ? slashSuggestions.length : mode === "file" ? fileSuggestions.length : 0;
534
- const navigable = activeCount > 0;
535
- const showSuggestions = mode !== null;
536
- let suggestionOffset = 0;
537
- if (navigable && activeCount > MAX_VISIBLE_SUGGESTIONS) {
538
- suggestionOffset = Math.min(Math.max(selectedIndex - Math.floor(MAX_VISIBLE_SUGGESTIONS / 2), 0), activeCount - MAX_VISIBLE_SUGGESTIONS);
539
- }
540
- const insertTextAtCursor = React.useCallback((insertion) => {
541
- if (!insertion)
542
- return;
543
- updateComposerBuffer((current) => insertComposerText(current, insertion));
544
- }, [updateComposerBuffer]);
545
- const addAttachment = React.useCallback((att) => {
546
- const base = imageLabelStartOverride ?? nextImageLabelStart;
547
- const index = attachmentCountRef.current;
548
- attachmentCountRef.current += 1;
549
- // Place the image label inline at the cursor so the reference appears where
550
- // it was pasted (not forced to the start), and moves with later edits. It is
551
- // stripped from the text on submit so the model still receives clean text.
552
- insertTextAtCursor(`${imageDisplayLabel(base + index)} `);
553
- ensureImageLabelStart();
554
- setAttachments((prev) => [...prev, att]);
555
- }, [ensureImageLabelStart, insertTextAtCursor, imageLabelStartOverride, nextImageLabelStart]);
556
- const notice = React.useCallback((msg) => {
557
- onPasteNotice?.(msg);
558
- }, [onPasteNotice]);
559
- // Empty paste is the common signal that the clipboard holds an image and the
560
- // terminal has nothing textual to deliver. Probe the clipboard; if it yields
561
- // an image, treat the paste as an image attachment. macOS only — Linux/Win
562
- // terminals don't reliably emit empty pastes on image-only clipboards.
563
- const tryClipboardImage = React.useCallback(async (expectedDraftEpoch) => {
564
- const { attachment, error } = await ingestClipboardImage();
565
- if (draftIdentityEpochRef.current !== expectedDraftEpoch)
566
- return false;
567
- if (attachment) {
568
- addAttachment(attachment);
569
- return true;
570
- }
571
- if (error && error !== "clipboard has no image") {
572
- notice(`image paste failed: ${error}`);
573
- }
574
- return false;
575
- }, [addAttachment, notice]);
576
- usePaste((pasted) => {
577
- const pasteOperationId = pasteOperationsRef.current.begin();
578
- const pasteDraftEpoch = draftIdentityEpochRef.current;
579
- // Clear the ref after React has committed the paste-driven setState.
580
- // setTimeout with 0 runs after the current discreteUpdates batch flushes.
581
- const clearPending = () => {
582
- setTimeout(() => {
583
- pasteOperationsRef.current.finish(pasteOperationId);
584
- }, 0);
585
- };
586
- // Strip orphaned focus-event tails that can appear if focus reporting
587
- // splits across the paste boundary. Bracketed paste also delivers line
588
- // breaks as bare CR on many terminals; left as-is, those CRs survive into
589
- // the rendered Text and the terminal interprets them as "return to column
590
- // 0", visually overwriting earlier characters even though the underlying
591
- // state still holds the full paste.
592
- const clean = pasted
593
- .replace(/\x1b\[I$/, "")
594
- .replace(/\x1b\[O$/, "")
595
- .replace(/\r\n?/g, "\n");
596
- if (!allowImageAttachments) {
597
- // A constrained text-only runtime must not probe the clipboard or read
598
- // pasted filesystem paths. Do not leak image path strings as chat text.
599
- const containsImagePath = clean.length > 0 && (bareImageFilenameFromPaste(clean) !== null
600
- || extractImagePathTokens(clean).length > 0);
601
- if (clean.length === 0 || containsImagePath) {
602
- notice("image attachments are disabled in this runtime session");
603
- }
604
- else {
605
- if (shouldCollapsePastedContent(clean)) {
606
- updateComposerBuffer((current) => insertComposerPaste(current, clean));
607
- }
608
- else {
609
- insertTextAtCursor(clean);
610
- }
611
- }
612
- clearPending();
613
- return;
614
- }
615
- // Empty paste on macOS usually means "Cmd+V with an image on the clipboard".
616
- if (clean.length === 0) {
617
- if (process.platform === "darwin") {
618
- void tryClipboardImage(pasteDraftEpoch).finally(clearPending);
619
- }
620
- else {
621
- clearPending();
622
- }
623
- return;
624
- }
625
- // Copying an image file in Finder pastes only the file's NAME while the
626
- // real bits stay on the system clipboard — attach from there. If the
627
- // clipboard turns out to hold no image, it was just text: insert it
628
- // quietly.
629
- const bareName = bareImageFilenameFromPaste(clean);
630
- if (bareName && process.platform === "darwin") {
631
- void tryClipboardImage(pasteDraftEpoch)
632
- .then((attached) => {
633
- if (draftIdentityEpochRef.current === pasteDraftEpoch && !attached) {
634
- insertTextAtCursor(clean);
635
- }
636
- })
637
- .finally(clearPending);
638
- return;
639
- }
640
- // Look for image paths inside the paste (drag-and-drop from Finder/
641
- // Nautilus/Explorer). Multi-selection can arrive newline- or
642
- // space-separated.
643
- const tokens = splitPastedPaths(clean);
644
- const imageTokens = tokens.filter(isImageFilePath);
645
- if (imageTokens.length === 0) {
646
- // Plain text paste — insert into the input at the cursor.
647
- if (shouldCollapsePastedContent(clean)) {
648
- updateComposerBuffer((current) => insertComposerPaste(current, clean));
649
- }
650
- else {
651
- insertTextAtCursor(clean);
652
- }
653
- clearPending();
654
- return;
655
- }
656
- const handle = async () => {
657
- const results = await Promise.all(imageTokens.map((t) => ingestImagePath(t)));
658
- if (draftIdentityEpochRef.current !== pasteDraftEpoch)
659
- return;
660
- const successful = [];
661
- const errors = [];
662
- for (let i = 0; i < results.length; i++) {
663
- const { attachment, error } = results[i];
664
- if (attachment) {
665
- successful.push(attachment);
666
- }
667
- else if (error) {
668
- errors.push(`${imageTokens[i]}: ${error}`);
669
- }
670
- }
671
- // macOS screenshot shortcut writes a TemporaryItems path into the
672
- // clipboard but the file may already be gone by the time we read it.
673
- // Fall back to the clipboard image when that happens.
674
- if (successful.length === 0 &&
675
- process.platform === "darwin" &&
676
- imageTokens.some(isScreenshotTempPath)) {
677
- const clipOk = await tryClipboardImage(pasteDraftEpoch);
678
- if (clipOk)
679
- return;
680
- }
681
- if (draftIdentityEpochRef.current !== pasteDraftEpoch)
682
- return;
683
- for (const att of successful)
684
- addAttachment(att);
685
- const nonImageLines = tokens.filter((t) => !isImageFilePath(t));
686
- if (successful.length > 0 && nonImageLines.length > 0) {
687
- insertTextAtCursor(nonImageLines.join("\n"));
688
- }
689
- else if (successful.length === 0) {
690
- // None resolved — fall back to treating the paste as text.
691
- insertTextAtCursor(clean);
692
- }
693
- for (const err of errors)
694
- notice(err);
695
- };
696
- void handle().finally(clearPending);
697
- }, { isActive: !disabled });
698
- const applyFileSuggestion = (selectedPath) => {
699
- if (!atContext)
700
- return;
701
- const insert = `@${selectedPath} `;
702
- updateComposerBuffer((current) => replaceComposerRange(current, atContext.start, atContext.end, insert));
703
- setSelectedIndex(0);
704
- };
705
- const submitInput = (submittedBuffer, target = "submit") => {
706
- const labelStartForSubmit = imageLabelStartOverride ?? nextImageLabelStart;
707
- const inlineLabels = attachments.map((_, index) => imageDisplayLabel(labelStartForSubmit + index));
708
- // Text-paste markers expand to their content (not replayable); image labels
709
- // are a composer-only affordance stripped here (replayable via attachments).
710
- const pasteExpanded = expandComposerBuffer(submittedBuffer);
711
- const expandedText = stripInlineImageLabels(pasteExpanded, inlineLabels);
712
- if (expandedText.trim().length === 0 && attachments.length === 0)
713
- return;
714
- const deliver = target === "queue" && onQueue ? onQueue : onSubmit;
715
- const payload = {
716
- // The pasted-content marker is a composer-only affordance. Submit the
717
- // fully-expanded text so the transcript shows what was actually sent —
718
- // the agent already receives the expanded text — rather than the marker.
719
- // `text` (model input) has image labels stripped; `displayText` keeps them
720
- // inline at their paste position so the transcript shows the image there.
721
- text: expandedText,
722
- ...(inlineLabels.length > 0 && pasteExpanded !== expandedText ? { displayText: pasteExpanded } : {}),
723
- images: attachments,
724
- imageDisplayStart: attachments.length > 0 ? (imageLabelStartOverride ?? nextImageLabelStart) : undefined,
725
- };
726
- const fingerprint = submitPayloadFingerprint(payload);
727
- if (submittedPayloadFingerprintRef.current === fingerprint)
728
- return;
729
- submittedPayloadFingerprintRef.current = fingerprint;
730
- deliver(payload);
731
- // Always record the submitted text so ↑/↓ can recall it. The full expanded
732
- // text is persisted (content is never lost); on recall, long text is
733
- // re-collapsed into a paste marker that still carries its full content.
734
- if (expandedText.trim().length > 0 || attachments.length > 0) {
735
- const historyEntry = {
736
- text: expandedText,
737
- images: attachments,
738
- ...(attachments.length > 0 ? { imageDisplayStart: imageLabelStartOverride ?? nextImageLabelStart } : {}),
739
- };
740
- setHistory((current) => {
741
- const nextHistory = pushHistoryEntry(current, historyEntry);
742
- if (nextHistory !== current) {
743
- appendHistoryEntry(historyEntry, { scope: historyScopeRef.current });
744
- }
745
- return nextHistory;
746
- });
747
- }
748
- updateComposerBuffer(() => createComposerBuffer());
749
- setSelectedIndex(0);
750
- setAttachments([]);
751
- attachmentCountRef.current = 0;
752
- setImageLabelStartOverride(null);
753
- setHistoryIndex(null);
754
- historyDraftRef.current = null;
755
- invalidateDraftAsyncWork();
756
- };
757
- const applySlashEnterAction = (submittedBuffer) => {
758
- const action = resolveSlashEnterAction(submittedBuffer.text, slashSuggestions, selectedIndex);
759
- if (action.kind === "submit") {
760
- submitInput(submittedBuffer);
761
- return true;
762
- }
763
- if (action.kind === "complete") {
764
- updateComposerBuffer((current) => replaceComposerRange(current, 0, current.text.length, action.text));
765
- setSelectedIndex(0);
766
- return true;
767
- }
768
- return false;
769
- };
770
- useInput((input, key) => {
771
- if (isKeyReleaseEvent(key))
772
- return;
773
- const strippedInput = stripTerminalMouseSequences(input);
774
- if (strippedInput !== input && !strippedInput) {
775
- return;
776
- }
777
- input = strippedInput;
778
- if (disabled)
779
- return;
780
- if (isCtrlCInput(input, key))
781
- return;
782
- if (process.env.BUBBLE_KEY_DEBUG) {
783
- try {
784
- appendFileSync("/tmp/bubble-key.log", JSON.stringify({
785
- t: new Date().toISOString(),
786
- input,
787
- inputCodes: [...input].map((ch) => ch.codePointAt(0)),
788
- key,
789
- }) + "\n");
790
- }
791
- catch { }
792
- }
793
- const enterIntent = resolveInkEnterIntent(input, key);
794
- if (shouldDeferComposerCommitWhilePastePending(pasteOperationsRef.current.hasPending, enterIntent, key)) {
795
- return;
796
- }
797
- if (enterIntent === "newline") {
798
- updateComposerBuffer((current) => insertComposerText(current, "\n"));
799
- setSelectedIndex(0);
800
- return;
801
- }
802
- if (enterIntent === "submit" && input && /[\r\n]/.test(input)) {
803
- const beforeReturn = input.split(/[\r\n]/)[0] ?? "";
804
- const nextBuffer = insertComposerText(composerBufferRef.current, beforeReturn);
805
- if (showSuggestions) {
806
- if (mode === "slash" && navigable && applySlashEnterAction(nextBuffer)) {
807
- return;
808
- }
809
- if (mode === "file") {
810
- if (navigable) {
811
- const suggestion = fileSuggestions[selectedIndex];
812
- if (suggestion)
813
- applyFileSuggestion(suggestion.path);
814
- }
815
- return;
816
- }
817
- }
818
- submitInput(nextBuffer);
819
- return;
820
- }
821
- const composerArrowDirection = composerVerticalArrowDirection(key);
822
- // Autocomplete navigation
823
- if (showSuggestions) {
824
- if (navigable && composerArrowDirection === "up") {
825
- setSelectedIndex((i) => (i - 1 + activeCount) % activeCount);
826
- return;
827
- }
828
- if (navigable && composerArrowDirection === "down") {
829
- setSelectedIndex((i) => (i + 1) % activeCount);
830
- return;
831
- }
832
- if (key.escape) {
833
- setSelectedIndex(0);
834
- return;
835
- }
836
- if (key.return || key.tab) {
837
- if (mode === "slash" && navigable) {
838
- if (key.return)
839
- applySlashEnterAction(composerBufferRef.current);
840
- if (key.tab) {
841
- const suggestion = slashSuggestions[selectedIndex];
842
- if (suggestion) {
843
- const newText = `/${suggestion.name} `;
844
- updateComposerBuffer((current) => replaceComposerRange(current, 0, current.text.length, newText));
845
- setSelectedIndex(0);
846
- }
847
- }
848
- return;
849
- }
850
- if (mode === "file") {
851
- if (navigable) {
852
- const suggestion = fileSuggestions[selectedIndex];
853
- if (suggestion)
854
- applyFileSuggestion(suggestion.path);
855
- }
856
- // Swallow Enter/Tab even when no matches to avoid accidental submit.
857
- return;
858
- }
859
- }
860
- }
861
- // While the agent runs, Tab queues the composer content for the next
862
- // turn (Enter steers — handled by the app-level submit routing).
863
- if (key.tab && !key.shift && onQueue && !showSuggestions) {
864
- submitInput(composerBufferRef.current, "queue");
865
- return;
866
- }
867
- if (enterIntent === "submit") {
868
- // A paste is still mid-flight — dropping this Enter avoids submitting
869
- // an input state that doesn't yet include the paste.
870
- submitInput(composerBufferRef.current);
871
- return;
872
- }
873
- const editAction = resolveComposerEditAction(input, key);
874
- if (editAction) {
875
- if (editAction === "word-left") {
876
- updateComposerBuffer((current) => moveComposerCursor(current, previousWordBoundary(current.text, current.cursor), "left"));
877
- }
878
- else if (editAction === "word-right") {
879
- updateComposerBuffer((current) => moveComposerCursor(current, nextWordBoundary(current.text, current.cursor), "right"));
880
- }
881
- else if (editAction === "line-start") {
882
- updateComposerBuffer((current) => moveComposerCursor(current, lineStartBoundary(current.text, current.cursor), "left"));
883
- }
884
- else if (editAction === "line-end") {
885
- updateComposerBuffer((current) => moveComposerCursor(current, lineEndBoundary(current.text, current.cursor), "right"));
886
- }
887
- else if (editAction === "delete-line-start") {
888
- updateComposerBuffer((current) => replaceComposerRange(current, lineStartBoundary(current.text, current.cursor), current.cursor));
889
- }
890
- else {
891
- updateComposerBuffer((current) => replaceComposerRange(current, current.cursor, lineEndBoundary(current.text, current.cursor)));
892
- }
893
- setSelectedIndex(0);
894
- return;
895
- }
896
- if (key.backspace) {
897
- if (composerBufferRef.current.cursor > 0) {
898
- updateComposerBuffer((current) => deleteComposerBackward(current));
899
- setSelectedIndex(0);
900
- }
901
- else if (attachmentCountRef.current > 0) {
902
- // Backspace at position 0 drops the most recent attachment so users
903
- // can undo a misfired paste without submitting the message.
904
- setAttachments((prev) => {
905
- const next = prev.slice(0, -1);
906
- if (next.length === 0)
907
- setImageLabelStartOverride(null);
908
- return next;
909
- });
910
- attachmentCountRef.current = Math.max(0, attachmentCountRef.current - 1);
911
- }
912
- return;
913
- }
914
- if (key.delete) {
915
- updateComposerBuffer((current) => deleteComposerForward(current));
916
- setSelectedIndex(0);
917
- return;
918
- }
919
- if (key.leftArrow) {
920
- updateComposerBuffer((current) => moveComposerCursor(current, current.cursor - 1, "left"));
921
- setSelectedIndex(0);
922
- return;
923
- }
924
- if (key.rightArrow) {
925
- updateComposerBuffer((current) => moveComposerCursor(current, current.cursor + 1, "right"));
926
- setSelectedIndex(0);
927
- return;
928
- }
929
- if (key.upArrow || key.downArrow) {
930
- if (composerArrowDirection) {
931
- classifyVerticalArrow(composerArrowDirection);
932
- }
933
- return;
934
- }
935
- // Ctrl/meta chords are app-level shortcuts (Ctrl+S selection mode,
936
- // Ctrl+O trace, Ctrl+R thinking, …) — never type their letter. Raw C0
937
- // control bytes (kitty protocol off) are equally not text.
938
- if (key.ctrl || key.meta)
939
- return;
940
- if (input) {
941
- const printable = input.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
942
- if (!printable)
943
- return;
944
- updateComposerBuffer((current) => insertComposerText(current, printable));
945
- setSelectedIndex(0);
946
- }
947
- });
948
- // Anchor the cursor directly to whichever line Box currently contains the
949
- // cursor. Its absolute yoga (top, left) IS the row the cursor should land
950
- // on — no manual border/row offsets that can drift one row off after a
951
- // layout shift.
952
- const cursorLineRef = useRef(null);
953
- const lastCursorRef = useRef(null);
954
- const previousOutputHeightRef = useRef(null);
955
- const previousViewportRowsRef = useRef(null);
956
- const previousInputFrameSignatureRef = useRef(null);
957
- const previousRowCompensationRef = useRef(0);
958
- const lastCursorResetEpochRef = useRef(null);
959
- const lastDraftEpochRef = useRef(null);
960
- const lastWidthRef = useRef(null);
961
- const { setCursorPosition } = useCursor();
962
- const { stdout } = useStdout();
963
- const [cursorTick, setCursorTick] = useState(0);
964
- useLayoutEffect(() => {
965
- const isInitialMount = lastCursorResetEpochRef.current === null;
966
- const shouldReset = !isInitialMount || cursorResetEpoch > 0;
967
- lastCursorResetEpochRef.current = cursorResetEpoch;
968
- if (!shouldReset)
969
- return;
970
- previousOutputHeightRef.current = null;
971
- previousViewportRowsRef.current = null;
972
- previousInputFrameSignatureRef.current = null;
973
- previousRowCompensationRef.current = 0;
974
- lastCursorRef.current = null;
975
- setCursorPosition(undefined);
976
- setCursorTick((t) => t + 1);
977
- }, [cursorResetEpoch, setCursorPosition]);
978
- useLayoutEffect(() => {
979
- if (lastDraftEpochRef.current === draftEpoch)
980
- return;
981
- lastDraftEpochRef.current = draftEpoch;
982
- if (draftText === undefined)
983
- return;
984
- updateComposerBuffer(() => createComposerBuffer(draftText));
985
- setSelectedIndex(0);
986
- setAttachments([]);
987
- attachmentCountRef.current = 0;
988
- setImageLabelStartOverride(null);
989
- setHistoryIndex(null);
990
- historyDraftRef.current = null;
991
- invalidateDraftAsyncWork();
992
- onDraftApplied?.();
993
- }, [draftEpoch, draftText, onDraftApplied]);
994
- useEffect(() => {
995
- if (text || attachments.length > 0) {
996
- submittedPayloadFingerprintRef.current = null;
997
- }
998
- }, [text, attachments.length]);
999
- // After a terminal resize the previous-frame refs reference a layout that no
1000
- // longer exists; carrying them forward makes `needsCursorRowCompensation`
1001
- // compare new yoga heights against stale ones and offsets the cursor by a
1002
- // row. Reset to a "no previous frame" state so the next layout effect treats
1003
- // the new width as a fresh start.
1004
- useLayoutEffect(() => {
1005
- if (lastWidthRef.current !== null && lastWidthRef.current !== width) {
1006
- previousOutputHeightRef.current = null;
1007
- previousViewportRowsRef.current = null;
1008
- previousInputFrameSignatureRef.current = null;
1009
- previousRowCompensationRef.current = 0;
1010
- lastCursorRef.current = null;
1011
- }
1012
- lastWidthRef.current = width;
1013
- }, [width]);
1014
- const contentWidth = Math.max(1, width - PADDING_X * 2);
1015
- const lineWidth = Math.max(1, contentWidth - PROMPT.length);
1016
- const imageLabelStart = imageLabelStartOverride ?? nextImageLabelStart;
1017
- const attachmentLabels = useMemo(() => attachments.map((_, index) => imageDisplayLabel(imageLabelStart + index)), [attachments, imageLabelStart]);
1018
- // Labels are normally inline in `text` at their paste position. Only labels
1019
- // that are NOT present inline (e.g. attachments restored from history) fall
1020
- // back to a leading prefix so they stay visible.
1021
- const unmarkedLabels = useMemo(() => attachmentLabels.filter((label) => !text.includes(label)), [attachmentLabels, text]);
1022
- const imageInlinePrefix = unmarkedLabels.length > 0 ? `${unmarkedLabels.join(" ")} ` : "";
1023
- const displayText = imageInlinePrefix + text;
1024
- const displayCursor = cursor + imageInlinePrefix.length;
1025
- const displayCursorToSourceCursor = (value) => Math.max(0, Math.min(text.length, value - imageInlinePrefix.length));
1026
- // Steady (non-blinking) cursor on purpose. The composer lives in the live
1027
- // (repainting) region; a blink timer would rewrite these rows ~twice a second
1028
- // even at idle, and the terminal drops any in-progress text selection every
1029
- // time the underlying cells are rewritten — which is why composer text could
1030
- // not be highlighted/copied while agent answers (committed to <Static>, never
1031
- // repainted) could. Keeping the cursor steady leaves the idle composer frame
1032
- // static, so native selection works. We still hide it while disabled.
1033
- useEffect(() => {
1034
- setSoftwareCursorVisible(!disabled);
1035
- }, [disabled]);
1036
- const visualLines = useMemo(() => computeVisualLines(displayText, lineWidth), [displayText, lineWidth]);
1037
- const { row: cursorVisualRow, col: cursorVisualCol } = cursorToVisual(visualLines, displayCursor);
1038
- // ---- Up/Down arrow handling in the composer ----
1039
- //
1040
- // Scrolling is the terminal's job now (native scrollback), so the composer
1041
- // owns Up/Down unconditionally: move within multiline input first, then
1042
- // browse prompt history at the top edge (Up → previous sent message) or the
1043
- // bottom edge (Down → next message, then back to the in-progress draft).
1044
- const performVerticalArrowRef = useRef(() => { });
1045
- performVerticalArrowRef.current = (direction) => {
1046
- // A paste and the following key can arrive in one stdin batch. Wait for
1047
- // that paste transition to commit before moving or snapshotting history.
1048
- if (pasteOperationsRef.current.hasPending)
1049
- return;
1050
- if (direction === "up") {
1051
- if (cursorVisualRow > 0) {
1052
- const requested = displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol));
1053
- updateComposerBuffer((current) => moveComposerCursor(current, requested, "nearest"));
1054
- return;
1055
- }
1056
- }
1057
- else {
1058
- if (cursorVisualRow < visualLines.length - 1) {
1059
- const requested = displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol));
1060
- updateComposerBuffer((current) => moveComposerCursor(current, requested, "nearest"));
1061
- return;
1062
- }
1063
- }
1064
- const result = stepComposerHistory({
1065
- buffer: composerBufferRef.current,
1066
- attachments,
1067
- imageLabelStartOverride,
1068
- historyIndex,
1069
- draftSnapshot: historyDraftRef.current,
1070
- }, history, direction);
1071
- if (result.changed) {
1072
- updateComposerBuffer(() => result.buffer);
1073
- setAttachments(result.attachments);
1074
- attachmentCountRef.current = result.attachments.length;
1075
- setImageLabelStartOverride(result.imageLabelStartOverride);
1076
- setHistoryIndex(result.historyIndex);
1077
- historyDraftRef.current = result.draftSnapshot;
1078
- setSelectedIndex(0);
1079
- invalidateDraftAsyncWork();
1080
- }
1081
- else if (direction === "down") {
1082
- // At the bottom edge with nothing newer in history: hand Down to the
1083
- // parent so focus can move into the subagent entry (Claude Code parity).
1084
- onArrowDownAtBottom?.();
1085
- }
1086
- };
1087
- const classifyVerticalArrow = (direction) => {
1088
- performVerticalArrowRef.current(direction);
1089
- };
1090
- const minVisibleLines = 1;
1091
- const totalLines = Math.max(visualLines.length, 1);
1092
- const visibleLines = Math.min(Math.max(totalLines, minVisibleLines), MAX_VISIBLE_LINES);
1093
- let scrollOffset = 0;
1094
- if (totalLines > visibleLines) {
1095
- scrollOffset = Math.min(Math.max(cursorVisualRow - Math.floor(visibleLines / 2), 0), totalLines - visibleLines);
1096
- }
1097
- const displayedLines = [];
1098
- const topPadLines = totalLines < visibleLines
1099
- ? Math.floor((visibleLines - totalLines) / 2)
1100
- : 0;
1101
- for (let i = 0; i < topPadLines; i++) {
1102
- displayedLines.push({ kind: "pad", key: `top-${i}` });
1103
- }
1104
- const contentLineCount = Math.min(totalLines, visibleLines - topPadLines);
1105
- for (let i = 0; i < contentLineCount; i++) {
1106
- const visualIdx = scrollOffset + i;
1107
- const vl = visualLines[visualIdx];
1108
- displayedLines.push({
1109
- kind: "content",
1110
- text: vl ? vl.text : "",
1111
- visualIdx,
1112
- });
1113
- }
1114
- while (displayedLines.length < visibleLines) {
1115
- displayedLines.push({ kind: "pad", key: `bottom-${displayedLines.length}` });
1116
- }
1117
- const hasMoreAbove = scrollOffset > 0;
1118
- const hasMoreBelow = scrollOffset + visibleLines < totalLines;
1119
- const inputFrameSignature = [
1120
- disabled ? "disabled" : "active",
1121
- text,
1122
- imageInlinePrefix,
1123
- scrollOffset.toString(),
1124
- visibleLines.toString(),
1125
- attachments.map((att) => `${att.filename ?? "clipboard"}:${att.bytes}`).join(","),
1126
- mode ?? "none",
1127
- selectedIndex.toString(),
1128
- suggestionOffset.toString(),
1129
- activeCount.toString(),
1130
- projectFiles?.length.toString() ?? "loading",
1131
- ].join("\u0000");
1132
- // Measure after yoga runs (useLayoutEffect fires after Ink's resetAfterCommit
1133
- // calls onComputeLayout). Push the new position into useCursor's ref and bump
1134
- // `cursorTick` to force one more render so useCursor's useInsertionEffect
1135
- // sees the fresh value and Ink emits a cursor-only update.
1136
- //
1137
- // While the input is disabled (agent is running, pickers open, etc.) the
1138
- // user can't type. Keeping the real cursor visible in the input makes it
1139
- // flicker every time streaming output above it re-lays out the frame, so
1140
- // we hide it entirely until input is active again.
1141
- useLayoutEffect(() => {
1142
- if (!hardwareCursorEnabled) {
1143
- if (lastCursorRef.current !== null) {
1144
- lastCursorRef.current = null;
1145
- }
1146
- setCursorPosition(undefined);
1147
- return;
1148
- }
1149
- let node = cursorLineRef.current ?? undefined;
1150
- if (!node?.yogaNode) {
1151
- if (disabled && lastCursorRef.current !== null) {
1152
- lastCursorRef.current = null;
1153
- setCursorTick((t) => t + 1);
1154
- }
1155
- setCursorPosition(undefined);
1156
- return;
1157
- }
1158
- let left = 0;
1159
- let top = 0;
1160
- let lastNode;
1161
- const trace = [];
1162
- while (node?.yogaNode) {
1163
- const layout = node.yogaNode.getComputedLayout();
1164
- left += layout.left;
1165
- top += layout.top;
1166
- if (process.env.BUBBLE_CURSOR_DEBUG) {
1167
- trace.push(`${node.nodeName}(+${layout.left},+${layout.top})`);
1168
- }
1169
- lastNode = node;
1170
- node = node.parentNode;
1171
- }
1172
- const rootHeight = lastNode?.yogaNode?.getComputedHeight() ?? 0;
1173
- // `||` on purpose: some ptys (and Bun on a detached tty) report rows as 0,
1174
- // which `??` would happily accept — and `rootHeight >= 0` then flags every
1175
- // frame as fullscreen, forcing a bogus +1 row compensation.
1176
- const viewportRows = stdout.rows || process.stdout.rows || 24;
1177
- const previousOutputHeight = previousOutputHeightRef.current;
1178
- // After a clear/sync frame, Ink's physical terminal cursor remains on the
1179
- // last rendered row even though log-update records an output string with a
1180
- // trailing newline. The forced cursor render that follows has the same
1181
- // visible frame, so keep the same row compensation until the input frame
1182
- // content or height actually changes.
1183
- const sameRenderedFrame = previousOutputHeight === rootHeight &&
1184
- previousViewportRowsRef.current === viewportRows &&
1185
- previousInputFrameSignatureRef.current === inputFrameSignature;
1186
- const rowCompensation = resolveCursorRowCompensation({
1187
- sameRenderedFrame,
1188
- previousRowCompensation: previousRowCompensationRef.current,
1189
- nextOutputHeight: rootHeight,
1190
- viewportRows,
1191
- previousOutputHeight,
1192
- });
1193
- previousOutputHeightRef.current = rootHeight;
1194
- previousViewportRowsRef.current = viewportRows;
1195
- previousInputFrameSignatureRef.current = inputFrameSignature;
1196
- previousRowCompensationRef.current = rowCompensation;
1197
- if (disabled) {
1198
- if (lastCursorRef.current !== null) {
1199
- lastCursorRef.current = null;
1200
- setCursorPosition(undefined);
1201
- setCursorTick((t) => t + 1);
1202
- }
1203
- return;
1204
- }
1205
- const next = {
1206
- x: left + PROMPT.length + cursorVisualCol,
1207
- y: top + rowCompensation,
1208
- };
1209
- if (process.env.BUBBLE_CURSOR_DEBUG) {
1210
- try {
1211
- appendFileSync("/tmp/bubble-cursor.log", `${new Date().toISOString()} row=${cursorVisualRow} col=${cursorVisualCol} -> x=${next.x} y=${next.y} (rootH=${rootHeight}, prevH=${previousOutputHeight ?? "none"}, vp=${viewportRows}, comp=${rowCompensation}) | ${trace.join(" < ")}\n`);
1212
- }
1213
- catch { }
1214
- }
1215
- const prev = lastCursorRef.current;
1216
- if (!prev || prev.x !== next.x || prev.y !== next.y) {
1217
- lastCursorRef.current = next;
1218
- setCursorPosition(next);
1219
- setCursorTick((t) => t + 1);
1220
- }
1221
- });
1222
- // Reference cursorTick so the effect re-runs on the forced render pass.
1223
- void cursorTick;
1224
- const cursorCellStyle = resolveSoftwareCursorCellStyle({
1225
- visible: softwareCursorVisible,
1226
- textColor: theme.inputText,
1227
- });
1228
- const moreBelow = totalLines - scrollOffset - visibleLines;
1229
- const filledLine = (value) => {
1230
- const visibleWidth = visualWidth(value);
1231
- return value + " ".repeat(Math.max(0, contentWidth - visibleWidth));
1232
- };
1233
- return (_jsxs(Box, { flexDirection: "column", width: width, backgroundColor: theme.background, children: [_jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "─".repeat(contentWidth) }) }), _jsxs(Box, { flexDirection: "column", paddingX: PADDING_X, width: width, backgroundColor: theme.background, children: [hasMoreAbove && (_jsx(Text, { color: theme.muted, dimColor: true, children: filledLine(` ↑ ${scrollOffset} more`) })), displayedLines.map((row) => {
1234
- if (row.kind === "pad") {
1235
- return (_jsx(Text, { children: " ".repeat(contentWidth) }, row.key));
1236
- }
1237
- const { text: line, visualIdx } = row;
1238
- const visualLine = visualLines[visualIdx];
1239
- const lineText = line.length === 0 ? " " : line;
1240
- const isFirst = visualIdx === 0;
1241
- const isCursorLine = visualIdx === cursorVisualRow;
1242
- const prompt = isFirst ? PROMPT : " ".repeat(PROMPT.length);
1243
- const highlight = imageInlinePrefix ? null : slashCommandHighlight;
1244
- const renderedSegments = splitComposerTextSegments({
1245
- text: lineText,
1246
- absStart: visualLine?.absStart ?? 0,
1247
- highlight,
1248
- cursorOffset: isCursorLine && !disabled
1249
- ? displayCursor - (visualLines[cursorVisualRow]?.absStart ?? 0)
1250
- : undefined,
1251
- });
1252
- const renderedLine = renderedSegments.map((segment) => segment.text).join("");
1253
- const fill = " ".repeat(Math.max(0, lineWidth - visualWidth(renderedLine)));
1254
- return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: theme.background, ref: isCursorLine
1255
- ? (el) => {
1256
- cursorLineRef.current = el;
1257
- }
1258
- : undefined, children: [_jsx(Text, { color: isFirst ? theme.accent : theme.inputText, children: prompt }), renderedSegments.map((segment, index) => {
1259
- if (segment.kind === "cursor") {
1260
- return (_jsx(Text, { inverse: cursorCellStyle.inverse, color: cursorCellStyle.color, children: segment.text }, index));
1261
- }
1262
- return (_jsx(Text, { color: segment.kind === "command" ? theme.accent : theme.inputText, bold: segment.kind === "command", children: segment.text }, index));
1263
- }), _jsx(Text, { children: fill })] }, visualIdx));
1264
- }), hasMoreBelow && (_jsx(Text, { color: theme.muted, dimColor: true, children: filledLine(` ↓ ${moreBelow} more`) }))] }), _jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "─".repeat(contentWidth) }) }), showSuggestions && mode === "slash" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 4, children: [slashSuggestions
1265
- .slice(suggestionOffset, suggestionOffset + MAX_VISIBLE_SUGGESTIONS)
1266
- .map((cmd, visibleIndex) => {
1267
- const i = suggestionOffset + visibleIndex;
1268
- const label = `/${cmd.name}`.padEnd(17);
1269
- const isSelected = i === selectedIndex;
1270
- return (_jsx(Box, { height: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: isSelected ? theme.accent : theme.muted, bold: isSelected, children: label }), cmd.type === "skill" && _jsx(Text, { color: theme.muted, children: " [skill]" }), _jsxs(Text, { dimColor: true, children: [" ", cmd.description] })] }) }, cmd.name));
1271
- }), slashSuggestions.length > MAX_VISIBLE_SUGGESTIONS && (_jsx(Text, { color: theme.muted, children: `Showing ${suggestionOffset + 1}-${Math.min(suggestionOffset + MAX_VISIBLE_SUGGESTIONS, slashSuggestions.length)} of ${slashSuggestions.length}` }))] })), showSuggestions && mode === "file" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [projectFiles === null && _jsx(Text, { dimColor: true, children: "Loading project files\u2026" }), projectFiles !== null && fileSuggestions.length === 0 && (_jsxs(Text, { dimColor: true, children: ["No files match \"", atContext?.query ?? "", "\""] })), fileSuggestions
1272
- .slice(suggestionOffset, suggestionOffset + MAX_VISIBLE_SUGGESTIONS)
1273
- .map((s, visibleIndex) => {
1274
- const i = suggestionOffset + visibleIndex;
1275
- const maxWidth = Math.max(10, Math.min(80, contentWidth - 2));
1276
- const label = s.path.length > maxWidth ? "…" + s.path.slice(-(maxWidth - 1)) : s.path;
1277
- return (_jsx(Box, { height: 1, children: i === selectedIndex ? (_jsx(Text, { color: theme.accent, bold: true, children: `› ${label}` })) : (_jsx(Text, { children: ` ${label}` })) }, s.path));
1278
- }), fileSuggestions.length > MAX_VISIBLE_SUGGESTIONS && (_jsx(Text, { color: theme.muted, children: `Showing ${suggestionOffset + 1}-${Math.min(suggestionOffset + MAX_VISIBLE_SUGGESTIONS, fileSuggestions.length)} of ${fileSuggestions.length}` }))] }))] }));
1279
- }