@opendash-ai/cli 0.1.0-beta.1

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 (1212) hide show
  1. package/Dockerfile +18 -0
  2. package/README.md +15 -0
  3. package/analysis.py +171 -0
  4. package/bin/opendash +180 -0
  5. package/bunfig.toml +7 -0
  6. package/drizzle.config.ts +10 -0
  7. package/git +0 -0
  8. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  9. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  10. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  11. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  12. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  13. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  14. package/migration/20260225215848_workspace/migration.sql +7 -0
  15. package/migration/20260225215848_workspace/snapshot.json +959 -0
  16. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  17. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  18. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  19. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  20. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  21. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  22. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  23. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  24. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  25. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  26. package/migration/20260323234822_events/migration.sql +13 -0
  27. package/migration/20260323234822_events/snapshot.json +1271 -0
  28. package/migration/20260410174513_workspace-name/migration.sql +16 -0
  29. package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
  30. package/migration/20260413175956_chief_energizer/migration.sql +13 -0
  31. package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
  32. package/migration/20260422160000_context_inheritance/migration.sql +3 -0
  33. package/migration/20260422170000_task_registry/migration.sql +18 -0
  34. package/migration/20260423145421_remove_session_entry/migration.sql +4 -0
  35. package/migration/20260515000000_actor_rename/migration.sql +7 -0
  36. package/migration/20260515010000_memory_fts/migration.sql +33 -0
  37. package/migration/20260515020000_user_task/migration.sql +29 -0
  38. package/migration/20260519000000_last_checkpoint_message_id/migration.sql +1 -0
  39. package/migration/20260521000000_message_agent_id/migration.sql +2 -0
  40. package/migration/20260521000100_actor_registry_v6/migration.sql +25 -0
  41. package/migration/20260521010000_memory_fts_v6/migration.sql +33 -0
  42. package/migration/20260521020000_memory_fts_triggers/migration.sql +17 -0
  43. package/migration/20260526000000_agent_id_main/migration.sql +14 -0
  44. package/migration/20260527000000_actor_lifecycle/migration.sql +8 -0
  45. package/migration/20260527000100_inbox/migration.sql +12 -0
  46. package/migration/20260529000000_task_todo_redesign/migration.sql +16 -0
  47. package/migration/20260603000000_task_in_progress_owner/migration.sql +1 -0
  48. package/migration/20260603000000_workflow_run/migration.sql +17 -0
  49. package/migration/20260604000000_workflow_script_sha/migration.sql +1 -0
  50. package/migration/20260608000000_claude_import/migration.sql +7 -0
  51. package/migration/20260608010000_claude_import_message_ids/migration.sql +1 -0
  52. package/migration/20260609000000_history_fts/migration.sql +29 -0
  53. package/migration/20260609230000_workflow_agent_timeout/migration.sql +1 -0
  54. package/migration/20260612000000_external_import/migration.sql +16 -0
  55. package/package.json +218 -0
  56. package/parsers-config.ts +290 -0
  57. package/script/build.ts +337 -0
  58. package/script/build_noinstall.ts +337 -0
  59. package/script/check-migrations.ts +16 -0
  60. package/script/dev.ts +38 -0
  61. package/script/fds-upload.ts +170 -0
  62. package/script/fix-node-pty.ts +28 -0
  63. package/script/generate.ts +23 -0
  64. package/script/postinstall.mjs +102 -0
  65. package/script/publish.ts +75 -0
  66. package/script/run-workspace-server +106 -0
  67. package/script/schema.ts +63 -0
  68. package/script/time.ts +6 -0
  69. package/script/trace-imports.ts +153 -0
  70. package/script/upgrade-opentui.ts +64 -0
  71. package/src/account/account.sql.ts +39 -0
  72. package/src/account/account.ts +456 -0
  73. package/src/account/repo.ts +166 -0
  74. package/src/account/schema.ts +99 -0
  75. package/src/account/url.ts +8 -0
  76. package/src/acp/README.md +174 -0
  77. package/src/acp/agent.ts +1787 -0
  78. package/src/acp/session.ts +116 -0
  79. package/src/acp/types.ts +24 -0
  80. package/src/actor/actor.sql.ts +38 -0
  81. package/src/actor/events.ts +67 -0
  82. package/src/actor/index.ts +2 -0
  83. package/src/actor/registry.ts +412 -0
  84. package/src/actor/return-header.ts +24 -0
  85. package/src/actor/schema.ts +47 -0
  86. package/src/actor/spawn-ref.ts +16 -0
  87. package/src/actor/spawn.ts +756 -0
  88. package/src/actor/turn.ts +49 -0
  89. package/src/actor/waiter.ts +166 -0
  90. package/src/agent/agent.ts +574 -0
  91. package/src/agent/config.ts +5 -0
  92. package/src/agent/generate.txt +75 -0
  93. package/src/agent/prompt/checkpoint-writer.txt +167 -0
  94. package/src/agent/prompt/compaction.txt +9 -0
  95. package/src/agent/prompt/distill.txt +199 -0
  96. package/src/agent/prompt/dream.txt +155 -0
  97. package/src/agent/prompt/explore.txt +18 -0
  98. package/src/agent/prompt/summary.txt +11 -0
  99. package/src/agent/prompt/title.txt +44 -0
  100. package/src/audio.d.ts +9 -0
  101. package/src/auth/index.ts +97 -0
  102. package/src/bus/bus-event.ts +33 -0
  103. package/src/bus/global.ts +12 -0
  104. package/src/bus/index.ts +193 -0
  105. package/src/cli/bootstrap.ts +33 -0
  106. package/src/cli/cmd/account.ts +258 -0
  107. package/src/cli/cmd/acp.ts +70 -0
  108. package/src/cli/cmd/agent.ts +248 -0
  109. package/src/cli/cmd/cmd.ts +7 -0
  110. package/src/cli/cmd/db.ts +120 -0
  111. package/src/cli/cmd/debug/agent.ts +192 -0
  112. package/src/cli/cmd/debug/config.ts +17 -0
  113. package/src/cli/cmd/debug/file.ts +100 -0
  114. package/src/cli/cmd/debug/index.ts +48 -0
  115. package/src/cli/cmd/debug/lsp.ts +61 -0
  116. package/src/cli/cmd/debug/ripgrep.ts +105 -0
  117. package/src/cli/cmd/debug/scrap.ts +16 -0
  118. package/src/cli/cmd/debug/skill.ts +23 -0
  119. package/src/cli/cmd/debug/snapshot.ts +53 -0
  120. package/src/cli/cmd/export.ts +306 -0
  121. package/src/cli/cmd/generate.ts +50 -0
  122. package/src/cli/cmd/github.ts +1647 -0
  123. package/src/cli/cmd/import.ts +208 -0
  124. package/src/cli/cmd/mcp.ts +812 -0
  125. package/src/cli/cmd/models.ts +88 -0
  126. package/src/cli/cmd/plug.ts +233 -0
  127. package/src/cli/cmd/pr.ts +138 -0
  128. package/src/cli/cmd/providers.ts +713 -0
  129. package/src/cli/cmd/run-completion.ts +77 -0
  130. package/src/cli/cmd/run.ts +694 -0
  131. package/src/cli/cmd/serve.ts +30 -0
  132. package/src/cli/cmd/session.ts +181 -0
  133. package/src/cli/cmd/stats.ts +413 -0
  134. package/src/cli/cmd/tui/app.tsx +1140 -0
  135. package/src/cli/cmd/tui/asset/TEN_VAD_LICENSE +12 -0
  136. package/src/cli/cmd/tui/asset/charge.wav +0 -0
  137. package/src/cli/cmd/tui/asset/pulse-a.wav +0 -0
  138. package/src/cli/cmd/tui/asset/pulse-b.wav +0 -0
  139. package/src/cli/cmd/tui/asset/pulse-c.wav +0 -0
  140. package/src/cli/cmd/tui/asset/ten_vad.wasm +0 -0
  141. package/src/cli/cmd/tui/asset/ten_vad_loader.js +30 -0
  142. package/src/cli/cmd/tui/attach.ts +84 -0
  143. package/src/cli/cmd/tui/component/background-image.tsx +150 -0
  144. package/src/cli/cmd/tui/component/bg-pulse.tsx +130 -0
  145. package/src/cli/cmd/tui/component/border.tsx +21 -0
  146. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  147. package/src/cli/cmd/tui/component/dialog-agreement.tsx +111 -0
  148. package/src/cli/cmd/tui/component/dialog-command.tsx +219 -0
  149. package/src/cli/cmd/tui/component/dialog-console-org.tsx +103 -0
  150. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +157 -0
  151. package/src/cli/cmd/tui/component/dialog-image-list.tsx +111 -0
  152. package/src/cli/cmd/tui/component/dialog-logo-design.tsx +37 -0
  153. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  154. package/src/cli/cmd/tui/component/dialog-mimo-login.tsx +165 -0
  155. package/src/cli/cmd/tui/component/dialog-model.tsx +335 -0
  156. package/src/cli/cmd/tui/component/dialog-provider.tsx +449 -0
  157. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +101 -0
  158. package/src/cli/cmd/tui/component/dialog-session-list.tsx +269 -0
  159. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  160. package/src/cli/cmd/tui/component/dialog-skill.tsx +42 -0
  161. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  162. package/src/cli/cmd/tui/component/dialog-status.tsx +170 -0
  163. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  164. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  165. package/src/cli/cmd/tui/component/dialog-token-plan.tsx +77 -0
  166. package/src/cli/cmd/tui/component/dialog-variant.tsx +39 -0
  167. package/src/cli/cmd/tui/component/dialog-workflows.tsx +51 -0
  168. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +289 -0
  169. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +81 -0
  170. package/src/cli/cmd/tui/component/dialog-worktree.tsx +90 -0
  171. package/src/cli/cmd/tui/component/error-component.tsx +92 -0
  172. package/src/cli/cmd/tui/component/logo.tsx +961 -0
  173. package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
  174. package/src/cli/cmd/tui/component/prompt/autocomplete-detect.ts +34 -0
  175. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +668 -0
  176. package/src/cli/cmd/tui/component/prompt/cwd.ts +0 -0
  177. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  178. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  179. package/src/cli/cmd/tui/component/prompt/index.tsx +1917 -0
  180. package/src/cli/cmd/tui/component/prompt/offset.ts +45 -0
  181. package/src/cli/cmd/tui/component/prompt/part.ts +36 -0
  182. package/src/cli/cmd/tui/component/prompt/shimmer.ts +62 -0
  183. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  184. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  185. package/src/cli/cmd/tui/component/starry-background.tsx +305 -0
  186. package/src/cli/cmd/tui/component/startup-loading.tsx +67 -0
  187. package/src/cli/cmd/tui/component/task-item.tsx +63 -0
  188. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  189. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  190. package/src/cli/cmd/tui/component/workflow-tree.tsx +177 -0
  191. package/src/cli/cmd/tui/config/cwd.ts +5 -0
  192. package/src/cli/cmd/tui/config/tui-migrate.ts +151 -0
  193. package/src/cli/cmd/tui/config/tui-schema.ts +38 -0
  194. package/src/cli/cmd/tui/config/tui.ts +219 -0
  195. package/src/cli/cmd/tui/context/args.tsx +16 -0
  196. package/src/cli/cmd/tui/context/directory.ts +15 -0
  197. package/src/cli/cmd/tui/context/event.ts +45 -0
  198. package/src/cli/cmd/tui/context/exit.tsx +65 -0
  199. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  200. package/src/cli/cmd/tui/context/keybind.tsx +105 -0
  201. package/src/cli/cmd/tui/context/kv.tsx +86 -0
  202. package/src/cli/cmd/tui/context/language.tsx +91 -0
  203. package/src/cli/cmd/tui/context/local.tsx +469 -0
  204. package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
  205. package/src/cli/cmd/tui/context/project.tsx +109 -0
  206. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  207. package/src/cli/cmd/tui/context/route.tsx +68 -0
  208. package/src/cli/cmd/tui/context/sdk.tsx +150 -0
  209. package/src/cli/cmd/tui/context/sync.tsx +884 -0
  210. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  211. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  212. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  213. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +230 -0
  214. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +230 -0
  215. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  216. package/src/cli/cmd/tui/context/theme/cobalt2.json +225 -0
  217. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  218. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  219. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  220. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  221. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  222. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  223. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  224. package/src/cli/cmd/tui/context/theme/lucent-orng.json +234 -0
  225. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  226. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  227. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  228. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  229. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  230. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  231. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  232. package/src/cli/cmd/tui/context/theme/opendash.json +243 -0
  233. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  234. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  235. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  236. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  237. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  238. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  239. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  240. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  241. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  242. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  243. package/src/cli/cmd/tui/context/theme.tsx +1299 -0
  244. package/src/cli/cmd/tui/context/thinking.ts +48 -0
  245. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  246. package/src/cli/cmd/tui/event.ts +56 -0
  247. package/src/cli/cmd/tui/feature-plugins/home/footer.tsx +93 -0
  248. package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +194 -0
  249. package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +54 -0
  250. package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +114 -0
  251. package/src/cli/cmd/tui/feature-plugins/sidebar/cwd.tsx +45 -0
  252. package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
  253. package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
  254. package/src/cli/cmd/tui/feature-plugins/sidebar/goal.tsx +84 -0
  255. package/src/cli/cmd/tui/feature-plugins/sidebar/instructions.tsx +54 -0
  256. package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
  257. package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +98 -0
  258. package/src/cli/cmd/tui/feature-plugins/sidebar/task.tsx +95 -0
  259. package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +51 -0
  260. package/src/cli/cmd/tui/feature-plugins/sidebar/tps.ts +31 -0
  261. package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +274 -0
  262. package/src/cli/cmd/tui/i18n/en.ts +470 -0
  263. package/src/cli/cmd/tui/i18n/es.ts +508 -0
  264. package/src/cli/cmd/tui/i18n/fr.ts +515 -0
  265. package/src/cli/cmd/tui/i18n/ja.ts +463 -0
  266. package/src/cli/cmd/tui/i18n/locales.ts +82 -0
  267. package/src/cli/cmd/tui/i18n/ru.ts +527 -0
  268. package/src/cli/cmd/tui/i18n/slash-command.ts +11 -0
  269. package/src/cli/cmd/tui/i18n/zh.ts +459 -0
  270. package/src/cli/cmd/tui/i18n/zht.ts +430 -0
  271. package/src/cli/cmd/tui/layer.ts +6 -0
  272. package/src/cli/cmd/tui/plugin/api.tsx +402 -0
  273. package/src/cli/cmd/tui/plugin/index.ts +3 -0
  274. package/src/cli/cmd/tui/plugin/internal.ts +35 -0
  275. package/src/cli/cmd/tui/plugin/runtime.ts +1057 -0
  276. package/src/cli/cmd/tui/plugin/slots.tsx +60 -0
  277. package/src/cli/cmd/tui/routes/home.tsx +172 -0
  278. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  279. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +116 -0
  280. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +47 -0
  281. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  282. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  283. package/src/cli/cmd/tui/routes/session/index.tsx +3087 -0
  284. package/src/cli/cmd/tui/routes/session/permission.tsx +697 -0
  285. package/src/cli/cmd/tui/routes/session/question.tsx +488 -0
  286. package/src/cli/cmd/tui/routes/session/sidebar.tsx +109 -0
  287. package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +143 -0
  288. package/src/cli/cmd/tui/thread.ts +323 -0
  289. package/src/cli/cmd/tui/ui/dialog-alert.tsx +61 -0
  290. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +95 -0
  291. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +223 -0
  292. package/src/cli/cmd/tui/ui/dialog-help.tsx +42 -0
  293. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +123 -0
  294. package/src/cli/cmd/tui/ui/dialog-select.tsx +467 -0
  295. package/src/cli/cmd/tui/ui/dialog.tsx +207 -0
  296. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  297. package/src/cli/cmd/tui/ui/spinner.ts +386 -0
  298. package/src/cli/cmd/tui/ui/toast.tsx +102 -0
  299. package/src/cli/cmd/tui/util/clipboard.ts +203 -0
  300. package/src/cli/cmd/tui/util/editor.ts +36 -0
  301. package/src/cli/cmd/tui/util/image-protocol.ts +35 -0
  302. package/src/cli/cmd/tui/util/index.ts +6 -0
  303. package/src/cli/cmd/tui/util/model.ts +23 -0
  304. package/src/cli/cmd/tui/util/pinyin.ts +20 -0
  305. package/src/cli/cmd/tui/util/provider-origin.ts +7 -0
  306. package/src/cli/cmd/tui/util/revert-diff.ts +18 -0
  307. package/src/cli/cmd/tui/util/scroll.ts +23 -0
  308. package/src/cli/cmd/tui/util/selection.ts +23 -0
  309. package/src/cli/cmd/tui/util/signal.ts +71 -0
  310. package/src/cli/cmd/tui/util/sound.ts +154 -0
  311. package/src/cli/cmd/tui/util/system-locale.ts +209 -0
  312. package/src/cli/cmd/tui/util/terminal.ts +110 -0
  313. package/src/cli/cmd/tui/util/transcript.ts +112 -0
  314. package/src/cli/cmd/tui/util/vad.ts +229 -0
  315. package/src/cli/cmd/tui/util/voice.ts +595 -0
  316. package/src/cli/cmd/tui/win32.ts +130 -0
  317. package/src/cli/cmd/tui/worker.ts +104 -0
  318. package/src/cli/cmd/uninstall.ts +351 -0
  319. package/src/cli/cmd/upgrade.ts +79 -0
  320. package/src/cli/cmd/web.ts +96 -0
  321. package/src/cli/effect/prompt.ts +25 -0
  322. package/src/cli/error.ts +82 -0
  323. package/src/cli/heap.ts +59 -0
  324. package/src/cli/i18n.ts +15 -0
  325. package/src/cli/logo.ts +52 -0
  326. package/src/cli/network.ts +68 -0
  327. package/src/cli/ui.ts +133 -0
  328. package/src/cli/upgrade.ts +41 -0
  329. package/src/command/index.ts +276 -0
  330. package/src/command/template/initialize.txt +66 -0
  331. package/src/command/template/review.txt +101 -0
  332. package/src/config/agent.ts +197 -0
  333. package/src/config/command.ts +69 -0
  334. package/src/config/compose.ts +26 -0
  335. package/src/config/config.ts +1052 -0
  336. package/src/config/console-state.ts +16 -0
  337. package/src/config/entry-name.ts +16 -0
  338. package/src/config/error.ts +21 -0
  339. package/src/config/formatter.ts +17 -0
  340. package/src/config/history.ts +21 -0
  341. package/src/config/index.ts +17 -0
  342. package/src/config/keybinds.ts +127 -0
  343. package/src/config/layout.ts +10 -0
  344. package/src/config/lsp.ts +45 -0
  345. package/src/config/managed.ts +70 -0
  346. package/src/config/markdown.ts +97 -0
  347. package/src/config/mcp.ts +172 -0
  348. package/src/config/model-id.ts +14 -0
  349. package/src/config/parse.ts +44 -0
  350. package/src/config/paths.ts +73 -0
  351. package/src/config/permission.ts +76 -0
  352. package/src/config/plugin.ts +88 -0
  353. package/src/config/provider.ts +118 -0
  354. package/src/config/server.ts +20 -0
  355. package/src/config/skills.ts +16 -0
  356. package/src/config/variable.ts +90 -0
  357. package/src/control-plane/adaptors/index.ts +52 -0
  358. package/src/control-plane/adaptors/worktree.ts +47 -0
  359. package/src/control-plane/dev/debug-workspace-plugin.ts +73 -0
  360. package/src/control-plane/schema.ts +19 -0
  361. package/src/control-plane/sse.ts +66 -0
  362. package/src/control-plane/types.ts +34 -0
  363. package/src/control-plane/util.ts +37 -0
  364. package/src/control-plane/workspace-context.ts +26 -0
  365. package/src/control-plane/workspace.sql.ts +17 -0
  366. package/src/control-plane/workspace.ts +615 -0
  367. package/src/effect/app-runtime.ts +146 -0
  368. package/src/effect/bootstrap-runtime.ts +33 -0
  369. package/src/effect/bridge.ts +48 -0
  370. package/src/effect/cross-spawn-spawner.ts +514 -0
  371. package/src/effect/index.ts +5 -0
  372. package/src/effect/instance-ref.ts +11 -0
  373. package/src/effect/instance-registry.ts +12 -0
  374. package/src/effect/instance-state.ts +81 -0
  375. package/src/effect/logger.ts +73 -0
  376. package/src/effect/memo-map.ts +3 -0
  377. package/src/effect/observability.ts +107 -0
  378. package/src/effect/run-service.ts +52 -0
  379. package/src/effect/runner.ts +210 -0
  380. package/src/effect/runtime.ts +19 -0
  381. package/src/env/index.ts +37 -0
  382. package/src/file/ignore.ts +81 -0
  383. package/src/file/index.ts +664 -0
  384. package/src/file/protected.ts +59 -0
  385. package/src/file/ripgrep.ts +485 -0
  386. package/src/file/watcher.ts +163 -0
  387. package/src/flag/flag.ts +212 -0
  388. package/src/format/formatter.ts +403 -0
  389. package/src/format/index.ts +203 -0
  390. package/src/git/index.ts +260 -0
  391. package/src/global/index.ts +54 -0
  392. package/src/history/backfill.ts +162 -0
  393. package/src/history/extract.ts +67 -0
  394. package/src/history/fts-query.ts +15 -0
  395. package/src/history/fts.sql.ts +20 -0
  396. package/src/history/index.ts +10 -0
  397. package/src/history/resolve.ts +65 -0
  398. package/src/history/service.ts +258 -0
  399. package/src/history/writer.ts +112 -0
  400. package/src/id/id.ts +87 -0
  401. package/src/ide/index.ts +73 -0
  402. package/src/inbox/inbox-ref.ts +38 -0
  403. package/src/inbox/inbox.sql.ts +26 -0
  404. package/src/inbox/inbox.ts +223 -0
  405. package/src/inbox/index.ts +3 -0
  406. package/src/inbox/render.ts +40 -0
  407. package/src/index.ts +261 -0
  408. package/src/installation/index.ts +362 -0
  409. package/src/installation/version.ts +32 -0
  410. package/src/intelligence/AUDIT.md +292 -0
  411. package/src/intelligence/ENTERPRISE-CAPABILITIES.md +317 -0
  412. package/src/intelligence/ENTERPRISE-REVIEW.md +284 -0
  413. package/src/intelligence/IMPLEMENTATION.md +214 -0
  414. package/src/intelligence/PHASE1.md +178 -0
  415. package/src/intelligence/PHASE2.5.md +162 -0
  416. package/src/intelligence/PHASE2.md +150 -0
  417. package/src/intelligence/PHASE3-REFINED.md +257 -0
  418. package/src/intelligence/PHASE3.md +188 -0
  419. package/src/intelligence/README.md +260 -0
  420. package/src/intelligence/analytics/SKILL.md +369 -0
  421. package/src/intelligence/analytics/adapters/index.ts +77 -0
  422. package/src/intelligence/analytics/benchmarks/observable-runner.ts +575 -0
  423. package/src/intelligence/analytics/benchmarks/runner.ts +758 -0
  424. package/src/intelligence/analytics/engine/index.ts +152 -0
  425. package/src/intelligence/analytics/hook.ts +154 -0
  426. package/src/intelligence/analytics/index.ts +194 -0
  427. package/src/intelligence/analytics/loader/index.ts +502 -0
  428. package/src/intelligence/analytics/memory/index.ts +455 -0
  429. package/src/intelligence/analytics/orchestrator/index.ts +329 -0
  430. package/src/intelligence/analytics/planner/index.ts +188 -0
  431. package/src/intelligence/analytics/prompt/index.ts +449 -0
  432. package/src/intelligence/analytics/reasoning/enterprise-capabilities.ts +534 -0
  433. package/src/intelligence/analytics/reasoning/enterprise-prompt.ts +321 -0
  434. package/src/intelligence/analytics/reasoning/index.ts +267 -0
  435. package/src/intelligence/analytics/reasoning/test.ts +166 -0
  436. package/src/intelligence/analytics/reasoning/visible-reasoning.ts +609 -0
  437. package/src/intelligence/analytics/registry/index.ts +176 -0
  438. package/src/intelligence/analytics/router/index.ts +352 -0
  439. package/src/intelligence/analytics/test.ts +127 -0
  440. package/src/intelligence/analytics/workflow/index.ts +375 -0
  441. package/src/intelligence/index.ts +50 -0
  442. package/src/lsp/client.ts +249 -0
  443. package/src/lsp/diagnostic.ts +29 -0
  444. package/src/lsp/index.ts +3 -0
  445. package/src/lsp/language.ts +120 -0
  446. package/src/lsp/launch.ts +21 -0
  447. package/src/lsp/lsp.ts +519 -0
  448. package/src/lsp/server.ts +1956 -0
  449. package/src/mcp/auth.ts +144 -0
  450. package/src/mcp/index.ts +939 -0
  451. package/src/mcp/oauth-callback.ts +236 -0
  452. package/src/mcp/oauth-provider.ts +214 -0
  453. package/src/memory/fts-query.ts +37 -0
  454. package/src/memory/fts.sql.ts +19 -0
  455. package/src/memory/index.ts +1 -0
  456. package/src/memory/paths.ts +116 -0
  457. package/src/memory/reconcile.ts +144 -0
  458. package/src/memory/service.ts +144 -0
  459. package/src/metrics/client.ts +40 -0
  460. package/src/metrics/event.ts +43 -0
  461. package/src/metrics/index.ts +5 -0
  462. package/src/metrics/installation.ts +18 -0
  463. package/src/metrics/subscriber.ts +58 -0
  464. package/src/metrics/util.ts +9 -0
  465. package/src/node.ts +6 -0
  466. package/src/npm/config.ts +0 -0
  467. package/src/npm/index.ts +293 -0
  468. package/src/npmcli-config.d.ts +43 -0
  469. package/src/patch/index.ts +680 -0
  470. package/src/permission/arity.ts +163 -0
  471. package/src/permission/evaluate.ts +15 -0
  472. package/src/permission/index.ts +382 -0
  473. package/src/permission/schema.ts +17 -0
  474. package/src/plugin/checkpoint-splitover.ts +60 -0
  475. package/src/plugin/cloudflare.ts +76 -0
  476. package/src/plugin/codex.ts +611 -0
  477. package/src/plugin/github-copilot/copilot.ts +368 -0
  478. package/src/plugin/github-copilot/models.ts +153 -0
  479. package/src/plugin/index.ts +637 -0
  480. package/src/plugin/install.ts +439 -0
  481. package/src/plugin/loader.ts +216 -0
  482. package/src/plugin/matcher.ts +33 -0
  483. package/src/plugin/meta.ts +188 -0
  484. package/src/plugin/mimo.ts +296 -0
  485. package/src/plugin/shared.ts +323 -0
  486. package/src/plugin/subagent-progress-checker.ts +147 -0
  487. package/src/project/bootstrap.ts +59 -0
  488. package/src/project/index.ts +2 -0
  489. package/src/project/instance.ts +215 -0
  490. package/src/project/project-id.ts +48 -0
  491. package/src/project/project.sql.ts +16 -0
  492. package/src/project/project.ts +522 -0
  493. package/src/project/schema.ts +15 -0
  494. package/src/project/vcs.ts +227 -0
  495. package/src/project/workspace-trust.ts +67 -0
  496. package/src/provider/auth.ts +234 -0
  497. package/src/provider/error.ts +216 -0
  498. package/src/provider/index.ts +5 -0
  499. package/src/provider/models.ts +180 -0
  500. package/src/provider/provider.ts +1788 -0
  501. package/src/provider/schema.ts +36 -0
  502. package/src/provider/sdk/copilot/README.md +5 -0
  503. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  504. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  505. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  506. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  507. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  508. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  509. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  510. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  511. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  512. package/src/provider/sdk/copilot/index.ts +2 -0
  513. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  514. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
  515. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  516. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  517. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  518. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
  519. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1770 -0
  520. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
  521. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  522. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
  523. package/src/provider/sdk/copilot/responses/tool/file-search.ts +127 -0
  524. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +114 -0
  525. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +64 -0
  526. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
  527. package/src/provider/sdk/copilot/responses/tool/web-search.ts +102 -0
  528. package/src/provider/transform.ts +1376 -0
  529. package/src/pty/index.ts +364 -0
  530. package/src/pty/pty.bun.ts +26 -0
  531. package/src/pty/pty.node.ts +27 -0
  532. package/src/pty/pty.ts +25 -0
  533. package/src/pty/schema.ts +17 -0
  534. package/src/question/index.ts +252 -0
  535. package/src/question/schema.ts +17 -0
  536. package/src/server/adapter.bun.ts +40 -0
  537. package/src/server/adapter.node.ts +66 -0
  538. package/src/server/adapter.ts +21 -0
  539. package/src/server/auth.ts +16 -0
  540. package/src/server/error.ts +53 -0
  541. package/src/server/event.ts +7 -0
  542. package/src/server/fence.ts +81 -0
  543. package/src/server/mdns.ts +60 -0
  544. package/src/server/middleware.ts +94 -0
  545. package/src/server/projectors.ts +28 -0
  546. package/src/server/proxy.ts +171 -0
  547. package/src/server/pty-ticket.ts +42 -0
  548. package/src/server/rate-limit.ts +38 -0
  549. package/src/server/routes/control/index.ts +160 -0
  550. package/src/server/routes/control/workspace.ts +203 -0
  551. package/src/server/routes/global.ts +367 -0
  552. package/src/server/routes/instance/bash-interactive.ts +82 -0
  553. package/src/server/routes/instance/config.ts +89 -0
  554. package/src/server/routes/instance/event.ts +108 -0
  555. package/src/server/routes/instance/experimental.ts +408 -0
  556. package/src/server/routes/instance/file.ts +190 -0
  557. package/src/server/routes/instance/httpapi/config.ts +51 -0
  558. package/src/server/routes/instance/httpapi/permission.ts +72 -0
  559. package/src/server/routes/instance/httpapi/project.ts +62 -0
  560. package/src/server/routes/instance/httpapi/provider.ts +142 -0
  561. package/src/server/routes/instance/httpapi/question.ts +121 -0
  562. package/src/server/routes/instance/httpapi/server.ts +153 -0
  563. package/src/server/routes/instance/index.ts +301 -0
  564. package/src/server/routes/instance/mcp.ts +260 -0
  565. package/src/server/routes/instance/middleware.ts +44 -0
  566. package/src/server/routes/instance/permission.ts +73 -0
  567. package/src/server/routes/instance/project.ts +122 -0
  568. package/src/server/routes/instance/provider.ts +158 -0
  569. package/src/server/routes/instance/pty.ts +302 -0
  570. package/src/server/routes/instance/question.ts +162 -0
  571. package/src/server/routes/instance/session.ts +1328 -0
  572. package/src/server/routes/instance/sync.ts +143 -0
  573. package/src/server/routes/instance/trace.ts +59 -0
  574. package/src/server/routes/instance/tui.ts +384 -0
  575. package/src/server/routes/instance/workflows.ts +142 -0
  576. package/src/server/routes/ui.ts +37 -0
  577. package/src/server/server.ts +146 -0
  578. package/src/server/workspace.ts +122 -0
  579. package/src/session/auto-dream.ts +123 -0
  580. package/src/session/boundary.ts +77 -0
  581. package/src/session/budgeted-read.ts +118 -0
  582. package/src/session/checkpoint-align.ts +29 -0
  583. package/src/session/checkpoint-context.ts +36 -0
  584. package/src/session/checkpoint-paths.ts +86 -0
  585. package/src/session/checkpoint-progress-reconcile.ts +111 -0
  586. package/src/session/checkpoint-retry.ts +192 -0
  587. package/src/session/checkpoint-templates.ts +114 -0
  588. package/src/session/checkpoint-validator.ts +259 -0
  589. package/src/session/checkpoint.ts +1560 -0
  590. package/src/session/classify.ts +117 -0
  591. package/src/session/claude-import.ts +381 -0
  592. package/src/session/codex-import.ts +416 -0
  593. package/src/session/compaction.ts +545 -0
  594. package/src/session/external-import.sql.ts +18 -0
  595. package/src/session/external-import.ts +136 -0
  596. package/src/session/goal.ts +232 -0
  597. package/src/session/index.ts +1 -0
  598. package/src/session/instruction.ts +276 -0
  599. package/src/session/last-message-info.ts +32 -0
  600. package/src/session/llm-request-prefix.ts +82 -0
  601. package/src/session/llm.ts +734 -0
  602. package/src/session/max-mode.ts +410 -0
  603. package/src/session/message-v2.ts +1138 -0
  604. package/src/session/message.ts +191 -0
  605. package/src/session/opendash-import.ts +281 -0
  606. package/src/session/overflow.ts +53 -0
  607. package/src/session/prefix-capture-ref.ts +48 -0
  608. package/src/session/processor.ts +983 -0
  609. package/src/session/projectors.ts +137 -0
  610. package/src/session/prompt/anthropic.txt +177 -0
  611. package/src/session/prompt/beast.txt +186 -0
  612. package/src/session/prompt/build-switch.txt +5 -0
  613. package/src/session/prompt/codex.txt +108 -0
  614. package/src/session/prompt/compose.txt +119 -0
  615. package/src/session/prompt/copilot-gpt-5.txt +173 -0
  616. package/src/session/prompt/deepseek.txt +156 -0
  617. package/src/session/prompt/default.old.txt +151 -0
  618. package/src/session/prompt/default.txt +209 -0
  619. package/src/session/prompt/gemini.txt +186 -0
  620. package/src/session/prompt/glm.txt +73 -0
  621. package/src/session/prompt/gpt.txt +134 -0
  622. package/src/session/prompt/kimi.txt +117 -0
  623. package/src/session/prompt/max-steps.txt +16 -0
  624. package/src/session/prompt/minimax.txt +165 -0
  625. package/src/session/prompt/text-loop-recovery.ts +40 -0
  626. package/src/session/prompt/text-ngram-detection.ts +116 -0
  627. package/src/session/prompt/trinity.txt +128 -0
  628. package/src/session/prompt.ts +3878 -0
  629. package/src/session/prune.ts +481 -0
  630. package/src/session/retry.ts +178 -0
  631. package/src/session/revert.ts +161 -0
  632. package/src/session/run-state.ts +135 -0
  633. package/src/session/schema.ts +36 -0
  634. package/src/session/session.sql.ts +110 -0
  635. package/src/session/session.ts +908 -0
  636. package/src/session/status.ts +89 -0
  637. package/src/session/summary.ts +163 -0
  638. package/src/session/system.ts +97 -0
  639. package/src/session/todo.ts +77 -0
  640. package/src/session/trajectory.ts +98 -0
  641. package/src/share/index.ts +2 -0
  642. package/src/share/session.ts +57 -0
  643. package/src/share/share-next.ts +381 -0
  644. package/src/share/share.sql.ts +13 -0
  645. package/src/shell/shell.ts +124 -0
  646. package/src/skill/builtin/.bundle/self-extend/SKILL.md +131 -0
  647. package/src/skill/builtin/.bundle/self-extend/reference/hook-api.md +242 -0
  648. package/src/skill/builtin/.bundle/self-extend/reference/skill-api.md +114 -0
  649. package/src/skill/builtin/.bundle/self-extend/reference/tool-api.md +115 -0
  650. package/src/skill/builtin/.bundle/self-extend/reference/tui-api.md +258 -0
  651. package/src/skill/builtin/bundle.macro.ts +30 -0
  652. package/src/skill/builtin/extract.ts +41 -0
  653. package/src/skill/compose/.bundle/ask/SKILL.md +58 -0
  654. package/src/skill/compose/.bundle/brainstorm/SKILL.md +200 -0
  655. package/src/skill/compose/.bundle/brainstorm/scripts/frame-template.html +214 -0
  656. package/src/skill/compose/.bundle/brainstorm/scripts/helper.js +88 -0
  657. package/src/skill/compose/.bundle/brainstorm/scripts/server.cjs +354 -0
  658. package/src/skill/compose/.bundle/brainstorm/scripts/start-server.sh +148 -0
  659. package/src/skill/compose/.bundle/brainstorm/scripts/stop-server.sh +56 -0
  660. package/src/skill/compose/.bundle/brainstorm/spec-document-reviewer-prompt.md +50 -0
  661. package/src/skill/compose/.bundle/brainstorm/visual-companion.md +258 -0
  662. package/src/skill/compose/.bundle/debug/CREATION-LOG.md +119 -0
  663. package/src/skill/compose/.bundle/debug/SKILL.md +297 -0
  664. package/src/skill/compose/.bundle/debug/condition-based-waiting-example.ts +158 -0
  665. package/src/skill/compose/.bundle/debug/condition-based-waiting.md +106 -0
  666. package/src/skill/compose/.bundle/debug/defense-in-depth.md +122 -0
  667. package/src/skill/compose/.bundle/debug/find-polluter.sh +63 -0
  668. package/src/skill/compose/.bundle/debug/root-cause-tracing.md +144 -0
  669. package/src/skill/compose/.bundle/debug/test-academic.md +14 -0
  670. package/src/skill/compose/.bundle/debug/test-pressure-1.md +58 -0
  671. package/src/skill/compose/.bundle/debug/test-pressure-2.md +68 -0
  672. package/src/skill/compose/.bundle/debug/test-pressure-3.md +69 -0
  673. package/src/skill/compose/.bundle/execute/SKILL.md +71 -0
  674. package/src/skill/compose/.bundle/feedback/SKILL.md +214 -0
  675. package/src/skill/compose/.bundle/merge/SKILL.md +252 -0
  676. package/src/skill/compose/.bundle/new-skill/SKILL.md +211 -0
  677. package/src/skill/compose/.bundle/parallel/SKILL.md +168 -0
  678. package/src/skill/compose/.bundle/plan/SKILL.md +183 -0
  679. package/src/skill/compose/.bundle/plan/plan-document-reviewer-prompt.md +50 -0
  680. package/src/skill/compose/.bundle/report/SKILL.md +180 -0
  681. package/src/skill/compose/.bundle/review/SKILL.md +104 -0
  682. package/src/skill/compose/.bundle/review/code-reviewer.md +178 -0
  683. package/src/skill/compose/.bundle/subagent/SKILL.md +325 -0
  684. package/src/skill/compose/.bundle/subagent/code-quality-reviewer-prompt.md +26 -0
  685. package/src/skill/compose/.bundle/subagent/implementer-prompt.md +128 -0
  686. package/src/skill/compose/.bundle/subagent/spec-reviewer-prompt.md +118 -0
  687. package/src/skill/compose/.bundle/tdd/SKILL.md +360 -0
  688. package/src/skill/compose/.bundle/tdd/testing-anti-patterns.md +299 -0
  689. package/src/skill/compose/.bundle/verify/SKILL.md +140 -0
  690. package/src/skill/compose/.bundle/worktree/SKILL.md +234 -0
  691. package/src/skill/compose/LICENSE-karpathy +28 -0
  692. package/src/skill/compose/LICENSE-superpowers +26 -0
  693. package/src/skill/compose/bundle.macro.ts +30 -0
  694. package/src/skill/compose/extract.ts +85 -0
  695. package/src/skill/discovery.ts +116 -0
  696. package/src/skill/index.ts +328 -0
  697. package/src/snapshot/index.ts +777 -0
  698. package/src/sql.d.ts +4 -0
  699. package/src/storage/db.bun.ts +8 -0
  700. package/src/storage/db.node.ts +8 -0
  701. package/src/storage/db.ts +172 -0
  702. package/src/storage/index.ts +26 -0
  703. package/src/storage/json-migration.ts +426 -0
  704. package/src/storage/read-sqlite.bun.ts +11 -0
  705. package/src/storage/read-sqlite.node.ts +13 -0
  706. package/src/storage/read-sqlite.ts +10 -0
  707. package/src/storage/schema.sql.ts +10 -0
  708. package/src/storage/schema.ts +7 -0
  709. package/src/storage/storage.ts +331 -0
  710. package/src/sync/README.md +179 -0
  711. package/src/sync/event.sql.ts +16 -0
  712. package/src/sync/index.ts +278 -0
  713. package/src/sync/schema.ts +14 -0
  714. package/src/task/events.ts +28 -0
  715. package/src/task/gate-state.ts +54 -0
  716. package/src/task/gate.ts +116 -0
  717. package/src/task/index.ts +1 -0
  718. package/src/task/registry.ts +394 -0
  719. package/src/task/schema.ts +43 -0
  720. package/src/task/task.sql.ts +50 -0
  721. package/src/team/events.ts +22 -0
  722. package/src/team/index.ts +113 -0
  723. package/src/team/schema.ts +31 -0
  724. package/src/temporary.ts +33 -0
  725. package/src/tool/actor.shell.txt +72 -0
  726. package/src/tool/actor.ts +804 -0
  727. package/src/tool/actor.txt +103 -0
  728. package/src/tool/apply_patch.ts +308 -0
  729. package/src/tool/apply_patch.txt +33 -0
  730. package/src/tool/bash-interactive.ts +183 -0
  731. package/src/tool/bash.ts +722 -0
  732. package/src/tool/bash.txt +123 -0
  733. package/src/tool/bash_token_efficient_pipeline.ts +189 -0
  734. package/src/tool/change-directory.ts +91 -0
  735. package/src/tool/codesearch.ts +63 -0
  736. package/src/tool/codesearch.txt +12 -0
  737. package/src/tool/edit.ts +752 -0
  738. package/src/tool/edit.txt +10 -0
  739. package/src/tool/external-directory.ts +132 -0
  740. package/src/tool/glob.ts +100 -0
  741. package/src/tool/glob.txt +6 -0
  742. package/src/tool/grep.ts +145 -0
  743. package/src/tool/grep.txt +8 -0
  744. package/src/tool/history.ts +146 -0
  745. package/src/tool/history.txt +17 -0
  746. package/src/tool/index.ts +4 -0
  747. package/src/tool/invalid.ts +20 -0
  748. package/src/tool/invocation-style.ts +17 -0
  749. package/src/tool/lsp.ts +91 -0
  750. package/src/tool/lsp.txt +19 -0
  751. package/src/tool/mcp-exa.ts +78 -0
  752. package/src/tool/memory-path-guard.ts +162 -0
  753. package/src/tool/memory.ts +81 -0
  754. package/src/tool/memory.txt +69 -0
  755. package/src/tool/multiedit.ts +54 -0
  756. package/src/tool/multiedit.txt +41 -0
  757. package/src/tool/notebook-edit.ts +225 -0
  758. package/src/tool/notebook-edit.txt +10 -0
  759. package/src/tool/plan-enter.txt +16 -0
  760. package/src/tool/plan-exit.txt +14 -0
  761. package/src/tool/plan.ts +179 -0
  762. package/src/tool/question.ts +67 -0
  763. package/src/tool/question.txt +10 -0
  764. package/src/tool/read-state.ts +44 -0
  765. package/src/tool/read.ts +327 -0
  766. package/src/tool/read.txt +14 -0
  767. package/src/tool/recoverable.ts +35 -0
  768. package/src/tool/registry.ts +429 -0
  769. package/src/tool/schema.ts +17 -0
  770. package/src/tool/session-cwd.ts +35 -0
  771. package/src/tool/shell-tokenize.ts +374 -0
  772. package/src/tool/shell-wrap.ts +235 -0
  773. package/src/tool/skill.ts +76 -0
  774. package/src/tool/skill.txt +5 -0
  775. package/src/tool/task.shell.txt +57 -0
  776. package/src/tool/task.ts +456 -0
  777. package/src/tool/task.txt +56 -0
  778. package/src/tool/tool.ts +166 -0
  779. package/src/tool/truncate.ts +201 -0
  780. package/src/tool/truncation-dir.ts +4 -0
  781. package/src/tool/webfetch.ts +208 -0
  782. package/src/tool/webfetch.txt +13 -0
  783. package/src/tool/websearch/index.ts +104 -0
  784. package/src/tool/websearch/mimo.ts +118 -0
  785. package/src/tool/websearch/websearch.txt +14 -0
  786. package/src/tool/workflow.ts +357 -0
  787. package/src/tool/workflow.txt +25 -0
  788. package/src/tool/write.ts +88 -0
  789. package/src/tool/write.txt +10 -0
  790. package/src/util/abort.ts +35 -0
  791. package/src/util/archive.ts +15 -0
  792. package/src/util/color.ts +17 -0
  793. package/src/util/data-url.ts +9 -0
  794. package/src/util/defer.ts +10 -0
  795. package/src/util/effect-http-client.ts +11 -0
  796. package/src/util/effect-zod.ts +367 -0
  797. package/src/util/env-info.ts +62 -0
  798. package/src/util/error.ts +78 -0
  799. package/src/util/filesystem.ts +243 -0
  800. package/src/util/fn.ts +21 -0
  801. package/src/util/format.ts +20 -0
  802. package/src/util/iife.ts +3 -0
  803. package/src/util/index.ts +14 -0
  804. package/src/util/keybind.ts +101 -0
  805. package/src/util/lazy.ts +18 -0
  806. package/src/util/local-context.ts +23 -0
  807. package/src/util/locale.ts +79 -0
  808. package/src/util/lock.ts +96 -0
  809. package/src/util/log.ts +234 -0
  810. package/src/util/media.ts +26 -0
  811. package/src/util/network.ts +9 -0
  812. package/src/util/opendash-process.ts +24 -0
  813. package/src/util/process.ts +174 -0
  814. package/src/util/provider-priority.ts +48 -0
  815. package/src/util/queue.ts +60 -0
  816. package/src/util/record.ts +3 -0
  817. package/src/util/rpc.ts +64 -0
  818. package/src/util/schema.ts +53 -0
  819. package/src/util/scrap.ts +10 -0
  820. package/src/util/signal.ts +12 -0
  821. package/src/util/ssrf.ts +116 -0
  822. package/src/util/timeout.ts +14 -0
  823. package/src/util/token.ts +5 -0
  824. package/src/util/tool-compat.ts +144 -0
  825. package/src/util/update-schema.ts +13 -0
  826. package/src/util/which.ts +14 -0
  827. package/src/util/wildcard.ts +57 -0
  828. package/src/workflow/builtin/compose.js +749 -0
  829. package/src/workflow/builtin/deep-research.js +398 -0
  830. package/src/workflow/builtin.ts +59 -0
  831. package/src/workflow/events.ts +72 -0
  832. package/src/workflow/meta.ts +335 -0
  833. package/src/workflow/persistence.ts +312 -0
  834. package/src/workflow/resolve.ts +45 -0
  835. package/src/workflow/runtime-ref.ts +18 -0
  836. package/src/workflow/runtime.ts +1447 -0
  837. package/src/workflow/sandbox.ts +286 -0
  838. package/src/workflow/workflow.sql.ts +31 -0
  839. package/src/workflow/workspace.ts +69 -0
  840. package/src/worktree/index.ts +629 -0
  841. package/sst-env.d.ts +10 -0
  842. package/test/AGENTS.md +133 -0
  843. package/test/account/repo.test.ts +352 -0
  844. package/test/account/service.test.ts +456 -0
  845. package/test/acp/agent-interface.test.ts +51 -0
  846. package/test/acp/event-subscription.test.ts +725 -0
  847. package/test/actor/cancel-cascade.test.ts +432 -0
  848. package/test/actor/no-completion-listener.test.ts +41 -0
  849. package/test/actor/poststop-progress-write-permission.repro.test.ts +414 -0
  850. package/test/actor/registry-render.test.ts +113 -0
  851. package/test/actor/registry-status.test.ts +111 -0
  852. package/test/actor/registry.test.ts +619 -0
  853. package/test/actor/return-header.test.ts +40 -0
  854. package/test/actor/spawn-lifecycle.test.ts +346 -0
  855. package/test/actor/spawn-no-deadlock.test.ts +340 -0
  856. package/test/actor/spawn-notification.test.ts +393 -0
  857. package/test/actor/spawn-task-autostart.test.ts +530 -0
  858. package/test/actor/spawn.test.ts +1106 -0
  859. package/test/actor/status-event-payload.test.ts +132 -0
  860. package/test/actor/terminology.test.ts +39 -0
  861. package/test/actor/turn.test.ts +125 -0
  862. package/test/actor/waiter.test.ts +246 -0
  863. package/test/agent/agent.test.ts +935 -0
  864. package/test/agent/allowlist.test.ts +45 -0
  865. package/test/auth/auth.test.ts +86 -0
  866. package/test/bus/bus-effect.test.ts +162 -0
  867. package/test/bus/bus-integration.test.ts +87 -0
  868. package/test/bus/bus.test.ts +219 -0
  869. package/test/cli/account.test.ts +26 -0
  870. package/test/cli/cmd/tui/autocomplete-detect.test.ts +55 -0
  871. package/test/cli/cmd/tui/extmark-cjk.test.ts +96 -0
  872. package/test/cli/cmd/tui/offset.test.ts +101 -0
  873. package/test/cli/cmd/tui/prompt-part.test.ts +97 -0
  874. package/test/cli/error.test.ts +18 -0
  875. package/test/cli/github-action.test.ts +198 -0
  876. package/test/cli/github-remote.test.ts +80 -0
  877. package/test/cli/import.test.ts +54 -0
  878. package/test/cli/plugin-auth-picker.test.ts +120 -0
  879. package/test/cli/run-completion.test.ts +131 -0
  880. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  881. package/test/cli/tui/plugin-add.test.ts +111 -0
  882. package/test/cli/tui/plugin-install.test.ts +87 -0
  883. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  884. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  885. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  886. package/test/cli/tui/plugin-loader.test.ts +816 -0
  887. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  888. package/test/cli/tui/revert-diff.test.ts +35 -0
  889. package/test/cli/tui/route-agent-id.test.ts +26 -0
  890. package/test/cli/tui/sidebar-tps.test.ts +63 -0
  891. package/test/cli/tui/slot-replace.test.tsx +47 -0
  892. package/test/cli/tui/sync-bucket.test.ts +29 -0
  893. package/test/cli/tui/theme-store.test.ts +51 -0
  894. package/test/cli/tui/thread.test.ts +125 -0
  895. package/test/cli/tui/transcript.test.ts +426 -0
  896. package/test/cli/tui/use-event.test.tsx +175 -0
  897. package/test/cli/tui/voice.test.ts +443 -0
  898. package/test/command/deep-research-command.test.ts +16 -0
  899. package/test/config/agent-color.test.ts +77 -0
  900. package/test/config/checkpoint-fork.test.ts +21 -0
  901. package/test/config/config.test.ts +2577 -0
  902. package/test/config/fixtures/empty-frontmatter.md +4 -0
  903. package/test/config/fixtures/frontmatter.md +28 -0
  904. package/test/config/fixtures/markdown-header.md +11 -0
  905. package/test/config/fixtures/no-frontmatter.md +1 -0
  906. package/test/config/fixtures/weird-model-id.md +13 -0
  907. package/test/config/lsp.test.ts +87 -0
  908. package/test/config/markdown.test.ts +228 -0
  909. package/test/config/plugin.test.ts +0 -0
  910. package/test/config/tui.test.ts +627 -0
  911. package/test/control-plane/adaptors.test.ts +71 -0
  912. package/test/control-plane/sse.test.ts +56 -0
  913. package/test/effect/app-runtime-logger.test.ts +92 -0
  914. package/test/effect/cross-spawn-spawner.test.ts +411 -0
  915. package/test/effect/instance-state.test.ts +482 -0
  916. package/test/effect/observability.test.ts +46 -0
  917. package/test/effect/run-service.test.ts +46 -0
  918. package/test/effect/runner-warn-log.test.ts +111 -0
  919. package/test/effect/runner.test.ts +494 -0
  920. package/test/fake/provider.ts +90 -0
  921. package/test/file/fsmonitor.test.ts +68 -0
  922. package/test/file/ignore.test.ts +10 -0
  923. package/test/file/index.test.ts +963 -0
  924. package/test/file/path-traversal.test.ts +243 -0
  925. package/test/file/ripgrep.test.ts +218 -0
  926. package/test/file/watcher.test.ts +249 -0
  927. package/test/filesystem/filesystem.test.ts +319 -0
  928. package/test/fixture/db.ts +11 -0
  929. package/test/fixture/fixture.test.ts +58 -0
  930. package/test/fixture/fixture.ts +223 -0
  931. package/test/fixture/flock-worker.ts +72 -0
  932. package/test/fixture/lsp/fake-lsp-server.js +75 -0
  933. package/test/fixture/plug-worker.ts +93 -0
  934. package/test/fixture/plugin-meta-worker.ts +19 -0
  935. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  936. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  937. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  938. package/test/fixture/skills/index.json +6 -0
  939. package/test/fixture/tui-plugin.ts +328 -0
  940. package/test/fixture/tui-runtime.ts +31 -0
  941. package/test/format/format.test.ts +244 -0
  942. package/test/git/git.test.ts +128 -0
  943. package/test/global/fixture/global-paths-worker.ts +17 -0
  944. package/test/global/mimocode-home.test.ts +143 -0
  945. package/test/history/backfill.test.ts +160 -0
  946. package/test/history/extract.test.ts +106 -0
  947. package/test/history/fts-query.test.ts +30 -0
  948. package/test/history/resolve.test.ts +130 -0
  949. package/test/history/service.test.ts +210 -0
  950. package/test/history/writer.test.ts +163 -0
  951. package/test/ide/ide.test.ts +82 -0
  952. package/test/inbox/drain-in-loop.test.ts +230 -0
  953. package/test/inbox/fork-agent-compat.test.ts +387 -0
  954. package/test/inbox/gc-on-init.test.ts +167 -0
  955. package/test/inbox/send-no-block.test.ts +120 -0
  956. package/test/inbox/sender-cancel-independence.test.ts +160 -0
  957. package/test/inbox/wake-matrix.test.ts +141 -0
  958. package/test/installation/installation.test.ts +255 -0
  959. package/test/keybind.test.ts +421 -0
  960. package/test/lib/effect.ts +53 -0
  961. package/test/lib/filesystem.ts +10 -0
  962. package/test/lib/llm-server.ts +770 -0
  963. package/test/lib/mock-llm.ts +159 -0
  964. package/test/lib/scripted-llm-server.ts +245 -0
  965. package/test/lsp/client.test.ts +98 -0
  966. package/test/lsp/index.test.ts +109 -0
  967. package/test/lsp/launch.test.ts +22 -0
  968. package/test/lsp/lifecycle.test.ts +184 -0
  969. package/test/mcp/headers.test.ts +178 -0
  970. package/test/mcp/lifecycle.test.ts +786 -0
  971. package/test/mcp/oauth-auto-connect.test.ts +281 -0
  972. package/test/mcp/oauth-browser.test.ts +268 -0
  973. package/test/mcp/oauth-callback.test.ts +34 -0
  974. package/test/memory/abort-leak-webfetch.ts +49 -0
  975. package/test/memory/abort-leak.test.ts +127 -0
  976. package/test/memory/cc-frontmatter.test.ts +85 -0
  977. package/test/memory/cc-paths.test.ts +60 -0
  978. package/test/memory/cc-reconcile.test.ts +239 -0
  979. package/test/memory/cc-search.test.ts +151 -0
  980. package/test/memory/fts-query.test.ts +48 -0
  981. package/test/memory/fts-rowid-stability.test.ts +271 -0
  982. package/test/memory/paths.test.ts +210 -0
  983. package/test/memory/reconcile.test.ts +115 -0
  984. package/test/memory/service.test.ts +169 -0
  985. package/test/npm.test.ts +18 -0
  986. package/test/patch/patch.test.ts +348 -0
  987. package/test/permission/abort.test.ts +116 -0
  988. package/test/permission/arity.test.ts +33 -0
  989. package/test/permission/disabled.test.ts +68 -0
  990. package/test/permission/next.test.ts +1116 -0
  991. package/test/permission/non-interactive.test.ts +55 -0
  992. package/test/permission-task.test.ts +326 -0
  993. package/test/plugin/actor-hooks.test.ts +1471 -0
  994. package/test/plugin/auth-override.test.ts +79 -0
  995. package/test/plugin/checkpoint-splitover.test.ts +434 -0
  996. package/test/plugin/cloudflare.test.ts +68 -0
  997. package/test/plugin/codex.test.ts +123 -0
  998. package/test/plugin/github-copilot-models.test.ts +163 -0
  999. package/test/plugin/install-concurrency.test.ts +140 -0
  1000. package/test/plugin/install.test.ts +570 -0
  1001. package/test/plugin/loader-shared.test.ts +1169 -0
  1002. package/test/plugin/matcher.test.ts +97 -0
  1003. package/test/plugin/meta.test.ts +137 -0
  1004. package/test/plugin/mimo.test.ts +259 -0
  1005. package/test/plugin/session-loop-hooks.test.ts +159 -0
  1006. package/test/plugin/shared.test.ts +88 -0
  1007. package/test/plugin/subagent-progress-checker.test.ts +227 -0
  1008. package/test/plugin/trigger.test.ts +116 -0
  1009. package/test/plugin/workspace-adaptor.test.ts +109 -0
  1010. package/test/preload.ts +102 -0
  1011. package/test/project/migrate-global.test.ts +152 -0
  1012. package/test/project/project-id.test.ts +64 -0
  1013. package/test/project/project.test.ts +504 -0
  1014. package/test/project/vcs.test.ts +286 -0
  1015. package/test/project/worktree-remove.test.ts +126 -0
  1016. package/test/project/worktree.test.ts +222 -0
  1017. package/test/provider/amazon-bedrock.test.ts +462 -0
  1018. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  1019. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  1020. package/test/provider/error.test.ts +160 -0
  1021. package/test/provider/gitlab-duo.test.ts +413 -0
  1022. package/test/provider/model-groups.test.ts +389 -0
  1023. package/test/provider/provider-chunk-timeout.test.ts +23 -0
  1024. package/test/provider/provider.test.ts +2610 -0
  1025. package/test/provider/transform.test.ts +3523 -0
  1026. package/test/pty/pty-output-isolation.test.ts +146 -0
  1027. package/test/pty/pty-session.test.ts +102 -0
  1028. package/test/pty/pty-shell.test.ts +69 -0
  1029. package/test/question/question.test.ts +464 -0
  1030. package/test/server/global-session-list.test.ts +105 -0
  1031. package/test/server/project-init-git.test.ts +137 -0
  1032. package/test/server/rate-limit.test.ts +66 -0
  1033. package/test/server/session-actions.test.ts +49 -0
  1034. package/test/server/session-list.test.ts +110 -0
  1035. package/test/server/session-messages.test.ts +220 -0
  1036. package/test/server/session-prompt-busy.test.ts +146 -0
  1037. package/test/server/session-prompt-heartbeat.test.ts +131 -0
  1038. package/test/server/session-select.test.ts +100 -0
  1039. package/test/server/session-task-route.test.ts +165 -0
  1040. package/test/server/summarize-route-main-slice.test.ts +99 -0
  1041. package/test/server/trace-attributes.test.ts +76 -0
  1042. package/test/server/workflows-route.test.ts +363 -0
  1043. package/test/session/bootstrap-skip-system.test.ts +121 -0
  1044. package/test/session/boundary.test.ts +33 -0
  1045. package/test/session/budgeted-read.test.ts +74 -0
  1046. package/test/session/checkpoint-align.test.ts +58 -0
  1047. package/test/session/checkpoint-boundary.test.ts +186 -0
  1048. package/test/session/checkpoint-child-session.test.ts +509 -0
  1049. package/test/session/checkpoint-context.test.ts +141 -0
  1050. package/test/session/checkpoint-drain.test.ts +189 -0
  1051. package/test/session/checkpoint-extract-titles.test.ts +58 -0
  1052. package/test/session/checkpoint-fork-mode.test.ts +577 -0
  1053. package/test/session/checkpoint-main-slice.test.ts +261 -0
  1054. package/test/session/checkpoint-paths.test.ts +87 -0
  1055. package/test/session/checkpoint-permission.test.ts +136 -0
  1056. package/test/session/checkpoint-progress-reconcile.test.ts +219 -0
  1057. package/test/session/checkpoint-rebuild-unify.test.ts +158 -0
  1058. package/test/session/checkpoint-rebuild-v3.test.ts +259 -0
  1059. package/test/session/checkpoint-render-verify.test.ts +512 -0
  1060. package/test/session/checkpoint-retry.test.ts +150 -0
  1061. package/test/session/checkpoint-splitover-integration.test.ts +533 -0
  1062. package/test/session/checkpoint-templates.test.ts +51 -0
  1063. package/test/session/checkpoint-thresholds.test.ts +120 -0
  1064. package/test/session/checkpoint-validator.test.ts +189 -0
  1065. package/test/session/classify-integration.test.ts +476 -0
  1066. package/test/session/classify.test.ts +390 -0
  1067. package/test/session/codex-import.test.ts +331 -0
  1068. package/test/session/compaction-agent-scope.test.ts +209 -0
  1069. package/test/session/context-inheritance.test.ts +46 -0
  1070. package/test/session/external-import.test.ts +17 -0
  1071. package/test/session/fork-prefix-invariant.test.ts +116 -0
  1072. package/test/session/goal.test.ts +106 -0
  1073. package/test/session/instruction.test.ts +388 -0
  1074. package/test/session/invalid-output-continuation.test.ts +150 -0
  1075. package/test/session/last-message-info.test.ts +47 -0
  1076. package/test/session/length-tool-safety.test.ts +125 -0
  1077. package/test/session/llm-request-prefix.test.ts +197 -0
  1078. package/test/session/llm-retry.test.ts +59 -0
  1079. package/test/session/llm-system-prompt.test.ts +479 -0
  1080. package/test/session/llm.test.ts +1268 -0
  1081. package/test/session/main-lifecycle.test.ts +51 -0
  1082. package/test/session/main-runloop-history-invariant.test.ts +182 -0
  1083. package/test/session/max-mode-econnreset.test.ts +232 -0
  1084. package/test/session/max-mode.test.ts +54 -0
  1085. package/test/session/message-v2-filter.test.ts +197 -0
  1086. package/test/session/message-v2.test.ts +1119 -0
  1087. package/test/session/messages-default-main.test.ts +105 -0
  1088. package/test/session/messages-pagination.test.ts +888 -0
  1089. package/test/session/overflow.test.ts +576 -0
  1090. package/test/session/processor-effect.test.ts +853 -0
  1091. package/test/session/prompt-effect.test.ts +1534 -0
  1092. package/test/session/prompt-rebuild-loop.test.ts +108 -0
  1093. package/test/session/prompt-rebuild-reset.test.ts +67 -0
  1094. package/test/session/prompt-sweep.test.ts +145 -0
  1095. package/test/session/prompt-task-gate.test.ts +127 -0
  1096. package/test/session/prompt.test.ts +827 -0
  1097. package/test/session/prune-main-slice.test.ts +272 -0
  1098. package/test/session/prune-skip-system.test.ts +346 -0
  1099. package/test/session/prune.test.ts +419 -0
  1100. package/test/session/rebuild-microcompact.test.ts +318 -0
  1101. package/test/session/recall-reminder.test.ts +37 -0
  1102. package/test/session/recent-user-msg.test.ts +219 -0
  1103. package/test/session/retry.test.ts +410 -0
  1104. package/test/session/revert-compact.test.ts +639 -0
  1105. package/test/session/run-state-tuple-key.test.ts +152 -0
  1106. package/test/session/session-create-registers-main.test.ts +70 -0
  1107. package/test/session/session.test.ts +181 -0
  1108. package/test/session/snapshot-tool-race.test.ts +301 -0
  1109. package/test/session/structured-output-integration.test.ts +264 -0
  1110. package/test/session/structured-output-retry.test.ts +127 -0
  1111. package/test/session/structured-output.test.ts +397 -0
  1112. package/test/session/summary-main-slice.test.ts +170 -0
  1113. package/test/session/system.test.ts +72 -0
  1114. package/test/session/text-loop-detection.test.ts +185 -0
  1115. package/test/session/text-loop-integration.test.ts +448 -0
  1116. package/test/session/text-ngram-detection.test.ts +169 -0
  1117. package/test/session/trajectory.test.ts +236 -0
  1118. package/test/share/share-next.test.ts +332 -0
  1119. package/test/shell/shell.test.ts +73 -0
  1120. package/test/skill/compose-review.test.ts +174 -0
  1121. package/test/skill/discovery.test.ts +116 -0
  1122. package/test/skill/skill.test.ts +466 -0
  1123. package/test/snapshot/snapshot.test.ts +1531 -0
  1124. package/test/storage/db.test.ts +16 -0
  1125. package/test/storage/json-migration.test.ts +831 -0
  1126. package/test/storage/storage.test.ts +293 -0
  1127. package/test/sync/index.test.ts +237 -0
  1128. package/test/task/gate-state.test.ts +66 -0
  1129. package/test/task/gate.test.ts +167 -0
  1130. package/test/task/registry.test.ts +171 -0
  1131. package/test/task/state-machine.test.ts +292 -0
  1132. package/test/team/migrate-to-inbox.test.ts +124 -0
  1133. package/test/team/team.test.ts +75 -0
  1134. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  1135. package/test/tool/actor-cancel.test.ts +211 -0
  1136. package/test/tool/actor-recover.test.ts +50 -0
  1137. package/test/tool/actor-send.test.ts +200 -0
  1138. package/test/tool/actor-status.test.ts +296 -0
  1139. package/test/tool/actor-wait.test.ts +193 -0
  1140. package/test/tool/actor.shell.test.ts +266 -0
  1141. package/test/tool/actor.test.ts +753 -0
  1142. package/test/tool/apply_patch.test.ts +626 -0
  1143. package/test/tool/bash.test.ts +1232 -0
  1144. package/test/tool/bash_token_efficient_pipeline.test.ts +329 -0
  1145. package/test/tool/describe-workflow.test.ts +12 -0
  1146. package/test/tool/edit.test.ts +730 -0
  1147. package/test/tool/external-directory.test.ts +207 -0
  1148. package/test/tool/fixtures/large-image.png +0 -0
  1149. package/test/tool/fixtures/models-api.json +65179 -0
  1150. package/test/tool/glob.test.ts +81 -0
  1151. package/test/tool/grep.test.ts +114 -0
  1152. package/test/tool/history.test.ts +144 -0
  1153. package/test/tool/invocation-style.test.ts +30 -0
  1154. package/test/tool/memory-edit-ask-skip.test.ts +62 -0
  1155. package/test/tool/memory-path-guard.test.ts +594 -0
  1156. package/test/tool/memory.test.ts +71 -0
  1157. package/test/tool/question.test.ts +167 -0
  1158. package/test/tool/read.test.ts +483 -0
  1159. package/test/tool/recoverable.test.ts +36 -0
  1160. package/test/tool/registry-invocation-style.test.ts +121 -0
  1161. package/test/tool/registry.test.ts +164 -0
  1162. package/test/tool/shell-tokenize.test.ts +309 -0
  1163. package/test/tool/shell-wrap-missing-script.test.ts +128 -0
  1164. package/test/tool/shell-wrap.test.ts +345 -0
  1165. package/test/tool/skill.test.ts +99 -0
  1166. package/test/tool/task-recover.test.ts +36 -0
  1167. package/test/tool/task.shell.test.ts +234 -0
  1168. package/test/tool/task.test.ts +296 -0
  1169. package/test/tool/tool-def-shell-shape.test.ts +23 -0
  1170. package/test/tool/tool-define.test.ts +59 -0
  1171. package/test/tool/tool-validation-error.test.ts +25 -0
  1172. package/test/tool/truncation.test.ts +253 -0
  1173. package/test/tool/webfetch.test.ts +103 -0
  1174. package/test/tool/whitelist.test.ts +377 -0
  1175. package/test/tool/write.test.ts +244 -0
  1176. package/test/util/data-url.test.ts +14 -0
  1177. package/test/util/effect-zod.test.ts +869 -0
  1178. package/test/util/env-info.test.ts +26 -0
  1179. package/test/util/error.test.ts +38 -0
  1180. package/test/util/filesystem.test.ts +656 -0
  1181. package/test/util/format.test.ts +59 -0
  1182. package/test/util/glob.test.ts +164 -0
  1183. package/test/util/iife.test.ts +36 -0
  1184. package/test/util/lazy.test.ts +50 -0
  1185. package/test/util/lock.test.ts +72 -0
  1186. package/test/util/log.test.ts +69 -0
  1187. package/test/util/module.test.ts +59 -0
  1188. package/test/util/process.test.ts +128 -0
  1189. package/test/util/queue.test.ts +64 -0
  1190. package/test/util/ssrf.test.ts +115 -0
  1191. package/test/util/timeout.test.ts +21 -0
  1192. package/test/util/tool-compat.test.ts +189 -0
  1193. package/test/util/which.test.ts +100 -0
  1194. package/test/util/wildcard.test.ts +90 -0
  1195. package/test/workflow/builtin.test.ts +36 -0
  1196. package/test/workflow/compose.test.ts +668 -0
  1197. package/test/workflow/deep-research-cluster.test.ts +47 -0
  1198. package/test/workflow/lib.ts +243 -0
  1199. package/test/workflow/meta.test.ts +142 -0
  1200. package/test/workflow/model-routing.test.ts +68 -0
  1201. package/test/workflow/persistence.test.ts +229 -0
  1202. package/test/workflow/resolve.test.ts +37 -0
  1203. package/test/workflow/runtime-nested.test.ts +419 -0
  1204. package/test/workflow/runtime-worktree.test.ts +261 -0
  1205. package/test/workflow/runtime.test.ts +1142 -0
  1206. package/test/workflow/sandbox.test.ts +259 -0
  1207. package/test/workflow/tool.test.ts +487 -0
  1208. package/test/workflow/verify-wow.test.ts +144 -0
  1209. package/test/workflow/workspace.test.ts +88 -0
  1210. package/test/workspace/workspace-restore.test.ts +281 -0
  1211. package/test/worktree/index.test.ts +30 -0
  1212. package/tsconfig.json +24 -0
@@ -0,0 +1,3087 @@
1
+ import {
2
+ batch,
3
+ createContext,
4
+ createEffect,
5
+ createMemo,
6
+ createSignal,
7
+ For,
8
+ Match,
9
+ on,
10
+ onCleanup,
11
+ Show,
12
+ Switch,
13
+ useContext,
14
+ } from "solid-js"
15
+ import { Dynamic } from "solid-js/web"
16
+ import path from "path"
17
+ import { useCurrentAgentID, useRoute, useRouteData } from "@tui/context/route"
18
+ import { useProject } from "@tui/context/project"
19
+ import { useSync } from "@tui/context/sync"
20
+ import { useEvent } from "@tui/context/event"
21
+ import { SplitBorder } from "@tui/component/border"
22
+ import { Spinner } from "@tui/component/spinner"
23
+ import { selectedForeground, useTheme } from "@tui/context/theme"
24
+ import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA, MouseEvent } from "@opentui/core"
25
+ import { Prompt, type PromptRef } from "@tui/component/prompt"
26
+ import type {
27
+ AssistantMessage,
28
+ Part,
29
+ Provider,
30
+ ToolPart,
31
+ UserMessage,
32
+ TextPart,
33
+ ReasoningPart,
34
+ } from "@opendash-ai/sdk/v2"
35
+ import { useLocal } from "@tui/context/local"
36
+ import { Locale } from "@/util"
37
+ import type { Tool } from "@/tool"
38
+ import type { ReadTool } from "@/tool/read"
39
+ import type { WriteTool } from "@/tool/write"
40
+ import { BashTool } from "@/tool/bash"
41
+ import type { GlobTool } from "@/tool/glob"
42
+ import type { GrepTool } from "@/tool/grep"
43
+ import type { EditTool } from "@/tool/edit"
44
+ import type { ApplyPatchTool } from "@/tool/apply_patch"
45
+ import type { WebFetchTool } from "@/tool/webfetch"
46
+ import type { CodeSearchTool } from "@/tool/codesearch"
47
+ import type { WebSearchTool } from "@/tool/websearch"
48
+ import type { ActorTool } from "@/tool/actor"
49
+ import type { TaskTool } from "@/tool/task"
50
+ import type { QuestionTool } from "@/tool/question"
51
+ import type { SkillTool } from "@/tool/skill"
52
+ import type { WorkflowTool } from "@/tool/workflow"
53
+ import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
54
+ import { useSDK } from "@tui/context/sdk"
55
+ import { useCommandDialog } from "@tui/component/dialog-command"
56
+ import { useLanguage } from "@tui/context/language"
57
+ import type { DialogContext } from "@tui/ui/dialog"
58
+ import { useKeybind } from "@tui/context/keybind"
59
+ import { useDialog } from "../../ui/dialog"
60
+ import { DialogMessage } from "./dialog-message"
61
+ import type { PromptInfo } from "../../component/prompt/history"
62
+ import { DialogConfirm } from "@tui/ui/dialog-confirm"
63
+ import { DialogTimeline } from "./dialog-timeline"
64
+ import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
65
+ import { DialogSessionRename } from "../../component/dialog-session-rename"
66
+ import { Sidebar } from "./sidebar"
67
+ import { WorkflowTree } from "@tui/component/workflow-tree"
68
+ import { SubagentFooter } from "./subagent-footer.tsx"
69
+ import { DialogSubagent } from "./dialog-subagent.tsx"
70
+ import { Flag } from "@/flag/flag"
71
+ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
72
+ import parsers from "../../../../../../parsers-config.ts"
73
+ import * as Clipboard from "../../util/clipboard"
74
+ import { Toast, useToast } from "../../ui/toast"
75
+ import { useKV } from "../../context/kv.tsx"
76
+ import * as Editor from "../../util/editor"
77
+ import stripAnsi from "strip-ansi"
78
+ import { usePromptRef } from "../../context/prompt"
79
+ import { useExit } from "../../context/exit"
80
+ import { Filesystem } from "@/util"
81
+ import { Global } from "@/global"
82
+ import { PermissionPrompt } from "./permission"
83
+ import { QuestionPrompt } from "./question"
84
+ import { DialogExportOptions } from "../../ui/dialog-export-options"
85
+ import * as Model from "../../util/model"
86
+ import { formatTranscript } from "../../util/transcript"
87
+ import { UI } from "@/cli/ui.ts"
88
+ import { useTuiConfig } from "../../context/tui-config"
89
+ import { getScrollAcceleration } from "../../util/scroll"
90
+ import { nextThinkingMode, reasoningSummary, useThinkingMode, type ThinkingMode } from "../../context/thinking"
91
+ import { TuiPluginRuntime } from "../../plugin"
92
+ import { DialogGoUpsell } from "../../component/dialog-go-upsell"
93
+ import { DialogTokenPlan } from "../../component/dialog-token-plan"
94
+ import { SessionRetry } from "@/session/retry"
95
+ import { getRevertDiffFiles } from "../../util/revert-diff"
96
+ import { getAgentBrandColor } from "../../component/prompt/shimmer"
97
+
98
+ addDefaultParsers(parsers.parsers)
99
+
100
+ const GO_UPSELL_LAST_SEEN_AT = "go_upsell_last_seen_at"
101
+ const GO_UPSELL_DONT_SHOW = "go_upsell_dont_show"
102
+ const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs
103
+
104
+ const QUEUE_TOKEN_PLAN_LAST_SEEN_AT = "queue_token_plan_last_seen_at"
105
+ const QUEUE_TOKEN_PLAN_WINDOW = 86_400_000 // 24 hrs
106
+
107
+ const context = createContext<{
108
+ width: number
109
+ sessionID: string
110
+ conceal: () => boolean
111
+ thinkingMode: () => ThinkingMode
112
+ showThinking: () => boolean
113
+ showTimestamps: () => boolean
114
+ showDetails: () => boolean
115
+ showGenericToolOutput: () => boolean
116
+ diffWrapMode: () => "word" | "none"
117
+ providers: () => ReadonlyMap<string, Provider>
118
+ sync: ReturnType<typeof useSync>
119
+ tui: ReturnType<typeof useTuiConfig>
120
+ }>()
121
+
122
+ function use() {
123
+ const ctx = useContext(context)
124
+ if (!ctx) throw new Error("useContext must be used within a Session component")
125
+ return ctx
126
+ }
127
+
128
+ export function Session() {
129
+ const route = useRouteData("session")
130
+ const fullRoute = useRoute()
131
+ const navigate = fullRoute.navigate
132
+ const sync = useSync()
133
+ const event = useEvent()
134
+ const project = useProject()
135
+ const tuiConfig = useTuiConfig()
136
+ const kv = useKV()
137
+ const { theme } = useTheme()
138
+ const promptRef = usePromptRef()
139
+ const session = createMemo(() => sync.session.get(route.sessionID))
140
+ const currentAgentID = useCurrentAgentID()
141
+ const actors = createMemo(() => sync.data.actor[route.sessionID] ?? [])
142
+ const messages = createMemo(() => sync.data.message[route.sessionID]?.[currentAgentID()] ?? [])
143
+ const permissions = createMemo(() => sync.data.permission[route.sessionID] ?? [])
144
+ const questions = createMemo(() => sync.data.question[route.sessionID] ?? [])
145
+ const visible = createMemo(
146
+ () =>
147
+ !session()?.parentID &&
148
+ currentAgentID() === "main" &&
149
+ permissions().length === 0 &&
150
+ questions().length === 0,
151
+ )
152
+ const disabled = createMemo(() => permissions().length > 0 || questions().length > 0)
153
+
154
+ const pending = createMemo(() => {
155
+ return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
156
+ })
157
+
158
+ const lastAssistant = createMemo(() => {
159
+ return messages().findLast((x) => x.role === "assistant")
160
+ })
161
+
162
+ const dimensions = useTerminalDimensions()
163
+ const [sidebar, setSidebar] = kv.signal<"auto" | "hide">("sidebar", "auto")
164
+ const [sidebarOpen, setSidebarOpen] = createSignal(false)
165
+ const [conceal, setConceal] = createSignal(true)
166
+ const thinking = useThinkingMode()
167
+ const thinkingMode = thinking.mode
168
+ const showThinking = createMemo(() => true)
169
+ const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
170
+ const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
171
+ const [showAssistantMetadata, _setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
172
+ const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
173
+ const [scrolling, setScrolling] = createSignal(false)
174
+ let scrollHideTimer: ReturnType<typeof setTimeout> | undefined
175
+ const scrollbarVisible = createMemo(() => showScrollbar() || scrolling())
176
+ const onWheel = (evt: MouseEvent) => {
177
+ if (evt.type !== "scroll") return
178
+ setScrolling(true)
179
+ if (scrollHideTimer) clearTimeout(scrollHideTimer)
180
+ scrollHideTimer = setTimeout(() => setScrolling(false), 1000)
181
+ }
182
+ onCleanup(() => {
183
+ if (scrollHideTimer) clearTimeout(scrollHideTimer)
184
+ })
185
+ const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
186
+ const [_animationsEnabled, _setAnimationsEnabled] = kv.signal("animations_enabled", true)
187
+ const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
188
+
189
+ // The workflow run shown as a FULL-SCREEN page (replacing the message stream),
190
+ // mirroring how agentID renders a subagent's conversation. Driven by the route so
191
+ // it's a real navigable view, not a side panel.
192
+ const workflowRunID = createMemo(() => route.workflowRunID)
193
+ const fromWorkflowRunID = createMemo(() => route.fromWorkflowRunID)
194
+
195
+ const wide = createMemo(() => dimensions().width > 120)
196
+ const sidebarVisible = createMemo(() => {
197
+ if (session()?.parentID) return false
198
+ if (currentAgentID() !== "main") return false
199
+ if (sidebarOpen()) return true
200
+ if (sidebar() === "auto" && wide()) return true
201
+ return false
202
+ })
203
+ const showTimestamps = createMemo(() => timestamps() === "show")
204
+ const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
205
+ const providers = createMemo(() => Model.index(sync.data.provider))
206
+
207
+ const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
208
+ const toast = useToast()
209
+ const sdk = useSDK()
210
+
211
+ createEffect(async () => {
212
+ const previousWorkspace = project.workspace.current()
213
+ const result = await sdk.client.session.get({ sessionID: route.sessionID }, { throwOnError: true })
214
+ if (!result.data) {
215
+ toast.show({
216
+ message: `Session not found: ${route.sessionID}`,
217
+ variant: "error",
218
+ })
219
+ navigate({ type: "home" })
220
+ return
221
+ }
222
+
223
+ if (result.data.workspaceID !== previousWorkspace) {
224
+ project.workspace.set(result.data.workspaceID)
225
+
226
+ // Sync all the data for this workspace. Note that this
227
+ // workspace may not exist anymore which is why this is not
228
+ // fatal. If it doesn't we still want to show the session
229
+ // (which will be non-interactive)
230
+ try {
231
+ await sync.bootstrap({ fatal: false })
232
+ } catch (e) {}
233
+ }
234
+ await sync.session.sync(route.sessionID)
235
+ if (scroll) scroll.scrollBy(100_000)
236
+ })
237
+
238
+ let lastSwitch: string | undefined = undefined
239
+ event.on("message.part.updated", (evt) => {
240
+ const part = evt.properties.part
241
+ if (part.type !== "tool") return
242
+ if (part.sessionID !== route.sessionID) return
243
+ if (part.state.status !== "completed") return
244
+ if (part.id === lastSwitch) return
245
+
246
+ if (part.tool === "plan_exit" && part.state.metadata?.switched) {
247
+ local.agent.set("build")
248
+ lastSwitch = part.id
249
+ } else if (part.tool === "plan_enter") {
250
+ local.agent.set("plan")
251
+ lastSwitch = part.id
252
+ }
253
+ })
254
+
255
+ let seeded = false
256
+ let scroll: ScrollBoxRenderable
257
+ let prompt: PromptRef | undefined
258
+ const bind = (r: PromptRef | undefined) => {
259
+ prompt = r
260
+ promptRef.set(r)
261
+ if (seeded || !route.prompt || !r) return
262
+ seeded = true
263
+ r.set(route.prompt)
264
+ }
265
+ const keybind = useKeybind()
266
+ const dialog = useDialog()
267
+ const renderer = useRenderer()
268
+
269
+ event.on("session.status", (evt) => {
270
+ if (evt.properties.sessionID !== route.sessionID) return
271
+ if (evt.properties.status.type !== "retry") return
272
+ if (evt.properties.status.message !== SessionRetry.GO_UPSELL_MESSAGE) return
273
+ if (dialog.stack.length > 0) return
274
+
275
+ const seen = kv.get(GO_UPSELL_LAST_SEEN_AT)
276
+ if (typeof seen === "number" && Date.now() - seen < GO_UPSELL_WINDOW) return
277
+
278
+ if (kv.get(GO_UPSELL_DONT_SHOW)) return
279
+
280
+ void DialogGoUpsell.show(dialog).then((dontShowAgain) => {
281
+ if (dontShowAgain) kv.set(GO_UPSELL_DONT_SHOW, true)
282
+ kv.set(GO_UPSELL_LAST_SEEN_AT, Date.now())
283
+ })
284
+ })
285
+
286
+ // Allow exit when in child session (prompt is hidden)
287
+ const exit = useExit()
288
+
289
+ createEffect(() => {
290
+ const title = Locale.truncate(session()?.title ?? "", 50)
291
+ const pad = (text: string) => text.padEnd(10, " ")
292
+ const weak = (text: string) => UI.Style.TEXT_DIM + pad(text) + UI.Style.TEXT_NORMAL
293
+ const logo = UI.logo(" ").split(/\r?\n/)
294
+ return exit.message.set(
295
+ [
296
+ ...logo,
297
+ ``,
298
+ ` ${weak("Session")}${UI.Style.TEXT_NORMAL_BOLD}${title}${UI.Style.TEXT_NORMAL}`,
299
+ ` ${weak("Continue")}${UI.Style.TEXT_NORMAL_BOLD}opendash -s ${session()?.id}${UI.Style.TEXT_NORMAL}`,
300
+ ``,
301
+ ].join("\n"),
302
+ )
303
+ })
304
+
305
+ useKeyboard((evt) => {
306
+ if (!session()?.parentID && currentAgentID() === "main") return
307
+ if (keybind.match("app_exit", evt)) {
308
+ const status = sync.data.session_status?.[route.sessionID]
309
+ if (status && status.type !== "idle") {
310
+ void sdk.client.session.abort({ sessionID: route.sessionID }).catch(() => {})
311
+ return
312
+ }
313
+ void exit()
314
+ }
315
+ })
316
+
317
+ // Helper: Find next visible message boundary in direction
318
+ const findNextVisibleMessage = (direction: "next" | "prev"): string | null => {
319
+ const children = scroll.getChildren()
320
+ const messagesList = messages()
321
+ const scrollTop = scroll.y
322
+
323
+ // Get visible messages sorted by position, filtering for valid non-synthetic, non-ignored content
324
+ const visibleMessages = children
325
+ .filter((c) => {
326
+ if (!c.id) return false
327
+ const message = messagesList.find((m) => m.id === c.id)
328
+ if (!message) return false
329
+
330
+ // Check if message has valid non-synthetic, non-ignored text parts
331
+ const parts = sync.data.part[message.id]
332
+ if (!parts || !Array.isArray(parts)) return false
333
+
334
+ return parts.some((part) => part && part.type === "text" && !part.synthetic && !part.ignored)
335
+ })
336
+ .sort((a, b) => a.y - b.y)
337
+
338
+ if (visibleMessages.length === 0) return null
339
+
340
+ if (direction === "next") {
341
+ // Find first message below current position
342
+ return visibleMessages.find((c) => c.y > scrollTop + 10)?.id ?? null
343
+ }
344
+ // Find last message above current position
345
+ return [...visibleMessages].reverse().find((c) => c.y < scrollTop - 10)?.id ?? null
346
+ }
347
+
348
+ // Helper: Scroll to message in direction or fallback to page scroll
349
+ const scrollToMessage = (direction: "next" | "prev", dialog: ReturnType<typeof useDialog>) => {
350
+ const targetID = findNextVisibleMessage(direction)
351
+
352
+ if (!targetID) {
353
+ scroll.scrollBy(direction === "next" ? scroll.height : -scroll.height)
354
+ dialog.clear()
355
+ return
356
+ }
357
+
358
+ const child = scroll.getChildren().find((c) => c.id === targetID)
359
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
360
+ dialog.clear()
361
+ }
362
+
363
+ function toBottom() {
364
+ setTimeout(() => {
365
+ if (!scroll || scroll.isDestroyed) return
366
+ scroll.scrollTo(scroll.scrollHeight)
367
+ }, 50)
368
+ }
369
+
370
+ const local = useLocal()
371
+
372
+ // Free "mimo-auto" channel: on a rate-limit / queue ("too many requests"),
373
+ // nudge the user toward a Token Plan — at most once per 24h.
374
+ event.on("session.status", (evt) => {
375
+ if (evt.properties.sessionID !== route.sessionID) return
376
+ if (evt.properties.status.type !== "retry") return
377
+ if (!SessionRetry.isRateLimitMessage(evt.properties.status.message)) return
378
+ const model = local.model.current()
379
+ if (!model || model.providerID !== "mimo" || model.modelID !== "mimo-auto") return
380
+ if (dialog.stack.length > 0) return
381
+
382
+ const seen = kv.get(QUEUE_TOKEN_PLAN_LAST_SEEN_AT)
383
+ if (typeof seen === "number" && Date.now() - seen < QUEUE_TOKEN_PLAN_WINDOW) return
384
+
385
+ // Record the 24h cooldown only after the user dismisses, so a show() that
386
+ // fails (or never reaches the user) doesn't silently burn the whole day.
387
+ void DialogTokenPlan.show(dialog).then(() => {
388
+ kv.set(QUEUE_TOKEN_PLAN_LAST_SEEN_AT, Date.now())
389
+ })
390
+ })
391
+
392
+ function moveFirstChild() {
393
+ const list = actors().filter((a) => a.mode === "subagent")
394
+ if (list.length === 0) {
395
+ dialog.replace(() => <DialogSubagent sessionID={route.sessionID} />)
396
+ return
397
+ }
398
+ if (fullRoute.data.type !== "session") return
399
+ navigate({ ...fullRoute.data, agentID: list[0].actor_id, fromWorkflowRunID: undefined })
400
+ }
401
+
402
+ function moveChild(direction: 1 | -1) {
403
+ const list = actors().filter((a) => a.mode === "subagent")
404
+ if (list.length === 0) return
405
+ if (fullRoute.data.type !== "session") return
406
+ const cur = currentAgentID()
407
+ const idx = list.findIndex((a) => a.actor_id === cur)
408
+ const next =
409
+ idx === -1
410
+ ? direction === 1
411
+ ? 0
412
+ : list.length - 1
413
+ : (idx + direction + list.length) % list.length
414
+ navigate({ ...fullRoute.data, agentID: list[next].actor_id, fromWorkflowRunID: undefined })
415
+ }
416
+
417
+ const command = useCommandDialog()
418
+ const t = useLanguage().t
419
+ command.register(() => [
420
+ {
421
+ title: t(session()?.share?.url ? "tui.command.session.share.copy_link" : "tui.command.session.share.title"),
422
+ value: "session.share",
423
+ suggested: route.type === "session",
424
+ keybind: "session_share",
425
+ category: "session",
426
+ enabled: sync.data.config.share !== "disabled",
427
+ slash: {
428
+ name: "share",
429
+ },
430
+ onSelect: async (dialog) => {
431
+ const copy = (url: string) =>
432
+ Clipboard.copy(url)
433
+ .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
434
+ .catch(() => toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }))
435
+ const url = session()?.share?.url
436
+ if (url) {
437
+ await copy(url)
438
+ dialog.clear()
439
+ return
440
+ }
441
+ if (!kv.get("share_consent", false)) {
442
+ const ok = await DialogConfirm.show(dialog, "Share Session", "Are you sure you want to share it?")
443
+ if (ok !== true) return
444
+ kv.set("share_consent", true)
445
+ }
446
+ await sdk.client.session
447
+ .share({
448
+ sessionID: route.sessionID,
449
+ })
450
+ .then((res) => copy(res.data!.share!.url))
451
+ .catch((error) => {
452
+ toast.show({
453
+ message: error instanceof Error ? error.message : "Failed to share session",
454
+ variant: "error",
455
+ })
456
+ })
457
+ dialog.clear()
458
+ },
459
+ },
460
+ {
461
+ title: t("tui.command.session.rename.title"),
462
+ value: "session.rename",
463
+ keybind: "session_rename",
464
+ category: "session",
465
+ slash: {
466
+ name: "rename",
467
+ },
468
+ onSelect: (dialog) => {
469
+ dialog.replace(() => <DialogSessionRename session={route.sessionID} />)
470
+ },
471
+ },
472
+ {
473
+ title: t("tui.command.session.timeline.title"),
474
+ value: "session.timeline",
475
+ keybind: "session_timeline",
476
+ category: "session",
477
+ slash: {
478
+ name: "timeline",
479
+ },
480
+ onSelect: (dialog) => {
481
+ dialog.replace(() => (
482
+ <DialogTimeline
483
+ onMove={(messageID) => {
484
+ const child = scroll.getChildren().find((child) => {
485
+ return child.id === messageID
486
+ })
487
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
488
+ }}
489
+ sessionID={route.sessionID}
490
+ setPrompt={(promptInfo) => prompt?.set(promptInfo)}
491
+ />
492
+ ))
493
+ },
494
+ },
495
+ {
496
+ title: t("tui.command.session.fork.title"),
497
+ value: "session.fork",
498
+ keybind: "session_fork",
499
+ category: "session",
500
+ slash: {
501
+ name: "fork",
502
+ },
503
+ onSelect: (dialog) => {
504
+ dialog.replace(() => (
505
+ <DialogForkFromTimeline
506
+ onMove={(messageID) => {
507
+ if (!messageID) return
508
+ const child = scroll.getChildren().find((child) => {
509
+ return child.id === messageID
510
+ })
511
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
512
+ }}
513
+ sessionID={route.sessionID}
514
+ />
515
+ ))
516
+ },
517
+ },
518
+ {
519
+ title: t("tui.command.session.compact.title"),
520
+ value: "session.compact",
521
+ keybind: "session_compact",
522
+ category: "session",
523
+ slash: {
524
+ name: "compact",
525
+ aliases: ["summarize"],
526
+ },
527
+ onSelect: (dialog) => {
528
+ const selectedModel = local.model.current()
529
+ if (!selectedModel) {
530
+ toast.show({
531
+ variant: "warning",
532
+ message: "Connect a provider to summarize this session",
533
+ duration: 3000,
534
+ })
535
+ return
536
+ }
537
+ void sdk.client.session.summarize({
538
+ sessionID: route.sessionID,
539
+ modelID: selectedModel.modelID,
540
+ providerID: selectedModel.providerID,
541
+ })
542
+ dialog.clear()
543
+ },
544
+ },
545
+ {
546
+ title: t("tui.command.session.unshare.title"),
547
+ value: "session.unshare",
548
+ keybind: "session_unshare",
549
+ category: "session",
550
+ enabled: !!session()?.share?.url,
551
+ slash: {
552
+ name: "unshare",
553
+ },
554
+ onSelect: async (dialog) => {
555
+ await sdk.client.session
556
+ .unshare({
557
+ sessionID: route.sessionID,
558
+ })
559
+ .then(() => toast.show({ message: "Session unshared successfully", variant: "success" }))
560
+ .catch((error) => {
561
+ toast.show({
562
+ message: error instanceof Error ? error.message : "Failed to unshare session",
563
+ variant: "error",
564
+ })
565
+ })
566
+ dialog.clear()
567
+ },
568
+ },
569
+ {
570
+ title: t("tui.command.session.undo.title"),
571
+ value: "session.undo",
572
+ keybind: "messages_undo",
573
+ category: "session",
574
+ slash: {
575
+ name: "undo",
576
+ },
577
+ onSelect: async (dialog) => {
578
+ const status = sync.data.session_status?.[route.sessionID]
579
+ if (status?.type !== "idle") await sdk.client.session.abort({ sessionID: route.sessionID }).catch(() => {})
580
+ const revert = session()?.revert?.messageID
581
+ const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user")
582
+ if (!message) return
583
+ void sdk.client.session
584
+ .revert({
585
+ sessionID: route.sessionID,
586
+ messageID: message.id,
587
+ })
588
+ .then(() => {
589
+ toBottom()
590
+ })
591
+ const parts = sync.data.part[message.id]
592
+ prompt?.set(
593
+ parts.reduce(
594
+ (agg, part) => {
595
+ if (part.type === "text") {
596
+ if (!part.synthetic) agg.input += part.text
597
+ }
598
+ if (part.type === "file") agg.parts.push(part)
599
+ return agg
600
+ },
601
+ { input: "", parts: [] as PromptInfo["parts"] },
602
+ ),
603
+ )
604
+ dialog.clear()
605
+ },
606
+ },
607
+ {
608
+ title: t("tui.command.session.redo.title"),
609
+ value: "session.redo",
610
+ keybind: "messages_redo",
611
+ category: "session",
612
+ enabled: !!session()?.revert?.messageID,
613
+ slash: {
614
+ name: "redo",
615
+ },
616
+ onSelect: (dialog) => {
617
+ dialog.clear()
618
+ const messageID = session()?.revert?.messageID
619
+ if (!messageID) return
620
+ const message = messages().find((x) => x.role === "user" && x.id > messageID)
621
+ if (!message) {
622
+ void sdk.client.session.unrevert({
623
+ sessionID: route.sessionID,
624
+ })
625
+ prompt?.set({ input: "", parts: [] })
626
+ return
627
+ }
628
+ void sdk.client.session.revert({
629
+ sessionID: route.sessionID,
630
+ messageID: message.id,
631
+ })
632
+ },
633
+ },
634
+ {
635
+ title: t(sidebarVisible() ? "tui.command.session.sidebar.hide" : "tui.command.session.sidebar.show"),
636
+ value: "session.sidebar.toggle",
637
+ keybind: "sidebar_toggle",
638
+ slash: {
639
+ name: "sidebar",
640
+ },
641
+ category: "session",
642
+ onSelect: (dialog) => {
643
+ batch(() => {
644
+ const isVisible = sidebarVisible()
645
+ setSidebar(() => (isVisible ? "hide" : "auto"))
646
+ setSidebarOpen(!isVisible)
647
+ })
648
+ dialog.clear()
649
+ },
650
+ },
651
+ {
652
+ title: t(conceal() ? "tui.command.session.conceal.disable" : "tui.command.session.conceal.enable"),
653
+ value: "session.toggle.conceal",
654
+ keybind: "messages_toggle_conceal",
655
+ category: "session",
656
+ onSelect: (dialog) => {
657
+ setConceal((prev) => !prev)
658
+ dialog.clear()
659
+ },
660
+ },
661
+ {
662
+ title: t(showTimestamps() ? "tui.command.session.timestamps.hide" : "tui.command.session.timestamps.show"),
663
+ value: "session.toggle.timestamps",
664
+ category: "session",
665
+ slash: {
666
+ name: "timestamps",
667
+ aliases: ["toggle-timestamps"],
668
+ },
669
+ onSelect: (dialog) => {
670
+ setTimestamps((prev) => (prev === "show" ? "hide" : "show"))
671
+ dialog.clear()
672
+ },
673
+ },
674
+ {
675
+ title: t(
676
+ nextThinkingMode(thinkingMode()) === "hide"
677
+ ? "tui.command.session.thinking.collapse"
678
+ : "tui.command.session.thinking.expand",
679
+ ),
680
+ value: "session.toggle.thinking",
681
+ keybind: "display_thinking",
682
+ category: "session",
683
+ slash: {
684
+ name: "thinking",
685
+ aliases: ["toggle-thinking"],
686
+ },
687
+ onSelect: (dialog) => {
688
+ thinking.set(nextThinkingMode(thinkingMode()))
689
+ dialog.clear()
690
+ },
691
+ },
692
+ {
693
+ title: t(showDetails() ? "tui.command.session.tool_details.hide" : "tui.command.session.tool_details.show"),
694
+ value: "session.toggle.actions",
695
+ keybind: "tool_details",
696
+ category: "session",
697
+ onSelect: (dialog) => {
698
+ setShowDetails((prev) => !prev)
699
+ dialog.clear()
700
+ },
701
+ },
702
+ {
703
+ title: t("tui.command.session.scrollbar.toggle"),
704
+ value: "session.toggle.scrollbar",
705
+ keybind: "scrollbar_toggle",
706
+ category: "session",
707
+ onSelect: (dialog) => {
708
+ setShowScrollbar((prev) => !prev)
709
+ dialog.clear()
710
+ },
711
+ },
712
+ {
713
+ title: t(
714
+ showGenericToolOutput()
715
+ ? "tui.command.session.generic_tool_output.hide"
716
+ : "tui.command.session.generic_tool_output.show",
717
+ ),
718
+ value: "session.toggle.generic_tool_output",
719
+ category: "session",
720
+ onSelect: (dialog) => {
721
+ setShowGenericToolOutput((prev) => !prev)
722
+ dialog.clear()
723
+ },
724
+ },
725
+ {
726
+ title: t("tui.command.session.page_up.title"),
727
+ value: "session.page.up",
728
+ keybind: "messages_page_up",
729
+ category: "session",
730
+ hidden: true,
731
+ onSelect: (dialog) => {
732
+ scroll.scrollBy(-scroll.height / 2)
733
+ dialog.clear()
734
+ },
735
+ },
736
+ {
737
+ title: t("tui.command.session.page_down.title"),
738
+ value: "session.page.down",
739
+ keybind: "messages_page_down",
740
+ category: "session",
741
+ hidden: true,
742
+ onSelect: (dialog) => {
743
+ scroll.scrollBy(scroll.height / 2)
744
+ dialog.clear()
745
+ },
746
+ },
747
+ {
748
+ title: t("tui.command.session.line_up.title"),
749
+ value: "session.line.up",
750
+ keybind: "messages_line_up",
751
+ category: "session",
752
+ disabled: true,
753
+ onSelect: (dialog) => {
754
+ scroll.scrollBy(-1)
755
+ dialog.clear()
756
+ },
757
+ },
758
+ {
759
+ title: t("tui.command.session.line_down.title"),
760
+ value: "session.line.down",
761
+ keybind: "messages_line_down",
762
+ category: "session",
763
+ disabled: true,
764
+ onSelect: (dialog) => {
765
+ scroll.scrollBy(1)
766
+ dialog.clear()
767
+ },
768
+ },
769
+ {
770
+ title: t("tui.command.session.half_page_up.title"),
771
+ value: "session.half.page.up",
772
+ keybind: "messages_half_page_up",
773
+ category: "session",
774
+ hidden: true,
775
+ onSelect: (dialog) => {
776
+ scroll.scrollBy(-scroll.height / 4)
777
+ dialog.clear()
778
+ },
779
+ },
780
+ {
781
+ title: t("tui.command.session.half_page_down.title"),
782
+ value: "session.half.page.down",
783
+ keybind: "messages_half_page_down",
784
+ category: "session",
785
+ hidden: true,
786
+ onSelect: (dialog) => {
787
+ scroll.scrollBy(scroll.height / 4)
788
+ dialog.clear()
789
+ },
790
+ },
791
+ {
792
+ title: t("tui.command.session.first.title"),
793
+ value: "session.first",
794
+ keybind: "messages_first",
795
+ category: "session",
796
+ hidden: true,
797
+ onSelect: (dialog) => {
798
+ scroll.scrollTo(0)
799
+ dialog.clear()
800
+ },
801
+ },
802
+ {
803
+ title: t("tui.command.session.last.title"),
804
+ value: "session.last",
805
+ keybind: "messages_last",
806
+ category: "session",
807
+ hidden: true,
808
+ onSelect: (dialog) => {
809
+ scroll.scrollTo(scroll.scrollHeight)
810
+ dialog.clear()
811
+ },
812
+ },
813
+ {
814
+ title: t("tui.command.session.last_user.title"),
815
+ value: "session.messages_last_user",
816
+ keybind: "messages_last_user",
817
+ category: "session",
818
+ hidden: true,
819
+ onSelect: () => {
820
+ const msgs = messages()
821
+ if (!msgs || !msgs.length) return
822
+
823
+ // Find the most recent user message with non-ignored, non-synthetic text parts
824
+ for (let i = msgs.length - 1; i >= 0; i--) {
825
+ const message = msgs[i]
826
+ if (!message || message.role !== "user") continue
827
+
828
+ const parts = sync.data.part[message.id]
829
+ if (!parts || !Array.isArray(parts)) continue
830
+
831
+ const hasValidTextPart = parts.some(
832
+ (part) => part && part.type === "text" && !part.synthetic && !part.ignored,
833
+ )
834
+
835
+ if (hasValidTextPart) {
836
+ const child = scroll.getChildren().find((child) => {
837
+ return child.id === message.id
838
+ })
839
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
840
+ break
841
+ }
842
+ }
843
+ },
844
+ },
845
+ {
846
+ title: t("tui.command.session.message_next.title"),
847
+ value: "session.message.next",
848
+ keybind: "messages_next",
849
+ category: "session",
850
+ hidden: true,
851
+ onSelect: (dialog) => scrollToMessage("next", dialog),
852
+ },
853
+ {
854
+ title: t("tui.command.session.message_previous.title"),
855
+ value: "session.message.previous",
856
+ keybind: "messages_previous",
857
+ category: "session",
858
+ hidden: true,
859
+ onSelect: (dialog) => scrollToMessage("prev", dialog),
860
+ },
861
+ {
862
+ title: t("tui.command.messages.copy.title"),
863
+ value: "messages.copy",
864
+ keybind: "messages_copy",
865
+ category: "session",
866
+ onSelect: (dialog) => {
867
+ const revertID = session()?.revert?.messageID
868
+ const lastAssistantMessage = messages().findLast(
869
+ (msg) => msg.role === "assistant" && (!revertID || msg.id < revertID),
870
+ )
871
+ if (!lastAssistantMessage) {
872
+ toast.show({ message: "No assistant messages found", variant: "error" })
873
+ dialog.clear()
874
+ return
875
+ }
876
+
877
+ const parts = sync.data.part[lastAssistantMessage.id] ?? []
878
+ const textParts = parts.filter((part) => part.type === "text")
879
+ if (textParts.length === 0) {
880
+ toast.show({ message: "No text parts found in last assistant message", variant: "error" })
881
+ dialog.clear()
882
+ return
883
+ }
884
+
885
+ const text = textParts
886
+ .map((part) => part.text)
887
+ .join("\n")
888
+ .trim()
889
+ if (!text) {
890
+ toast.show({
891
+ message: "No text content found in last assistant message",
892
+ variant: "error",
893
+ })
894
+ dialog.clear()
895
+ return
896
+ }
897
+
898
+ Clipboard.copy(text)
899
+ .then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" }))
900
+ .catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" }))
901
+ dialog.clear()
902
+ },
903
+ },
904
+ {
905
+ title: t("tui.command.session.copy.title"),
906
+ value: "session.copy",
907
+ category: "session",
908
+ slash: {
909
+ name: "copy",
910
+ },
911
+ onSelect: async (dialog) => {
912
+ try {
913
+ const sessionData = session()
914
+ if (!sessionData) return
915
+ const sessionMessages = messages()
916
+ const transcript = formatTranscript(
917
+ sessionData,
918
+ sessionMessages.map((msg) => ({ info: msg, parts: sync.data.part[msg.id] ?? [] })),
919
+ {
920
+ thinking: showThinking(),
921
+ toolDetails: showDetails(),
922
+ assistantMetadata: showAssistantMetadata(),
923
+ providers: sync.data.provider,
924
+ },
925
+ )
926
+ await Clipboard.copy(transcript)
927
+ toast.show({ message: "Session transcript copied to clipboard!", variant: "success" })
928
+ } catch {
929
+ toast.show({ message: "Failed to copy session transcript", variant: "error" })
930
+ }
931
+ dialog.clear()
932
+ },
933
+ },
934
+ {
935
+ title: t("tui.command.session.export.title"),
936
+ value: "session.export",
937
+ keybind: "session_export",
938
+ category: "session",
939
+ slash: {
940
+ name: "export",
941
+ },
942
+ onSelect: async (dialog) => {
943
+ try {
944
+ const sessionData = session()
945
+ if (!sessionData) return
946
+ const sessionMessages = messages()
947
+
948
+ const defaultFilename = `session-${sessionData.id.slice(0, 8)}.md`
949
+
950
+ const options = await DialogExportOptions.show(
951
+ dialog,
952
+ defaultFilename,
953
+ showThinking(),
954
+ showDetails(),
955
+ showAssistantMetadata(),
956
+ false,
957
+ )
958
+
959
+ if (options === null) return
960
+
961
+ const transcript = formatTranscript(
962
+ sessionData,
963
+ sessionMessages.map((msg) => ({ info: msg, parts: sync.data.part[msg.id] ?? [] })),
964
+ {
965
+ thinking: options.thinking,
966
+ toolDetails: options.toolDetails,
967
+ assistantMetadata: options.assistantMetadata,
968
+ providers: sync.data.provider,
969
+ },
970
+ )
971
+
972
+ if (options.openWithoutSaving) {
973
+ // Just open in editor without saving
974
+ await Editor.open({ value: transcript, renderer })
975
+ } else {
976
+ const exportDir = process.cwd()
977
+ const filename = options.filename.trim()
978
+ const filepath = path.join(exportDir, filename)
979
+
980
+ await Filesystem.write(filepath, transcript)
981
+
982
+ // Open with EDITOR if available
983
+ const result = await Editor.open({ value: transcript, renderer })
984
+ if (result !== undefined) {
985
+ await Filesystem.write(filepath, result)
986
+ }
987
+
988
+ toast.show({ message: `Session exported to ${filename}`, variant: "success" })
989
+ }
990
+ } catch {
991
+ toast.show({ message: "Failed to export session", variant: "error" })
992
+ }
993
+ dialog.clear()
994
+ },
995
+ },
996
+ {
997
+ title: t("tui.command.session.child_first.title"),
998
+ value: "session.child.first",
999
+ keybind: "session_child_first",
1000
+ category: "session",
1001
+ hidden: true,
1002
+ onSelect: (dialog) => {
1003
+ moveFirstChild()
1004
+ dialog.clear()
1005
+ },
1006
+ },
1007
+ {
1008
+ title: t("tui.command.session.parent.title"),
1009
+ value: "session.parent",
1010
+ keybind: "session_parent",
1011
+ category: "session",
1012
+ hidden: true,
1013
+ enabled: currentAgentID() !== "main" || !!workflowRunID() || !!session()?.parentID,
1014
+ onSelect: (dialog) => {
1015
+ // Workflow page → back to the conversation (parallels agentID → main).
1016
+ if (fullRoute.data.type === "session" && workflowRunID()) {
1017
+ navigate({ ...fullRoute.data, workflowRunID: undefined })
1018
+ dialog.clear()
1019
+ return
1020
+ }
1021
+ // Agent opened FROM a workflow page → back returns to that workflow.
1022
+ if (fullRoute.data.type === "session" && currentAgentID() !== "main" && fromWorkflowRunID()) {
1023
+ navigate({ ...fullRoute.data, agentID: undefined, fromWorkflowRunID: undefined, workflowRunID: fromWorkflowRunID() })
1024
+ dialog.clear()
1025
+ return
1026
+ }
1027
+ if (fullRoute.data.type === "session" && currentAgentID() !== "main") {
1028
+ navigate({ ...fullRoute.data, agentID: undefined })
1029
+ dialog.clear()
1030
+ return
1031
+ }
1032
+ const parentID = session()?.parentID
1033
+ if (parentID) {
1034
+ navigate({
1035
+ type: "session",
1036
+ sessionID: parentID,
1037
+ })
1038
+ }
1039
+ dialog.clear()
1040
+ },
1041
+ },
1042
+ {
1043
+ title: t("tui.command.session.child_next.title"),
1044
+ value: "session.child.next",
1045
+ keybind: "session_child_cycle",
1046
+ category: "session",
1047
+ hidden: true,
1048
+ onSelect: (dialog) => {
1049
+ moveChild(1)
1050
+ dialog.clear()
1051
+ },
1052
+ },
1053
+ {
1054
+ title: t("tui.command.session.child_previous.title"),
1055
+ value: "session.child.previous",
1056
+ keybind: "session_child_cycle_reverse",
1057
+ category: "session",
1058
+ hidden: true,
1059
+ onSelect: (dialog) => {
1060
+ moveChild(-1)
1061
+ dialog.clear()
1062
+ },
1063
+ },
1064
+ ])
1065
+
1066
+ const revertInfo = createMemo(() => session()?.revert)
1067
+ const revertMessageID = createMemo(() => revertInfo()?.messageID)
1068
+
1069
+ const revertDiffFiles = createMemo(() => getRevertDiffFiles(revertInfo()?.diff ?? ""))
1070
+
1071
+ const revertRevertedMessages = createMemo(() => {
1072
+ const messageID = revertMessageID()
1073
+ if (!messageID) return []
1074
+ return messages().filter((x) => x.id >= messageID && x.role === "user")
1075
+ })
1076
+
1077
+ const revert = createMemo(() => {
1078
+ const info = revertInfo()
1079
+ if (!info) return
1080
+ if (!info.messageID) return
1081
+ return {
1082
+ messageID: info.messageID,
1083
+ reverted: revertRevertedMessages(),
1084
+ diff: info.diff,
1085
+ diffFiles: revertDiffFiles(),
1086
+ }
1087
+ })
1088
+
1089
+ // snap to bottom when session changes
1090
+ createEffect(on(() => route.sessionID, toBottom))
1091
+
1092
+ return (
1093
+ <context.Provider
1094
+ value={{
1095
+ get width() {
1096
+ return contentWidth()
1097
+ },
1098
+ sessionID: route.sessionID,
1099
+ conceal,
1100
+ thinkingMode,
1101
+ showThinking,
1102
+ showTimestamps,
1103
+ showDetails,
1104
+ showGenericToolOutput,
1105
+ diffWrapMode,
1106
+ providers,
1107
+ sync,
1108
+ tui: tuiConfig,
1109
+ }}
1110
+ >
1111
+ <box flexDirection="row">
1112
+ <box flexGrow={1} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} onMouse={onWheel}>
1113
+ <Show
1114
+ when={!workflowRunID()}
1115
+ fallback={
1116
+ <WorkflowPage
1117
+ runID={workflowRunID()!}
1118
+ onBack={() => navigate({ ...route, workflowRunID: undefined })}
1119
+ onOpenAgent={(actorID) =>
1120
+ navigate({ ...route, workflowRunID: undefined, agentID: actorID, fromWorkflowRunID: workflowRunID() })
1121
+ }
1122
+ onOpenChild={(childRunID) => navigate({ ...route, workflowRunID: childRunID })}
1123
+ />
1124
+ }
1125
+ >
1126
+ <Show when={session()}>
1127
+ <scrollbox
1128
+ ref={(r) => (scroll = r)}
1129
+ viewportOptions={{
1130
+ paddingRight: 1,
1131
+ }}
1132
+ verticalScrollbarOptions={{
1133
+ paddingLeft: 1,
1134
+ visible: true,
1135
+ trackOptions: {
1136
+ backgroundColor: scrollbarVisible() ? theme.backgroundElement : theme.background,
1137
+ foregroundColor: scrollbarVisible() ? theme.border : theme.background,
1138
+ },
1139
+ }}
1140
+ stickyScroll={true}
1141
+ stickyStart="bottom"
1142
+ flexGrow={1}
1143
+ scrollAcceleration={scrollAcceleration()}
1144
+ >
1145
+ <box height={1} />
1146
+ <For each={messages()}>
1147
+ {(message, index) => (
1148
+ <Switch>
1149
+ <Match when={message.id === revert()?.messageID}>
1150
+ {(function () {
1151
+ const command = useCommandDialog()
1152
+ const [hover, setHover] = createSignal(false)
1153
+ const dialog = useDialog()
1154
+
1155
+ const handleUnrevert = async () => {
1156
+ const confirmed = await DialogConfirm.show(
1157
+ dialog,
1158
+ "Confirm Redo",
1159
+ "Are you sure you want to restore the reverted messages?",
1160
+ )
1161
+ if (confirmed) {
1162
+ command.trigger("session.redo")
1163
+ }
1164
+ }
1165
+
1166
+ return (
1167
+ <box
1168
+ onMouseOver={() => setHover(true)}
1169
+ onMouseOut={() => setHover(false)}
1170
+ onMouseUp={handleUnrevert}
1171
+ marginTop={1}
1172
+ flexShrink={0}
1173
+ border={["left"]}
1174
+ customBorderChars={SplitBorder.customBorderChars}
1175
+ borderColor={theme.backgroundPanel}
1176
+ >
1177
+ <box
1178
+ paddingTop={1}
1179
+ paddingBottom={1}
1180
+ paddingLeft={2}
1181
+ backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
1182
+ >
1183
+ <text fg={theme.textMuted}>{revert()!.reverted.length} message reverted</text>
1184
+ <text fg={theme.textMuted}>
1185
+ <span style={{ fg: theme.text }}>{keybind.print("messages_redo")}</span> or /redo to
1186
+ restore
1187
+ </text>
1188
+ <Show when={revert()!.diffFiles?.length}>
1189
+ <box marginTop={1}>
1190
+ <For each={revert()!.diffFiles}>
1191
+ {(file) => (
1192
+ <text fg={theme.text}>
1193
+ {file.filename}
1194
+ <Show when={file.additions > 0}>
1195
+ <span style={{ fg: theme.diffAdded }}> +{file.additions}</span>
1196
+ </Show>
1197
+ <Show when={file.deletions > 0}>
1198
+ <span style={{ fg: theme.diffRemoved }}> -{file.deletions}</span>
1199
+ </Show>
1200
+ </text>
1201
+ )}
1202
+ </For>
1203
+ </box>
1204
+ </Show>
1205
+ </box>
1206
+ </box>
1207
+ )
1208
+ })()}
1209
+ </Match>
1210
+ <Match when={revert()?.messageID && message.id >= revert()!.messageID}>
1211
+ <></>
1212
+ </Match>
1213
+ <Match when={message.role === "user"}>
1214
+ <UserMessage
1215
+ index={index()}
1216
+ onMouseUp={() => {
1217
+ if (renderer.getSelection()?.getSelectedText()) return
1218
+ dialog.replace(() => (
1219
+ <DialogMessage
1220
+ messageID={message.id}
1221
+ sessionID={route.sessionID}
1222
+ setPrompt={(promptInfo) => prompt?.set(promptInfo)}
1223
+ />
1224
+ ))
1225
+ }}
1226
+ message={message as UserMessage}
1227
+ parts={sync.data.part[message.id] ?? []}
1228
+ pending={pending()}
1229
+ />
1230
+ </Match>
1231
+ <Match when={message.role === "assistant"}>
1232
+ <AssistantMessage
1233
+ last={lastAssistant()?.id === message.id}
1234
+ message={message as AssistantMessage}
1235
+ parts={sync.data.part[message.id] ?? []}
1236
+ />
1237
+ </Match>
1238
+ </Switch>
1239
+ )}
1240
+ </For>
1241
+ </scrollbox>
1242
+ <box flexShrink={0}>
1243
+ <Show when={permissions().length > 0}>
1244
+ <PermissionPrompt request={permissions()[0]} />
1245
+ </Show>
1246
+ <Show when={permissions().length === 0 && questions().length > 0}>
1247
+ <QuestionPrompt request={questions()[0]} />
1248
+ </Show>
1249
+ <Show when={session()?.parentID || currentAgentID() !== "main"}>
1250
+ <SubagentFooter />
1251
+ </Show>
1252
+ <Show when={visible()}>
1253
+ <TuiPluginRuntime.Slot
1254
+ name="session_prompt"
1255
+ mode="replace"
1256
+ session_id={route.sessionID}
1257
+ visible={visible()}
1258
+ disabled={disabled()}
1259
+ on_submit={toBottom}
1260
+ ref={bind}
1261
+ >
1262
+ <Prompt
1263
+ visible={visible()}
1264
+ ref={bind}
1265
+ disabled={disabled()}
1266
+ onSubmit={() => {
1267
+ toBottom()
1268
+ }}
1269
+ sessionID={route.sessionID}
1270
+ right={<TuiPluginRuntime.Slot name="session_prompt_right" session_id={route.sessionID} />}
1271
+ />
1272
+ </TuiPluginRuntime.Slot>
1273
+ </Show>
1274
+ </box>
1275
+ </Show>
1276
+ </Show>
1277
+ <Toast />
1278
+ </box>
1279
+ <Show when={sidebarVisible()}>
1280
+ <Switch>
1281
+ <Match when={wide()}>
1282
+ <Sidebar sessionID={route.sessionID} onToggle={() => {
1283
+ batch(() => {
1284
+ setSidebar("hide")
1285
+ setSidebarOpen(false)
1286
+ })
1287
+ }} />
1288
+ </Match>
1289
+ <Match when={!wide()}>
1290
+ <box
1291
+ position="absolute"
1292
+ top={0}
1293
+ left={0}
1294
+ right={0}
1295
+ bottom={0}
1296
+ alignItems="flex-end"
1297
+ backgroundColor={RGBA.fromInts(0, 0, 0, 70)}
1298
+ >
1299
+ <Sidebar sessionID={route.sessionID} onToggle={() => {
1300
+ batch(() => {
1301
+ setSidebar("hide")
1302
+ setSidebarOpen(false)
1303
+ })
1304
+ }} />
1305
+ </box>
1306
+ </Match>
1307
+ </Switch>
1308
+ </Show>
1309
+ </box>
1310
+ </context.Provider>
1311
+ )
1312
+ }
1313
+
1314
+ const MIME_BADGE: Record<string, string> = {
1315
+ "text/plain": "txt",
1316
+ "image/png": "img",
1317
+ "image/jpeg": "img",
1318
+ "image/gif": "img",
1319
+ "image/webp": "img",
1320
+ "application/pdf": "pdf",
1321
+ "application/x-directory": "dir",
1322
+ }
1323
+
1324
+ function UserMessage(props: {
1325
+ message: UserMessage
1326
+ parts: Part[]
1327
+ onMouseUp: () => void
1328
+ index: number
1329
+ pending?: string
1330
+ }) {
1331
+ const ctx = use()
1332
+ const local = useLocal()
1333
+ const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
1334
+ const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
1335
+ const { theme } = useTheme()
1336
+ const [hover, setHover] = createSignal(false)
1337
+ const queued = createMemo(() => props.pending && props.message.id > props.pending)
1338
+ const color = createMemo(() => getAgentBrandColor(props.message.agent))
1339
+ const queuedFg = createMemo(() => selectedForeground(theme, color()))
1340
+ const metadataVisible = createMemo(() => queued() || ctx.showTimestamps())
1341
+
1342
+ return (
1343
+ <>
1344
+ <Show when={text()}>
1345
+ <box
1346
+ id={props.message.id}
1347
+ border={["top", "right", "bottom", "left"]}
1348
+ borderColor={theme.primary}
1349
+ customBorderChars={{
1350
+ topLeft: "╭",
1351
+ topRight: "╮",
1352
+ bottomLeft: "╰",
1353
+ bottomRight: "╯",
1354
+ horizontal: "─",
1355
+ vertical: "│",
1356
+ topT: "┬",
1357
+ bottomT: "┴",
1358
+ cross: "┼",
1359
+ leftT: "├",
1360
+ rightT: "┤",
1361
+ }}
1362
+ marginTop={props.index === 0 ? 0 : 1}
1363
+ >
1364
+ <box
1365
+ onMouseOver={() => {
1366
+ setHover(true)
1367
+ }}
1368
+ onMouseOut={() => {
1369
+ setHover(false)
1370
+ }}
1371
+ onMouseUp={props.onMouseUp}
1372
+ paddingTop={1}
1373
+ paddingBottom={1}
1374
+ paddingLeft={2}
1375
+ backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
1376
+ flexShrink={0}
1377
+ >
1378
+ <text fg={theme.text}>{text()?.text}</text>
1379
+ <Show when={files().length}>
1380
+ <box flexDirection="row" paddingBottom={metadataVisible() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
1381
+ <For each={files()}>
1382
+ {(file) => {
1383
+ const bg = createMemo(() => {
1384
+ if (file.mime.startsWith("image/")) return theme.accent
1385
+ if (file.mime === "application/pdf") return theme.primary
1386
+ return theme.secondary
1387
+ })
1388
+ return (
1389
+ <text fg={theme.text}>
1390
+ <span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
1391
+ <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
1392
+ </text>
1393
+ )
1394
+ }}
1395
+ </For>
1396
+ </box>
1397
+ </Show>
1398
+ <Show
1399
+ when={queued()}
1400
+ fallback={
1401
+ <Show when={ctx.showTimestamps()}>
1402
+ <text fg={theme.textMuted}>
1403
+ <span style={{ fg: theme.textMuted }}>
1404
+ {Locale.todayTimeOrDateTime(props.message.time.created)}
1405
+ </span>
1406
+ </text>
1407
+ </Show>
1408
+ }
1409
+ >
1410
+ <text fg={theme.textMuted}>
1411
+ <span style={{ bg: color(), fg: queuedFg(), bold: true }}> QUEUED </span>
1412
+ </text>
1413
+ </Show>
1414
+ </box>
1415
+ </box>
1416
+ </Show>
1417
+ </>
1418
+ )
1419
+ }
1420
+
1421
+ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean }) {
1422
+ const ctx = use()
1423
+ const local = useLocal()
1424
+ const { theme } = useTheme()
1425
+ const sync = useSync()
1426
+ const toast = useToast()
1427
+ const renderer = useRenderer()
1428
+ const t = useLanguage().t
1429
+ const [copyHover, setCopyHover] = createSignal(false)
1430
+ const messages = createMemo(() => sync.data.message[props.message.sessionID]?.[props.message.agentID ?? "main"] ?? [])
1431
+ const model = createMemo(() =>
1432
+ props.message.modelID === "mimo-auto"
1433
+ ? t("tui.model.mimo_auto.name")
1434
+ : Model.name(ctx.providers(), props.message.providerID, props.message.modelID),
1435
+ )
1436
+
1437
+ const final = createMemo(() => {
1438
+ return props.message.finish && props.message.finish !== "tool-calls"
1439
+ })
1440
+
1441
+ const duration = createMemo(() => {
1442
+ if (!final()) return 0
1443
+ if (!props.message.time.completed) return 0
1444
+ const user = messages().find((x) => x.role === "user" && x.id === props.message.parentID)
1445
+ if (!user || !user.time) return 0
1446
+ return props.message.time.completed - user.time.created
1447
+ })
1448
+
1449
+ const keybind = useKeybind()
1450
+
1451
+ const handleCopy = () => {
1452
+ if (renderer.getSelection()?.getSelectedText()) return
1453
+ const text = props.parts
1454
+ .filter((p) => p.type === "text")
1455
+ .map((p) => (p as TextPart).text)
1456
+ .join("\n")
1457
+ .trim()
1458
+ if (!text) return
1459
+ Clipboard.copy(text)
1460
+ .then(() => toast.show({ message: t("tui.toast.copied_to_clipboard"), variant: "success" }))
1461
+ .catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" }))
1462
+ }
1463
+
1464
+ // Goal judge verdict for this specific turn, if the stop-condition judge
1465
+ // evaluated it. Rendered as a foldable per-turn marker so the user can trace
1466
+ // back which turn failed the check — without polluting the message stream.
1467
+ const verdict = createMemo(() => sync.data.session_goal?.[props.message.sessionID]?.verdicts?.[props.message.id])
1468
+ const [verdictOpen, setVerdictOpen] = createSignal(false)
1469
+ const verdictMark = createMemo(() => {
1470
+ const v = verdict()
1471
+ if (!v) return undefined
1472
+ if (v.error) return { icon: "!", fg: theme.textMuted, label: "Judge: error (stopped)" }
1473
+ if (v.ok) return { icon: "✓", fg: theme.success, label: "Judge: met" }
1474
+ if (v.impossible) return { icon: "⊘", fg: theme.error, label: "Judge: impossible" }
1475
+ return { icon: "⟳", fg: theme.warning, label: `Judge [round ${v.attempt}]: not met` }
1476
+ })
1477
+
1478
+ // Both the `actor` and `workflow` tools spawn agents that register as
1479
+ // subagents in this session, so the `session_child_first` keybind opens the
1480
+ // Subagents panel for either. Advertise it with copy matching the tool that
1481
+ // produced the message; a mixed message falls back to the generic subagent
1482
+ // wording.
1483
+ const hasActorPart = createMemo(() => props.parts.some((x) => x.type === "tool" && x.tool === "actor"))
1484
+ const hasWorkflowPart = createMemo(() => props.parts.some((x) => x.type === "tool" && x.tool === "workflow"))
1485
+
1486
+ return (
1487
+ <>
1488
+ <For each={props.parts}>
1489
+ {(part, index) => {
1490
+ // The StructuredOutput tool call is the mechanism that produces
1491
+ // message.structured; we render that value as a dedicated colored block
1492
+ // below, so skip the redundant gray one-liner tool part here.
1493
+ if (part.type === "tool" && part.tool === "StructuredOutput") return null
1494
+ const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
1495
+ return (
1496
+ <Show when={component()}>
1497
+ <Dynamic
1498
+ last={index() === props.parts.length - 1}
1499
+ component={component()}
1500
+ part={part as any}
1501
+ message={props.message}
1502
+ />
1503
+ </Show>
1504
+ )
1505
+ }}
1506
+ </For>
1507
+ <Show when={props.message.structured !== undefined && props.message.structured !== null}>
1508
+ <StructuredOutput value={props.message.structured} />
1509
+ </Show>
1510
+ <Show when={hasActorPart() || hasWorkflowPart()}>
1511
+ <box paddingTop={1} paddingLeft={3}>
1512
+ <text fg={theme.text}>
1513
+ {keybind.print("session_child_first")}
1514
+ <span style={{ fg: theme.textMuted }}>{hasWorkflowPart() ? " view workflow agents" : " view subagents"}</span>
1515
+ </text>
1516
+ </box>
1517
+ </Show>
1518
+ <Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
1519
+ <ErrorBlock error={props.message.error!} />
1520
+ </Show>
1521
+ <Switch>
1522
+ <Match when={props.last || final() || props.message.error?.name === "MessageAbortedError"}>
1523
+ <box paddingLeft={3} flexDirection="row" justifyContent="space-between" marginTop={1}>
1524
+ <text>
1525
+ <span
1526
+ style={{
1527
+ fg:
1528
+ props.message.error?.name === "MessageAbortedError"
1529
+ ? theme.textMuted
1530
+ : getAgentBrandColor(props.message.agent),
1531
+ }}
1532
+ >
1533
+ ▣{" "}
1534
+ </span>{" "}
1535
+ <span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>
1536
+ <span style={{ fg: theme.textMuted }}> · {model()}</span>
1537
+ <Show when={duration()}>
1538
+ <span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
1539
+ </Show>
1540
+ <Show when={props.message.error?.name === "MessageAbortedError"}>
1541
+ <span style={{ fg: theme.textMuted }}> · interrupted</span>
1542
+ </Show>
1543
+ </text>
1544
+ <Show when={props.message.time.completed}>
1545
+ <box
1546
+ onMouseOver={() => setCopyHover(true)}
1547
+ onMouseOut={() => setCopyHover(false)}
1548
+ onMouseUp={handleCopy}
1549
+ >
1550
+ <text fg={copyHover() ? theme.text : theme.textMuted}>⎘ copy</text>
1551
+ </box>
1552
+ </Show>
1553
+ </box>
1554
+ </Match>
1555
+ </Switch>
1556
+ <Show when={verdictMark()}>
1557
+ {(mark) => (
1558
+ <box paddingLeft={3} onMouseUp={() => setVerdictOpen((x) => !x)}>
1559
+ <text>
1560
+ <span style={{ fg: theme.textMuted }}>{verdictOpen() ? "▼" : "▶"} </span>
1561
+ <span style={{ fg: mark().fg }}>
1562
+ {mark().icon} {mark().label}
1563
+ </span>
1564
+ </text>
1565
+ <Show when={verdictOpen()}>
1566
+ <box paddingLeft={2}>
1567
+ <text fg={theme.textMuted} wrapMode="word">
1568
+ {verdict()!.reason}
1569
+ </text>
1570
+ </box>
1571
+ </Show>
1572
+ </box>
1573
+ )}
1574
+ </Show>
1575
+ </>
1576
+ )
1577
+ }
1578
+
1579
+ const PART_MAPPING = {
1580
+ text: TextPart,
1581
+ tool: ToolPart,
1582
+ reasoning: ReasoningPart,
1583
+ }
1584
+
1585
+ type MessageError = NonNullable<AssistantMessage["error"]>
1586
+
1587
+ function errorBody(error: MessageError): string {
1588
+ if (error.name === "MessageOutputLengthError") return "Output length limit reached"
1589
+ return (error.data as { message?: string }).message ?? "Unknown error"
1590
+ }
1591
+
1592
+ function errorMeta(error: MessageError): string | undefined {
1593
+ if (error.name === "APIError") {
1594
+ const parts: string[] = []
1595
+ if (error.data.statusCode !== undefined) parts.push(`status ${error.data.statusCode}`)
1596
+ parts.push(error.data.isRetryable ? "retryable" : "non-retryable")
1597
+ return parts.join(" · ")
1598
+ }
1599
+ if (error.name === "ProviderAuthError") return `provider: ${error.data.providerID}`
1600
+ if (error.name === "StructuredOutputError") return `retries: ${error.data.retries}`
1601
+ return undefined
1602
+ }
1603
+
1604
+ function ErrorBlock(props: { error: MessageError }) {
1605
+ const { theme } = useTheme()
1606
+ const meta = createMemo(() => errorMeta(props.error))
1607
+ return (
1608
+ <box flexDirection="column" paddingLeft={3} marginTop={1}>
1609
+ <text fg={theme.error} wrapMode="word">
1610
+ <span style={{ fg: theme.error }}>✗ </span>
1611
+ {errorBody(props.error)}
1612
+ </text>
1613
+ <Show when={meta()}>
1614
+ <box paddingLeft={3}>
1615
+ <text fg={theme.textMuted} wrapMode="word">
1616
+ {meta()}
1617
+ </text>
1618
+ </box>
1619
+ </Show>
1620
+ </box>
1621
+ )
1622
+ }
1623
+
1624
+ // Structured output is a message-level field (AssistantMessage.structured), not a
1625
+ // part, so the parts loop never shows it. Agents called with a schema (common in
1626
+ // workflows) put their whole answer here — render it as syntax-highlighted JSON so
1627
+ // it's not invisible. Collapsible for large payloads.
1628
+ function StructuredOutput(props: { value: unknown }) {
1629
+ const { theme, syntax } = useTheme()
1630
+ const [collapsed, setCollapsed] = createSignal(false)
1631
+ const json = createMemo(() => {
1632
+ try {
1633
+ return JSON.stringify(props.value, null, 2)
1634
+ } catch {
1635
+ return String(props.value)
1636
+ }
1637
+ })
1638
+ const lineCount = createMemo(() => json().split("\n").length)
1639
+ const overflow = createMemo(() => lineCount() > 20)
1640
+ const shown = createMemo(() => (collapsed() ? json().split("\n").slice(0, 20).join("\n") + "\n…" : json()))
1641
+ return (
1642
+ <box paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0}>
1643
+ <box flexDirection="row" gap={1} onMouseUp={() => overflow() && setCollapsed((p) => !p)}>
1644
+ <text fg={theme.accent} attributes={TextAttributes.BOLD}>
1645
+ ⊟ structured output
1646
+ </text>
1647
+ <Show when={overflow()}>
1648
+ <text fg={theme.textMuted}>
1649
+ · {lineCount()} lines{collapsed() ? " · click to expand" : ""}
1650
+ </text>
1651
+ </Show>
1652
+ </box>
1653
+ <box marginTop={1}>
1654
+ <code filetype="json" drawUnstyledText={false} syntaxStyle={syntax()} content={shown()} fg={theme.text} />
1655
+ </box>
1656
+ </box>
1657
+ )
1658
+ }
1659
+
1660
+ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) {
1661
+ const { theme, subtleSyntax } = useTheme()
1662
+ const ctx = use()
1663
+ const [expanded, setExpanded] = createSignal(false)
1664
+
1665
+ const content = createMemo(() => {
1666
+ return props.part.text.replace("[REDACTED]", "").trim()
1667
+ })
1668
+ const isDone = createMemo(() => props.part.time.end !== undefined)
1669
+ const inMinimal = createMemo(() => ctx.thinkingMode() === "hide")
1670
+ const duration = createMemo(() => {
1671
+ const end = props.part.time.end
1672
+ return end === undefined ? 0 : Math.max(0, end - props.part.time.start)
1673
+ })
1674
+ const summary = createMemo(() => reasoningSummary(content()))
1675
+
1676
+ const toggle = () => {
1677
+ if (!inMinimal()) return
1678
+ setExpanded((prev) => !prev)
1679
+ }
1680
+
1681
+ return (
1682
+ <Show when={content()}>
1683
+ <box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0}>
1684
+ <box onMouseUp={toggle}>
1685
+ <ReasoningHeader
1686
+ toggleable={inMinimal()}
1687
+ open={!inMinimal() || expanded()}
1688
+ done={isDone()}
1689
+ title={summary().title}
1690
+ duration={isDone() ? Locale.duration(duration()) : undefined}
1691
+ />
1692
+ </box>
1693
+ <Show when={(!inMinimal() || expanded()) && summary().body}>
1694
+ <box paddingLeft={inMinimal() ? 2 : 0} marginTop={1}>
1695
+ <code
1696
+ filetype="markdown"
1697
+ drawUnstyledText={false}
1698
+ streaming={true}
1699
+ syntaxStyle={subtleSyntax()}
1700
+ content={summary().body}
1701
+ conceal={ctx.conceal()}
1702
+ fg={theme.textMuted}
1703
+ />
1704
+ </box>
1705
+ </Show>
1706
+ </box>
1707
+ </Show>
1708
+ )
1709
+ }
1710
+
1711
+ function ReasoningHeader(props: {
1712
+ toggleable: boolean
1713
+ open: boolean
1714
+ done: boolean
1715
+ title: string | null
1716
+ duration?: string
1717
+ }) {
1718
+ const { theme } = useTheme()
1719
+ const fg = () =>
1720
+ props.open
1721
+ ? RGBA.fromValues(theme.warning.r, theme.warning.g, theme.warning.b, theme.thinkingOpacity)
1722
+ : theme.warning
1723
+
1724
+ return (
1725
+ <Switch>
1726
+ <Match when={!props.done}>
1727
+ <box flexDirection="row">
1728
+ <Spinner color={fg()}>{props.title ? "Thinking: " + props.title : "Thinking"}</Spinner>
1729
+ </box>
1730
+ </Match>
1731
+ <Match when={true}>
1732
+ <text fg={fg()} wrapMode="none">
1733
+ <Show when={props.toggleable}>
1734
+ <span>{props.open ? "- " : "+ "}</span>
1735
+ </Show>
1736
+ <span>Thought</span>
1737
+ <Show when={props.title || props.duration}>
1738
+ <span>: </span>
1739
+ </Show>
1740
+ <Show when={props.title}>
1741
+ <span>{props.title}</span>
1742
+ </Show>
1743
+ <Show when={props.duration}>
1744
+ <span>
1745
+ {props.title ? " · " : ""}
1746
+ {props.duration}
1747
+ </span>
1748
+ </Show>
1749
+ </text>
1750
+ </Match>
1751
+ </Switch>
1752
+ )
1753
+ }
1754
+
1755
+ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) {
1756
+ const ctx = use()
1757
+ const { theme, syntax } = useTheme()
1758
+ return (
1759
+ <Show when={props.part.text.trim()}>
1760
+ <box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
1761
+ <Switch>
1762
+ <Match when={Flag.OPENDASH_EXPERIMENTAL_MARKDOWN}>
1763
+ <markdown
1764
+ syntaxStyle={syntax()}
1765
+ streaming={true}
1766
+ content={props.part.text.trim()}
1767
+ conceal={ctx.conceal()}
1768
+ fg={theme.markdownText}
1769
+ bg={theme.background}
1770
+ />
1771
+ </Match>
1772
+ <Match when={!Flag.OPENDASH_EXPERIMENTAL_MARKDOWN}>
1773
+ <code
1774
+ filetype="markdown"
1775
+ drawUnstyledText={false}
1776
+ streaming={true}
1777
+ syntaxStyle={syntax()}
1778
+ content={props.part.text.trim()}
1779
+ conceal={ctx.conceal()}
1780
+ fg={theme.text}
1781
+ />
1782
+ </Match>
1783
+ </Switch>
1784
+ </box>
1785
+ </Show>
1786
+ )
1787
+ }
1788
+
1789
+ // Pending messages moved to individual tool pending functions
1790
+
1791
+ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMessage }) {
1792
+ const ctx = use()
1793
+ const sync = useSync()
1794
+
1795
+ // Hide tool if showDetails is false and tool completed successfully
1796
+ const shouldHide = createMemo(() => {
1797
+ if (ctx.showDetails()) return false
1798
+ if (props.part.state.status !== "completed") return false
1799
+ return true
1800
+ })
1801
+
1802
+ const toolprops = {
1803
+ get metadata() {
1804
+ return props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
1805
+ },
1806
+ get input() {
1807
+ return props.part.state.input ?? {}
1808
+ },
1809
+ get output() {
1810
+ return props.part.state.status === "completed" ? props.part.state.output : undefined
1811
+ },
1812
+ get permission() {
1813
+ const permissions = sync.data.permission[props.message.sessionID] ?? []
1814
+ const permissionIndex = permissions.findIndex((x) => x.tool?.callID === props.part.callID)
1815
+ return permissions[permissionIndex]
1816
+ },
1817
+ get tool() {
1818
+ return props.part.tool
1819
+ },
1820
+ get part() {
1821
+ return props.part
1822
+ },
1823
+ }
1824
+
1825
+ return (
1826
+ <Show when={!shouldHide()}>
1827
+ <Switch>
1828
+ <Match when={props.part.tool === "bash"}>
1829
+ <Bash {...toolprops} />
1830
+ </Match>
1831
+ <Match when={props.part.tool === "glob"}>
1832
+ <Glob {...toolprops} />
1833
+ </Match>
1834
+ <Match when={props.part.tool === "read"}>
1835
+ <Read {...toolprops} />
1836
+ </Match>
1837
+ <Match when={props.part.tool === "grep"}>
1838
+ <Grep {...toolprops} />
1839
+ </Match>
1840
+ <Match when={props.part.tool === "webfetch"}>
1841
+ <WebFetch {...toolprops} />
1842
+ </Match>
1843
+ <Match when={props.part.tool === "codesearch"}>
1844
+ <CodeSearch {...toolprops} />
1845
+ </Match>
1846
+ <Match when={props.part.tool === "websearch"}>
1847
+ <WebSearch {...toolprops} />
1848
+ </Match>
1849
+ <Match when={props.part.tool === "write"}>
1850
+ <Write {...toolprops} />
1851
+ </Match>
1852
+ <Match when={props.part.tool === "edit"}>
1853
+ <Edit {...toolprops} />
1854
+ </Match>
1855
+ <Match when={props.part.tool === "actor"}>
1856
+ <Task {...toolprops} />
1857
+ </Match>
1858
+ <Match when={props.part.tool === "task"}>
1859
+ <WorkItemTask {...toolprops} />
1860
+ </Match>
1861
+ <Match when={props.part.tool === "apply_patch"}>
1862
+ <ApplyPatch {...toolprops} />
1863
+ </Match>
1864
+ <Match when={props.part.tool === "question"}>
1865
+ <Question {...toolprops} />
1866
+ </Match>
1867
+ <Match when={props.part.tool === "skill"}>
1868
+ <Skill {...toolprops} />
1869
+ </Match>
1870
+ <Match when={props.part.tool === "workflow"}>
1871
+ <Workflow {...toolprops} />
1872
+ </Match>
1873
+ <Match when={props.part.tool === "plan_exit"}>
1874
+ <PlanExit {...toolprops} />
1875
+ </Match>
1876
+ <Match when={true}>
1877
+ <GenericTool {...toolprops} />
1878
+ </Match>
1879
+ </Switch>
1880
+ </Show>
1881
+ )
1882
+ }
1883
+
1884
+ type ToolProps<T> = {
1885
+ input: Partial<Tool.InferParameters<T>>
1886
+ metadata: Partial<Tool.InferMetadata<T>>
1887
+ permission: Record<string, any>
1888
+ tool: string
1889
+ output?: string
1890
+ part: ToolPart
1891
+ }
1892
+ function PlanExit(props: ToolProps<any>) {
1893
+ const { theme } = useTheme()
1894
+ const dismissed = createMemo(
1895
+ () => props.part.state.status === "completed" && props.part.state.metadata?.switched === false,
1896
+ )
1897
+ const feedback = createMemo(() => (dismissed() ? props.metadata.feedback : undefined))
1898
+
1899
+ return (
1900
+ <>
1901
+ <InlineTool icon="⚙" pending="Asking..." complete={true} part={props.part} dismissed={dismissed()}>
1902
+ plan_exit
1903
+ </InlineTool>
1904
+ <Show when={feedback()}>
1905
+ <box paddingLeft={6}>
1906
+ <text fg={theme.textMuted}>{feedback()}</text>
1907
+ </box>
1908
+ </Show>
1909
+ </>
1910
+ )
1911
+ }
1912
+
1913
+ function GenericTool(props: ToolProps<any>) {
1914
+ const { theme } = useTheme()
1915
+ const ctx = use()
1916
+ const output = createMemo(() => props.output?.trim() ?? "")
1917
+ const [expanded, setExpanded] = createSignal(false)
1918
+ const lines = createMemo(() => output().split("\n"))
1919
+ const maxLines = 3
1920
+ const overflow = createMemo(() => lines().length > maxLines)
1921
+ const limited = createMemo(() => {
1922
+ if (expanded() || !overflow()) return output()
1923
+ return [...lines().slice(0, maxLines), "…"].join("\n")
1924
+ })
1925
+
1926
+ return (
1927
+ <Show
1928
+ when={props.output && ctx.showGenericToolOutput()}
1929
+ fallback={
1930
+ <InlineTool icon="⚙" pending="Writing command..." complete={true} part={props.part}>
1931
+ {props.tool} {input(props.input)}
1932
+ </InlineTool>
1933
+ }
1934
+ >
1935
+ <BlockTool
1936
+ title={`# ${props.tool} ${input(props.input)}`}
1937
+ part={props.part}
1938
+ onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
1939
+ >
1940
+ <box gap={1}>
1941
+ <text fg={theme.text}>{limited()}</text>
1942
+ <Show when={overflow()}>
1943
+ <text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
1944
+ </Show>
1945
+ </box>
1946
+ </BlockTool>
1947
+ </Show>
1948
+ )
1949
+ }
1950
+
1951
+ // Inline renderer for the work-item `task` tool (distinct from <Task>, which
1952
+ // renders the subagent-spawning `actor` tool). Shows the operation as a concise
1953
+ // one-liner derived from the nested `{ operation: { action } }` discriminator,
1954
+ // so task create/start/done don't fall through to GenericTool's raw-JSON dump.
1955
+ function WorkItemTask(props: ToolProps<typeof TaskTool>) {
1956
+ const summary = createMemo(() => {
1957
+ const op = (props.input as { operation?: Record<string, any> }).operation
1958
+ if (!op || typeof op !== "object") return "task"
1959
+ const verb = typeof op.action === "string" ? op.action : "task"
1960
+ if (verb === "create") return op.summary ? `create "${op.summary}"` : "create"
1961
+ if (verb === "list") return op.status ? `list ${op.status}` : "list"
1962
+ if (op.id) return `${verb} ${op.id}`
1963
+ return verb
1964
+ })
1965
+ return (
1966
+ <InlineTool icon="#" pending="Updating tasks..." complete={true} part={props.part}>
1967
+ task {summary()}
1968
+ </InlineTool>
1969
+ )
1970
+ }
1971
+
1972
+ // Inline renderer for the dynamic-workflow `workflow` tool. The "run" op blocks
1973
+ // until terminal and streams a transcript (phase transitions + log() messages)
1974
+ // into part-state metadata via ctx.metadata; the tool's `Workflow` view here
1975
+ // reads metadata.transcript reactively (each ctx.metadata call fires a
1976
+ // message.part.delta) and renders it as multi-line chat content alongside the
1977
+ // live header from sync.data.workflow[runID]. That way phase/log events show
1978
+ // up in the main agent's conversation as the workflow runs, not as a single
1979
+ // silent line that only updates once the run finishes.
1980
+ function Workflow(props: ToolProps<typeof WorkflowTool>) {
1981
+ const sync = useSync()
1982
+ const fullRoute = useRoute()
1983
+
1984
+ const operation = createMemo(() => {
1985
+ const op = (props.input as { operation?: string }).operation
1986
+ return typeof op === "string" ? op : "run"
1987
+ })
1988
+
1989
+ const runID = createMemo(
1990
+ () => (props.metadata.runID as string | undefined) ?? (props.input as { run_id?: string }).run_id,
1991
+ )
1992
+
1993
+ const run = createMemo(() => {
1994
+ const id = runID()
1995
+ if (!id) return undefined
1996
+ return sync.data.workflow[id]
1997
+ })
1998
+
1999
+ // Spinner is true while EITHER side reports running — the tool part stays
2000
+ // running until execute() returns (the whole workflow duration, since we
2001
+ // block), and the bus-fed run row independently reports "running" until the
2002
+ // workflow.finished event lands. Either signal alone is enough.
2003
+ const isRunning = createMemo(() => {
2004
+ if (props.part.state.status === "running") return true
2005
+ const r = run()
2006
+ return r?.status === "running"
2007
+ })
2008
+
2009
+ const transcript = createMemo(() => {
2010
+ const t = (props.metadata as { transcript?: { kind: "phase" | "log"; text: string }[] }).transcript
2011
+ return Array.isArray(t) ? t : []
2012
+ })
2013
+
2014
+ const name = createMemo(() => run()?.name ?? (props.input as { name?: string }).name ?? "inline")
2015
+ const status = createMemo(() => run()?.status ?? (props.metadata.status as string | undefined))
2016
+
2017
+ // Counters/phase prefer the live metadata streamed by the tool's 250ms flush
2018
+ // loop, falling back to the bus run row. The bus row only learns counters via
2019
+ // loadWorkflows polling (which only the /workflows dialog runs), so during a run
2020
+ // the inline panel would otherwise sit at 0✓ 0✗ 0⟳ — the streamed metadata is
2021
+ // the authoritative live source for this in-conversation view.
2022
+ const counters = createMemo(() => {
2023
+ const m = (props.metadata as { counters?: { running: number; succeeded: number; failed: number } }).counters
2024
+ if (m) return m
2025
+ const r = run()
2026
+ return r ? { running: r.running, succeeded: r.succeeded, failed: r.failed } : undefined
2027
+ })
2028
+ const currentPhase = createMemo(
2029
+ () => (props.metadata as { currentPhase?: string }).currentPhase ?? run()?.currentPhase,
2030
+ )
2031
+
2032
+ // Non-"run" ops (status/wait/cancel/resume) are one-shot control calls with no
2033
+ // live transcript — keep them as a compact inline line.
2034
+ return (
2035
+ <Show when={operation() === "run"} fallback={
2036
+ <InlineTool icon="⚡" spinner={isRunning()} pending="Starting workflow..." complete={true} part={props.part}>
2037
+ {`workflow ${operation()}${runID() ? ` ${runID()}` : ""}`}
2038
+ </InlineTool>
2039
+ }>
2040
+ <WorkflowPanel
2041
+ name={name()}
2042
+ status={status()}
2043
+ counters={counters()}
2044
+ currentPhase={currentPhase()}
2045
+ transcript={transcript()}
2046
+ running={isRunning()}
2047
+ part={props.part}
2048
+ onOpen={
2049
+ runID() && fullRoute.data.type === "session"
2050
+ ? () => {
2051
+ const d = fullRoute.data
2052
+ if (d.type === "session") fullRoute.navigate({ ...d, workflowRunID: runID() })
2053
+ }
2054
+ : undefined
2055
+ }
2056
+ />
2057
+ </Show>
2058
+ )
2059
+ }
2060
+
2061
+ // Bold panel for a `workflow run`. The transcript (phase + log lines streamed
2062
+ // every 250ms into part metadata) is the run's live activity — agents spawning,
2063
+ // per-source hits, facts checked. The old renderer dumped it all as one muted-
2064
+ // gray InlineTool blob, so a busy run read as "stuck". Here phases are bold
2065
+ // accent section headers, logs render in readable text, and a running run shows
2066
+ // a spinner on its current phase so progress is always visible. Bounded to the
2067
+ // last N lines in the conversation flow; full history lives in the detail dialog.
2068
+ const WORKFLOW_PANEL_TAIL = 12
2069
+
2070
+ // WorkflowPage is conditionally rendered (fallback of the conversation Show), so it
2071
+ // fully unmounts when you navigate into a subagent and remounts on return — which
2072
+ // would reset its scrollbox to the top. Remember the last scroll offset per runID
2073
+ // here so returning restores the position, like the persistent conversation scroll.
2074
+ const workflowScrollByRun = new Map<string, number>()
2075
+ function WorkflowPanel(props: {
2076
+ name: string
2077
+ status?: string
2078
+ counters?: { succeeded: number; failed: number; running: number }
2079
+ currentPhase?: string
2080
+ transcript: { kind: "phase" | "log"; text: string }[]
2081
+ running: boolean
2082
+ part: ToolPart
2083
+ onOpen?: () => void
2084
+ }) {
2085
+ const { theme } = useTheme()
2086
+ const renderer = useRenderer()
2087
+ const [collapsed, setCollapsed] = createSignal(false)
2088
+ const [openHover, setOpenHover] = createSignal(false)
2089
+
2090
+ const statusColor = createMemo(() => {
2091
+ const s = props.status
2092
+ if (s === "completed") return theme.success
2093
+ if (s === "failed") return theme.error
2094
+ if (s === "cancelled") return theme.textMuted
2095
+ return theme.warning
2096
+ })
2097
+
2098
+ const hiddenCount = createMemo(() => Math.max(0, props.transcript.length - WORKFLOW_PANEL_TAIL))
2099
+ const entries = createMemo(() => (collapsed() ? [] : props.transcript.slice(-WORKFLOW_PANEL_TAIL)))
2100
+
2101
+ return (
2102
+ <box
2103
+ border={["left"]}
2104
+ paddingTop={1}
2105
+ paddingBottom={1}
2106
+ paddingLeft={2}
2107
+ marginTop={1}
2108
+ gap={1}
2109
+ backgroundColor={theme.backgroundPanel}
2110
+ customBorderChars={SplitBorder.customBorderChars}
2111
+ borderColor={theme.background}
2112
+ onMouseUp={() => {
2113
+ if (renderer.getSelection()?.getSelectedText()) return
2114
+ setCollapsed((p) => !p)
2115
+ }}
2116
+ >
2117
+ <box flexDirection="row" gap={1} paddingLeft={3}>
2118
+ <Show when={props.running} fallback={<text fg={theme.accent} attributes={TextAttributes.BOLD}>⚡</text>}>
2119
+ <spinner frames={["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]} interval={80} color={theme.accent} />
2120
+ </Show>
2121
+ <text attributes={TextAttributes.BOLD} fg={theme.accent}>
2122
+ {props.name}
2123
+ </text>
2124
+ <Show when={props.status}>
2125
+ <text fg={statusColor()} attributes={TextAttributes.BOLD}>
2126
+ {props.status}
2127
+ </text>
2128
+ </Show>
2129
+ <Show when={props.currentPhase}>
2130
+ <text fg={theme.textMuted}>· {props.currentPhase}</text>
2131
+ </Show>
2132
+ <Show when={props.counters}>
2133
+ <text fg={theme.success}>{props.counters!.succeeded}✓</text>
2134
+ <text fg={props.counters!.failed > 0 ? theme.error : theme.textMuted}>{props.counters!.failed}✗</text>
2135
+ <text fg={props.counters!.running > 0 ? theme.warning : theme.textMuted}>{props.counters!.running}⟳</text>
2136
+ </Show>
2137
+ <Show when={props.onOpen}>
2138
+ <box flexGrow={1} />
2139
+ <text
2140
+ fg={openHover() ? theme.text : theme.markdownLink}
2141
+ onMouseOver={() => setOpenHover(true)}
2142
+ onMouseOut={() => setOpenHover(false)}
2143
+ onMouseUp={(evt) => {
2144
+ evt.stopPropagation()
2145
+ if (renderer.getSelection()?.getSelectedText()) return
2146
+ props.onOpen?.()
2147
+ }}
2148
+ >
2149
+ open ↗
2150
+ </text>
2151
+ </Show>
2152
+ </box>
2153
+ <Show
2154
+ when={!collapsed()}
2155
+ fallback={
2156
+ <text paddingLeft={3} fg={theme.textMuted}>
2157
+ {props.transcript.length} lines · click to expand
2158
+ </text>
2159
+ }
2160
+ >
2161
+ <box paddingLeft={3}>
2162
+ <Show when={hiddenCount() > 0}>
2163
+ <text fg={theme.textMuted}>+{hiddenCount()} earlier lines · open detail for full history</text>
2164
+ </Show>
2165
+ <For each={entries()}>
2166
+ {(e) => (
2167
+ <Show
2168
+ when={e.kind === "phase"}
2169
+ fallback={<text fg={theme.text}>{e.text}</text>}
2170
+ >
2171
+ <text fg={theme.accent} attributes={TextAttributes.BOLD}>
2172
+ ▸ {e.text}
2173
+ </text>
2174
+ </Show>
2175
+ )}
2176
+ </For>
2177
+ </box>
2178
+ </Show>
2179
+ </box>
2180
+ )
2181
+ }
2182
+
2183
+ // Full-screen workflow detail page: occupies the conversation column (like a
2184
+ // subagent view) and shows one run's structure tree + transcript, live while
2185
+ // running. An agent row navigates to that subagent's full conversation; a nested
2186
+ // workflow row drills into the child run; "Main" returns to the conversation.
2187
+ function WorkflowPage(props: {
2188
+ runID: string
2189
+ onBack: () => void
2190
+ onOpenAgent: (actorID: string) => void
2191
+ onOpenChild: (childRunID: string) => void
2192
+ }) {
2193
+ const sync = useSync()
2194
+ const dialog = useDialog()
2195
+ const keybind = useKeybind()
2196
+ const { theme } = useTheme()
2197
+ const renderer = useRenderer()
2198
+ const tuiConfig = useTuiConfig()
2199
+ const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
2200
+ let pageScroll: ScrollBoxRenderable | undefined
2201
+
2202
+ const run = createMemo(() => sync.data.workflow[props.runID])
2203
+
2204
+ // Describe what a running subagent is currently doing, from its live message
2205
+ // stream (last message's last meaningful part): a tool call → "⚙ <tool>", else
2206
+ // the latest text snippet. Returns undefined when nothing's streamed yet.
2207
+ const liveActivity = (actorID: string): string | undefined => {
2208
+ const sid = run()?.sessionID
2209
+ if (!sid) return undefined
2210
+ const msgs = sync.data.message[sid]?.[actorID]
2211
+ const last = msgs?.[msgs.length - 1]
2212
+ if (!last) return undefined
2213
+ const parts = sync.data.part[last.id] ?? []
2214
+ for (let i = parts.length - 1; i >= 0; i--) {
2215
+ const p = parts[i] as { type?: string; tool?: string; text?: string }
2216
+ if (p.type === "tool" && p.tool) return `⚙ ${p.tool}`
2217
+ if (p.type === "text" && p.text) return p.text
2218
+ }
2219
+ return undefined
2220
+ }
2221
+ const transcript = createMemo(() => sync.data.workflowTranscript[props.runID] ?? [])
2222
+ const structure = createMemo(() => sync.data.workflowStructure[props.runID] ?? [])
2223
+
2224
+ // Keyed on props.runID so a parent→child sub-workflow navigation (which does NOT
2225
+ // remount this component, since the <Show> fallback stays mounted while
2226
+ // workflowRunID is merely a different value) re-loads the new run's data, restores
2227
+ // its scroll, and re-arms the poll. The effect's onCleanup runs with `runID` bound
2228
+ // to the run being LEFT, so it saves that run's scroll under the correct key —
2229
+ // unlike reading props.runID at unmount, which is already stale.
2230
+ createEffect(
2231
+ on(
2232
+ () => props.runID,
2233
+ (runID) => {
2234
+ sync.loadWorkflowTranscript(runID)
2235
+ sync.loadWorkflowStructure(runID)
2236
+ // Restore the scroll position saved when we last left this run's page.
2237
+ // Deferred + retried so the cards (from the persisted structure store) are
2238
+ // laid out first, giving scrollTop a real range to land within.
2239
+ const saved = workflowScrollByRun.get(runID)
2240
+ if (saved) {
2241
+ let tries = 0
2242
+ const restore = () => {
2243
+ if (pageScroll) pageScroll.scrollTop = saved
2244
+ if (++tries < 5 && (pageScroll?.scrollTop ?? 0) < saved - 1) setTimeout(restore, 60)
2245
+ }
2246
+ setTimeout(restore, 0)
2247
+ } else if (pageScroll) {
2248
+ pageScroll.scrollTop = 0
2249
+ }
2250
+ const interval = setInterval(() => {
2251
+ sync.loadWorkflowStructure(runID)
2252
+ sync.loadWorkflowTranscript(runID)
2253
+ }, 1000)
2254
+ onCleanup(() => {
2255
+ clearInterval(interval)
2256
+ if (pageScroll) workflowScrollByRun.set(runID, pageScroll.scrollTop)
2257
+ })
2258
+ },
2259
+ ),
2260
+ )
2261
+
2262
+ const statusColor = createMemo(() => {
2263
+ const s = run()?.status
2264
+ if (s === "completed") return theme.success
2265
+ if (s === "failed") return theme.error
2266
+ if (s === "cancelled") return theme.textMuted
2267
+ return theme.warning
2268
+ })
2269
+
2270
+ const resumable = createMemo(() => {
2271
+ const s = run()?.status
2272
+ return s === "running" || s === "failed" || s === "cancelled"
2273
+ })
2274
+ const resume = async () => {
2275
+ const ok = await DialogConfirm.show(
2276
+ dialog,
2277
+ "Resume workflow",
2278
+ `Re-run "${run()?.name ?? props.runID}"? This re-executes the workflow and may incur cost.`,
2279
+ )
2280
+ if (ok === true) void sync.resumeWorkflow(props.runID)
2281
+ }
2282
+
2283
+ return (
2284
+ <box flexGrow={1} gap={1}>
2285
+ <box flexDirection="row" gap={1} flexShrink={0}>
2286
+ <text
2287
+ fg={theme.text}
2288
+ onMouseUp={() => {
2289
+ if (renderer.getSelection()?.getSelectedText()) return
2290
+ props.onBack()
2291
+ }}
2292
+ >
2293
+ ‹ Main <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span>
2294
+ </text>
2295
+ <box flexGrow={1} />
2296
+ <text attributes={TextAttributes.BOLD} fg={theme.accent}>
2297
+ {run()?.name ?? props.runID}
2298
+ </text>
2299
+ <Show when={run()?.status}>
2300
+ <text attributes={TextAttributes.BOLD} fg={statusColor()}>
2301
+ {run()!.status}
2302
+ </text>
2303
+ </Show>
2304
+ </box>
2305
+ <Show when={run()}>
2306
+ <box flexDirection="row" gap={1} flexShrink={0}>
2307
+ <Show when={run()!.currentPhase}>
2308
+ <text fg={theme.textMuted}>{run()!.currentPhase}</text>
2309
+ </Show>
2310
+ <text fg={theme.success}>{run()!.succeeded}✓</text>
2311
+ <text fg={run()!.failed > 0 ? theme.error : theme.textMuted}>{run()!.failed}✗</text>
2312
+ <text fg={run()!.running > 0 ? theme.warning : theme.textMuted}>{run()!.running}⟳</text>
2313
+ <Show when={resumable()}>
2314
+ <text fg={theme.markdownLink} onMouseUp={() => void resume()}>
2315
+ ↻ resume
2316
+ </text>
2317
+ </Show>
2318
+ </box>
2319
+ </Show>
2320
+ <scrollbox
2321
+ ref={(r) => (pageScroll = r)}
2322
+ flexGrow={1}
2323
+ scrollAcceleration={scrollAcceleration()}
2324
+ >
2325
+ <WorkflowTree
2326
+ nodes={structure()}
2327
+ onOpenChild={props.onOpenChild}
2328
+ onOpenAgent={props.onOpenAgent}
2329
+ liveActivity={liveActivity}
2330
+ />
2331
+ <Show when={transcript().length > 0}>
2332
+ <box paddingTop={1}>
2333
+ <text fg={theme.textMuted}>transcript</text>
2334
+ <For each={transcript()}>
2335
+ {(e) => (
2336
+ <Show when={e.kind === "phase"} fallback={<text fg={theme.text}>{e.text}</text>}>
2337
+ <text attributes={TextAttributes.BOLD} fg={theme.accent}>
2338
+ ▸ {e.text}
2339
+ </text>
2340
+ </Show>
2341
+ )}
2342
+ </For>
2343
+ </box>
2344
+ </Show>
2345
+ <Show when={run()?.error}>
2346
+ <text fg={theme.error}>{run()!.error}</text>
2347
+ </Show>
2348
+ </scrollbox>
2349
+ </box>
2350
+ )
2351
+ }
2352
+
2353
+ function CollapsibleError(props: { error: string; paddingLeft?: number }) {
2354
+ const { theme } = useTheme()
2355
+ const renderer = useRenderer()
2356
+ const [expanded, setExpanded] = createSignal(false)
2357
+
2358
+
2359
+ const lineCount = createMemo(() => props.error.split("\n").length)
2360
+
2361
+ return (
2362
+ <box
2363
+ paddingLeft={props.paddingLeft}
2364
+ onMouseUp={(evt) => {
2365
+ evt.stopPropagation()
2366
+ if (renderer.getSelection()?.getSelectedText()) return
2367
+ setExpanded((prev) => !prev)
2368
+ }}
2369
+ >
2370
+ <Show
2371
+ when={expanded()}
2372
+ fallback={
2373
+ <text fg={theme.error}>
2374
+ + Error ({lineCount()} {lineCount() === 1 ? "line" : "lines"})
2375
+ </text>
2376
+ }
2377
+ >
2378
+ <text fg={theme.error}>- Error</text>
2379
+ <box paddingLeft={2}>
2380
+ <text fg={theme.error}>{props.error}</text>
2381
+ </box>
2382
+ </Show>
2383
+ </box>
2384
+ )
2385
+ }
2386
+
2387
+ function InlineTool(props: {
2388
+ icon: string
2389
+ iconColor?: RGBA
2390
+ complete: any
2391
+ pending: string
2392
+ spinner?: boolean
2393
+ dismissed?: boolean
2394
+ children: JSX.Element
2395
+ part: ToolPart
2396
+ onClick?: () => void
2397
+ }) {
2398
+ const [margin, setMargin] = createSignal(0)
2399
+ const { theme } = useTheme()
2400
+ const ctx = use()
2401
+ const sync = useSync()
2402
+ const renderer = useRenderer()
2403
+ const [hover, setHover] = createSignal(false)
2404
+
2405
+ const permission = createMemo(() => {
2406
+ const callID = sync.data.permission[ctx.sessionID]?.at(0)?.tool?.callID
2407
+ if (!callID) return false
2408
+ return callID === props.part.callID
2409
+ })
2410
+
2411
+ const fg = createMemo(() => {
2412
+ if (permission()) return theme.warning
2413
+ if (hover() && props.onClick) return theme.text
2414
+ if (props.complete) return theme.textMuted
2415
+ return theme.text
2416
+ })
2417
+
2418
+ const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error : undefined))
2419
+
2420
+ const denied = createMemo(
2421
+ () =>
2422
+ error()?.includes("QuestionRejectedError") ||
2423
+ error()?.includes("rejected permission") ||
2424
+ error()?.includes("specified a rule") ||
2425
+ error()?.includes("user dismissed"),
2426
+ )
2427
+
2428
+ // Agent-recoverable failures (bad args, malformed call, unknown task/actor id)
2429
+ // are flagged on the error state. Render them muted (struck through, no red
2430
+ // block) like denials — the agent self-corrects; the user needn't be alarmed.
2431
+ const recoverable = createMemo(() => {
2432
+ const state = props.part.state
2433
+ return state.status === "error" && state.metadata?.recoverable === true
2434
+ })
2435
+
2436
+ return (
2437
+ <box
2438
+ marginTop={margin()}
2439
+ paddingLeft={3}
2440
+ onMouseOver={() => props.onClick && setHover(true)}
2441
+ onMouseOut={() => setHover(false)}
2442
+ onMouseUp={() => {
2443
+ if (renderer.getSelection()?.getSelectedText()) return
2444
+ props.onClick?.()
2445
+ }}
2446
+ renderBefore={function () {
2447
+ const el = this as BoxRenderable
2448
+ const parent = el.parent
2449
+ if (!parent) {
2450
+ return
2451
+ }
2452
+ if (el.height > 1) {
2453
+ setMargin(1)
2454
+ return
2455
+ }
2456
+ const children = parent.getChildren()
2457
+ const index = children.indexOf(el)
2458
+ const previous = children[index - 1]
2459
+ if (!previous) {
2460
+ setMargin(0)
2461
+ return
2462
+ }
2463
+ if (previous.height > 1 || previous.id.startsWith("text-")) {
2464
+ setMargin(1)
2465
+ return
2466
+ }
2467
+ }}
2468
+ >
2469
+ <Switch>
2470
+ <Match when={props.spinner}>
2471
+ <Spinner color={fg()} children={props.children} />
2472
+ </Match>
2473
+ <Match when={true}>
2474
+ <text paddingLeft={3} fg={fg()} attributes={denied() || recoverable() || props.dismissed ? TextAttributes.STRIKETHROUGH : undefined}>
2475
+ <Show fallback={<>~ {props.pending}</>} when={props.complete}>
2476
+ <span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
2477
+ </Show>
2478
+ </text>
2479
+ </Match>
2480
+ </Switch>
2481
+ <Show when={error() && !denied() && !recoverable()}>
2482
+ <CollapsibleError error={error()!} paddingLeft={3} />
2483
+ </Show>
2484
+ </box>
2485
+ )
2486
+ }
2487
+
2488
+ function BlockTool(props: {
2489
+ title: string
2490
+ children: JSX.Element
2491
+ onClick?: () => void
2492
+ part?: ToolPart
2493
+ spinner?: boolean
2494
+ }) {
2495
+ const { theme } = useTheme()
2496
+ const renderer = useRenderer()
2497
+ const [hover, setHover] = createSignal(false)
2498
+ const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
2499
+ return (
2500
+ <box
2501
+ border={["left"]}
2502
+ paddingTop={1}
2503
+ paddingBottom={1}
2504
+ paddingLeft={2}
2505
+ marginTop={1}
2506
+ gap={1}
2507
+ backgroundColor={hover() ? theme.backgroundMenu : theme.backgroundPanel}
2508
+ customBorderChars={SplitBorder.customBorderChars}
2509
+ borderColor={theme.background}
2510
+ onMouseOver={() => props.onClick && setHover(true)}
2511
+ onMouseOut={() => setHover(false)}
2512
+ onMouseUp={() => {
2513
+ if (renderer.getSelection()?.getSelectedText()) return
2514
+ props.onClick?.()
2515
+ }}
2516
+ >
2517
+ <Show
2518
+ when={props.spinner}
2519
+ fallback={
2520
+ <text paddingLeft={3} fg={theme.textMuted}>
2521
+ {props.title}
2522
+ </text>
2523
+ }
2524
+ >
2525
+ <Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
2526
+ </Show>
2527
+ {props.children}
2528
+ <Show when={error()}>
2529
+ <CollapsibleError error={error()!} />
2530
+ </Show>
2531
+ </box>
2532
+ )
2533
+ }
2534
+
2535
+ const TOOL_COLLAPSE_MAX_LINES = 3
2536
+ const TOOL_COLLAPSE_MAX_LINE_LENGTH = 120
2537
+
2538
+ function displayLines(content: string) {
2539
+ if (!content) return []
2540
+ return content.replace(/\n$/, "").split("\n")
2541
+ }
2542
+
2543
+ function hasLongDisplayLine(content: string) {
2544
+ return displayLines(content).some((line) => line.length > TOOL_COLLAPSE_MAX_LINE_LENGTH)
2545
+ }
2546
+
2547
+ function Bash(props: ToolProps<typeof BashTool>) {
2548
+ const { theme } = useTheme()
2549
+ const sync = useSync()
2550
+ const isRunning = createMemo(() => props.part.state.status === "running")
2551
+ const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
2552
+ const [expanded, setExpanded] = createSignal(false)
2553
+ const lines = createMemo(() => output().split("\n"))
2554
+ const overflow = createMemo(() => lines().length > 10)
2555
+ const limited = createMemo(() => {
2556
+ if (expanded() || !overflow()) return output()
2557
+ return [...lines().slice(0, 10), "…"].join("\n")
2558
+ })
2559
+
2560
+ const workdirDisplay = createMemo(() => {
2561
+ const workdir = props.input.workdir
2562
+ if (!workdir || workdir === ".") return undefined
2563
+
2564
+ const base = sync.path.directory
2565
+ if (!base) return undefined
2566
+
2567
+ const absolute = path.resolve(base, workdir)
2568
+ if (absolute === base) return undefined
2569
+
2570
+ const home = Global.Path.home
2571
+ if (!home) return absolute
2572
+
2573
+ const match = absolute === home || absolute.startsWith(home + path.sep)
2574
+ return match ? absolute.replace(home, "~") : absolute
2575
+ })
2576
+
2577
+ const title = createMemo(() => {
2578
+ const desc = props.input.description ?? "Shell"
2579
+ const wd = workdirDisplay()
2580
+ if (!wd) return `# ${desc}`
2581
+ if (desc.includes(wd)) return `# ${desc}`
2582
+ return `# ${desc} in ${wd}`
2583
+ })
2584
+
2585
+ return (
2586
+ <Switch>
2587
+ <Match when={props.metadata.output !== undefined}>
2588
+ <BlockTool
2589
+ title={title()}
2590
+ part={props.part}
2591
+ spinner={isRunning()}
2592
+ onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
2593
+ >
2594
+ <box gap={1}>
2595
+ <text fg={theme.text}>$ {props.input.command}</text>
2596
+ <Show when={output()}>
2597
+ <text fg={theme.text}>{limited()}</text>
2598
+ </Show>
2599
+ <Show when={overflow()}>
2600
+ <text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
2601
+ </Show>
2602
+ </box>
2603
+ </BlockTool>
2604
+ </Match>
2605
+ <Match when={true}>
2606
+ <InlineTool icon="$" pending="Writing command..." complete={props.input.command} part={props.part}>
2607
+ {props.input.command}
2608
+ </InlineTool>
2609
+ </Match>
2610
+ </Switch>
2611
+ )
2612
+ }
2613
+
2614
+ function Write(props: ToolProps<typeof WriteTool>) {
2615
+ const { theme, syntax } = useTheme()
2616
+ const [expanded, setExpanded] = createSignal(false)
2617
+ const code = createMemo(() => {
2618
+ if (!props.input.content) return ""
2619
+ return props.input.content
2620
+ })
2621
+ const lineCount = createMemo(() => displayLines(code()).length)
2622
+ const collapsed = createMemo(() => lineCount() > TOOL_COLLAPSE_MAX_LINES || hasLongDisplayLine(code()))
2623
+
2624
+ return (
2625
+ <Switch>
2626
+ <Match when={props.metadata.diagnostics !== undefined}>
2627
+ <BlockTool
2628
+ title={"# Wrote " + normalizePath(props.input.file_path!)}
2629
+ part={props.part}
2630
+ onClick={collapsed() ? () => setExpanded((prev) => !prev) : undefined}
2631
+ >
2632
+ <Show
2633
+ when={!collapsed() || expanded()}
2634
+ fallback={
2635
+ <text fg={theme.textMuted}>
2636
+ Click to expand ({lineCount()} {lineCount() === 1 ? "line" : "lines"})
2637
+ </text>
2638
+ }
2639
+ >
2640
+ <line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
2641
+ <code
2642
+ conceal={false}
2643
+ fg={theme.text}
2644
+ filetype={filetype(props.input.file_path!)}
2645
+ syntaxStyle={syntax()}
2646
+ content={code()}
2647
+ />
2648
+ </line_number>
2649
+ <Show when={collapsed()}>
2650
+ <text fg={theme.textMuted}>Click to collapse</text>
2651
+ </Show>
2652
+ </Show>
2653
+ <Diagnostics diagnostics={props.metadata.diagnostics} filePath={props.input.file_path ?? ""} />
2654
+ </BlockTool>
2655
+ </Match>
2656
+ <Match when={true}>
2657
+ <InlineTool icon="←" pending="Preparing write..." complete={props.input.file_path} part={props.part}>
2658
+ Write {normalizePath(props.input.file_path!)}
2659
+ </InlineTool>
2660
+ </Match>
2661
+ </Switch>
2662
+ )
2663
+ }
2664
+
2665
+ function Glob(props: ToolProps<typeof GlobTool>) {
2666
+ return (
2667
+ <InlineTool icon="✱" pending="Finding files..." complete={props.input.pattern} part={props.part}>
2668
+ Glob "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
2669
+ <Show when={props.metadata.count}>
2670
+ ({props.metadata.count} {props.metadata.count === 1 ? "match" : "matches"})
2671
+ </Show>
2672
+ </InlineTool>
2673
+ )
2674
+ }
2675
+
2676
+ function Read(props: ToolProps<typeof ReadTool>) {
2677
+ const { theme } = useTheme()
2678
+ const isRunning = createMemo(() => props.part.state.status === "running")
2679
+ const loaded = createMemo(() => {
2680
+ if (props.part.state.status !== "completed") return []
2681
+ if (props.part.state.time.compacted) return []
2682
+ const value = props.metadata.loaded
2683
+ if (!value || !Array.isArray(value)) return []
2684
+ return value.filter((p): p is string => typeof p === "string")
2685
+ })
2686
+ return (
2687
+ <>
2688
+ <InlineTool
2689
+ icon="→"
2690
+ pending="Reading file..."
2691
+ complete={props.input.file_path}
2692
+ spinner={isRunning()}
2693
+ part={props.part}
2694
+ >
2695
+ Read {normalizePath(props.input.file_path!)} {input(props.input, ["file_path"])}
2696
+ </InlineTool>
2697
+ <For each={loaded()}>
2698
+ {(filepath) => (
2699
+ <box paddingLeft={3}>
2700
+ <text paddingLeft={3} fg={theme.textMuted}>
2701
+ ↳ Loaded {normalizePath(filepath)}
2702
+ </text>
2703
+ </box>
2704
+ )}
2705
+ </For>
2706
+ </>
2707
+ )
2708
+ }
2709
+
2710
+ function Grep(props: ToolProps<typeof GrepTool>) {
2711
+ return (
2712
+ <InlineTool icon="✱" pending="Searching content..." complete={props.input.pattern} part={props.part}>
2713
+ Grep "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
2714
+ <Show when={props.metadata.matches}>
2715
+ ({props.metadata.matches} {props.metadata.matches === 1 ? "match" : "matches"})
2716
+ </Show>
2717
+ </InlineTool>
2718
+ )
2719
+ }
2720
+
2721
+ function WebFetch(props: ToolProps<typeof WebFetchTool>) {
2722
+ return (
2723
+ <InlineTool icon="%" pending="Fetching from the web..." complete={props.input.url} part={props.part}>
2724
+ WebFetch {props.input.url}
2725
+ </InlineTool>
2726
+ )
2727
+ }
2728
+
2729
+ function CodeSearch(props: ToolProps<typeof CodeSearchTool>) {
2730
+ const metadata = props.metadata as { results?: number }
2731
+ return (
2732
+ <InlineTool icon="◇" pending="Searching code..." complete={props.input.query} part={props.part}>
2733
+ Exa Code Search "{props.input.query}" <Show when={metadata.results}>({metadata.results} results)</Show>
2734
+ </InlineTool>
2735
+ )
2736
+ }
2737
+
2738
+ function WebSearch(props: ToolProps<typeof WebSearchTool>) {
2739
+ const metadata = props.metadata as { numResults?: number }
2740
+ return (
2741
+ <InlineTool icon="◈" pending="Searching web..." complete={props.input.query} part={props.part}>
2742
+ Web Search "{props.input.query}" <Show when={metadata.numResults}>({metadata.numResults} results)</Show>
2743
+ </InlineTool>
2744
+ )
2745
+ }
2746
+
2747
+ function Task(props: ToolProps<typeof ActorTool>) {
2748
+ const route = useRoute()
2749
+ const sync = useSync()
2750
+
2751
+ const input = createMemo(() => {
2752
+ const raw = props.input as Partial<{ operation: { description: string; subagent_type: string } } & {
2753
+ description: string
2754
+ subagent_type: string
2755
+ }>
2756
+ return (raw?.operation ?? raw) as Partial<{ description: string; subagent_type: string }>
2757
+ })
2758
+
2759
+ const targetSession = createMemo(() => props.metadata.sessionId as string | undefined)
2760
+ const targetBucket = createMemo(() => (props.metadata.actorId as string | undefined) ?? "main")
2761
+
2762
+ createEffect(() => {
2763
+ const session = targetSession()
2764
+ const bucket = targetBucket()
2765
+ if (session && !sync.data.message[session]?.[bucket]?.length)
2766
+ void sync.session.sync(session)
2767
+ })
2768
+
2769
+ const messages = createMemo(() => sync.data.message[targetSession() ?? ""]?.[targetBucket()] ?? [])
2770
+
2771
+ const tools = createMemo(() => {
2772
+ return messages().flatMap((msg) =>
2773
+ (sync.data.part[msg.id] ?? [])
2774
+ .filter((part): part is ToolPart => part.type === "tool")
2775
+ .map((part) => ({ tool: part.tool, state: part.state })),
2776
+ )
2777
+ })
2778
+
2779
+ const current = createMemo(() =>
2780
+ tools().findLast((x) => (x.state.status === "running" || x.state.status === "completed") && x.state.title),
2781
+ )
2782
+
2783
+ const isRunning = createMemo(() => props.part.state.status === "running")
2784
+
2785
+ const duration = createMemo(() => {
2786
+ const first = messages().find((x) => x.role === "user")?.time.created
2787
+ const assistant = messages().findLast((x) => x.role === "assistant")?.time.completed
2788
+ if (!first || !assistant) return 0
2789
+ return assistant - first
2790
+ })
2791
+
2792
+ const content = createMemo(() => {
2793
+ if (!input().description) return ""
2794
+ let content = [`${Locale.titlecase(input().subagent_type ?? "General")} Task — ${input().description}`]
2795
+
2796
+ if (isRunning() && tools().length > 0) {
2797
+ // content[0] += ` · ${tools().length} toolcalls`
2798
+ if (current()) {
2799
+ const state = current()!.state
2800
+ const title = state.status === "running" || state.status === "completed" ? state.title : undefined
2801
+ content.push(`↳ ${Locale.titlecase(current()!.tool)} ${title}`)
2802
+ } else content.push(`↳ ${tools().length} toolcalls`)
2803
+ }
2804
+
2805
+ if (props.part.state.status === "completed") {
2806
+ content.push(`└ ${tools().length} toolcalls · ${Locale.duration(duration())}`)
2807
+ }
2808
+
2809
+ return content.join("\n")
2810
+ })
2811
+
2812
+ return (
2813
+ <InlineTool
2814
+ icon="│"
2815
+ spinner={isRunning()}
2816
+ complete={input().description}
2817
+ pending="Delegating..."
2818
+ part={props.part}
2819
+ onClick={() => {
2820
+ const session = targetSession()
2821
+ if (!session) return
2822
+ const actor = targetBucket()
2823
+ if (
2824
+ route.data.type === "session" &&
2825
+ session === route.data.sessionID &&
2826
+ actor !== "main"
2827
+ ) {
2828
+ route.navigate({ ...route.data, agentID: actor })
2829
+ return
2830
+ }
2831
+ route.navigate({ type: "session", sessionID: session, agentID: actor !== "main" ? actor : undefined })
2832
+ }}
2833
+ >
2834
+ {content()}
2835
+ </InlineTool>
2836
+ )
2837
+ }
2838
+
2839
+ function Edit(props: ToolProps<typeof EditTool>) {
2840
+ const ctx = use()
2841
+ const { theme, syntax } = useTheme()
2842
+
2843
+ const view = createMemo(() => {
2844
+ const diffStyle = ctx.tui.diff_style
2845
+ if (diffStyle === "stacked") return "unified"
2846
+ // Default to "auto" behavior
2847
+ return ctx.width > 120 ? "split" : "unified"
2848
+ })
2849
+
2850
+ const ft = createMemo(() => filetype(props.input.file_path))
2851
+
2852
+ const diffContent = createMemo(() => props.metadata.diff)
2853
+
2854
+ return (
2855
+ <Switch>
2856
+ <Match when={props.metadata.diff !== undefined}>
2857
+ <BlockTool title={"← Edit " + normalizePath(props.input.file_path!)} part={props.part}>
2858
+ <box paddingLeft={1}>
2859
+ <diff
2860
+ diff={diffContent()}
2861
+ view={view()}
2862
+ filetype={ft()}
2863
+ syntaxStyle={syntax()}
2864
+ showLineNumbers={true}
2865
+ width="100%"
2866
+ wrapMode={ctx.diffWrapMode()}
2867
+ fg={theme.text}
2868
+ addedBg={theme.diffAddedBg}
2869
+ removedBg={theme.diffRemovedBg}
2870
+ contextBg={theme.diffContextBg}
2871
+ addedSignColor={theme.diffHighlightAdded}
2872
+ removedSignColor={theme.diffHighlightRemoved}
2873
+ lineNumberFg={theme.diffLineNumber}
2874
+ lineNumberBg={theme.diffContextBg}
2875
+ addedLineNumberBg={theme.diffAddedLineNumberBg}
2876
+ removedLineNumberBg={theme.diffRemovedLineNumberBg}
2877
+ />
2878
+ </box>
2879
+ <Diagnostics diagnostics={props.metadata.diagnostics} filePath={props.input.file_path ?? ""} />
2880
+ </BlockTool>
2881
+ </Match>
2882
+ <Match when={true}>
2883
+ <InlineTool icon="←" pending="Preparing edit..." complete={props.input.file_path} part={props.part}>
2884
+ Edit {normalizePath(props.input.file_path!)} {input({ replace_all: props.input.replace_all })}
2885
+ </InlineTool>
2886
+ </Match>
2887
+ </Switch>
2888
+ )
2889
+ }
2890
+
2891
+ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
2892
+ const ctx = use()
2893
+ const { theme, syntax } = useTheme()
2894
+ const [expanded, setExpanded] = createSignal<string[]>([])
2895
+
2896
+ const files = createMemo(() => props.metadata.files ?? [])
2897
+
2898
+ const view = createMemo(() => {
2899
+ const diffStyle = ctx.tui.diff_style
2900
+ if (diffStyle === "stacked") return "unified"
2901
+ return ctx.width > 120 ? "split" : "unified"
2902
+ })
2903
+
2904
+ function Diff(p: { diff: string; filePath: string }) {
2905
+ return (
2906
+ <box paddingLeft={1}>
2907
+ <diff
2908
+ diff={p.diff}
2909
+ view={view()}
2910
+ filetype={filetype(p.filePath)}
2911
+ syntaxStyle={syntax()}
2912
+ showLineNumbers={true}
2913
+ width="100%"
2914
+ wrapMode={ctx.diffWrapMode()}
2915
+ fg={theme.text}
2916
+ addedBg={theme.diffAddedBg}
2917
+ removedBg={theme.diffRemovedBg}
2918
+ contextBg={theme.diffContextBg}
2919
+ addedSignColor={theme.diffHighlightAdded}
2920
+ removedSignColor={theme.diffHighlightRemoved}
2921
+ lineNumberFg={theme.diffLineNumber}
2922
+ lineNumberBg={theme.diffContextBg}
2923
+ addedLineNumberBg={theme.diffAddedLineNumberBg}
2924
+ removedLineNumberBg={theme.diffRemovedLineNumberBg}
2925
+ />
2926
+ </box>
2927
+ )
2928
+ }
2929
+
2930
+ function title(file: { type: string; relativePath: string; filePath: string; deletions: number }) {
2931
+ if (file.type === "delete") return "# Deleted " + file.relativePath
2932
+ if (file.type === "add") return "# Created " + file.relativePath
2933
+ if (file.type === "move") return "# Moved " + normalizePath(file.filePath) + " → " + file.relativePath
2934
+ return "← Patched " + file.relativePath
2935
+ }
2936
+
2937
+ function toggle(filePath: string) {
2938
+ setExpanded((prev) => (prev.includes(filePath) ? prev.filter((item) => item !== filePath) : [...prev, filePath]))
2939
+ }
2940
+
2941
+ return (
2942
+ <Switch>
2943
+ <Match when={files().length > 0}>
2944
+ <For each={files()}>
2945
+ {(file) => {
2946
+ const open = createMemo(() => expanded().includes(file.filePath))
2947
+ const count = createMemo(() => file.additions + file.deletions)
2948
+ const collapsed = createMemo(() => count() > TOOL_COLLAPSE_MAX_LINES || hasLongDisplayLine(file.patch))
2949
+
2950
+ return (
2951
+ <BlockTool
2952
+ title={title(file)}
2953
+ part={props.part}
2954
+ onClick={file.type !== "delete" && collapsed() ? () => toggle(file.filePath) : undefined}
2955
+ >
2956
+ <Show
2957
+ when={file.type !== "delete"}
2958
+ fallback={
2959
+ <text fg={theme.diffRemoved}>
2960
+ -{file.deletions} line{file.deletions !== 1 ? "s" : ""}
2961
+ </text>
2962
+ }
2963
+ >
2964
+ <Show
2965
+ when={!collapsed() || open()}
2966
+ fallback={
2967
+ <text fg={theme.textMuted}>
2968
+ Click to expand ({count()} change{count() !== 1 ? "s" : ""})
2969
+ </text>
2970
+ }
2971
+ >
2972
+ <Diff diff={file.patch} filePath={file.filePath} />
2973
+ <Show when={collapsed()}>
2974
+ <text fg={theme.textMuted}>Click to collapse</text>
2975
+ </Show>
2976
+ </Show>
2977
+ <Diagnostics diagnostics={props.metadata.diagnostics} filePath={file.movePath ?? file.filePath} />
2978
+ </Show>
2979
+ </BlockTool>
2980
+ )
2981
+ }}
2982
+ </For>
2983
+ </Match>
2984
+ <Match when={true}>
2985
+ <InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}>
2986
+ Patch
2987
+ </InlineTool>
2988
+ </Match>
2989
+ </Switch>
2990
+ )
2991
+ }
2992
+
2993
+ function Question(props: ToolProps<typeof QuestionTool>) {
2994
+ const { theme } = useTheme()
2995
+ const count = createMemo(() => props.input.questions?.length ?? 0)
2996
+
2997
+ function format(answer?: ReadonlyArray<string>) {
2998
+ if (!answer?.length) return "(no answer)"
2999
+ return answer.join(", ")
3000
+ }
3001
+
3002
+ return (
3003
+ <Switch>
3004
+ <Match when={props.metadata.answers}>
3005
+ <BlockTool title="# Questions" part={props.part}>
3006
+ <box gap={1}>
3007
+ <For each={props.input.questions ?? []}>
3008
+ {(q, i) => (
3009
+ <box flexDirection="column">
3010
+ <text fg={theme.textMuted}>{q.question}</text>
3011
+ <text fg={theme.text}>{format(props.metadata.answers?.[i()])}</text>
3012
+ </box>
3013
+ )}
3014
+ </For>
3015
+ </box>
3016
+ </BlockTool>
3017
+ </Match>
3018
+ <Match when={true}>
3019
+ <InlineTool icon="→" pending="Asking questions..." complete={count()} part={props.part}>
3020
+ Asked {count()} question{count() !== 1 ? "s" : ""}
3021
+ </InlineTool>
3022
+ </Match>
3023
+ </Switch>
3024
+ )
3025
+ }
3026
+
3027
+ function Skill(props: ToolProps<typeof SkillTool>) {
3028
+ return (
3029
+ <InlineTool icon="→" pending="Loading skill..." complete={props.input.name} part={props.part}>
3030
+ Skill "{props.input.name}"
3031
+ </InlineTool>
3032
+ )
3033
+ }
3034
+
3035
+ function Diagnostics(props: { diagnostics?: Record<string, Record<string, any>[]>; filePath: string }) {
3036
+ const { theme } = useTheme()
3037
+ const errors = createMemo(() => {
3038
+ const normalized = Filesystem.normalizePath(props.filePath)
3039
+ const arr = props.diagnostics?.[normalized] ?? []
3040
+ return arr.filter((x) => x.severity === 1).slice(0, 3)
3041
+ })
3042
+
3043
+ return (
3044
+ <Show when={errors().length}>
3045
+ <box>
3046
+ <For each={errors()}>
3047
+ {(diagnostic) => (
3048
+ <text fg={theme.error}>
3049
+ Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message}
3050
+ </text>
3051
+ )}
3052
+ </For>
3053
+ </box>
3054
+ </Show>
3055
+ )
3056
+ }
3057
+
3058
+ function normalizePath(input?: string) {
3059
+ if (!input) return ""
3060
+
3061
+ const cwd = process.cwd()
3062
+ const absolute = path.isAbsolute(input) ? input : path.resolve(cwd, input)
3063
+ const relative = path.relative(cwd, absolute)
3064
+
3065
+ if (!relative) return "."
3066
+ if (!relative.startsWith("..")) return relative
3067
+
3068
+ // outside cwd - use absolute
3069
+ return absolute
3070
+ }
3071
+
3072
+ function input(input: Record<string, any>, omit?: string[]): string {
3073
+ const primitives = Object.entries(input).filter(([key, value]) => {
3074
+ if (omit?.includes(key)) return false
3075
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
3076
+ })
3077
+ if (primitives.length === 0) return ""
3078
+ return `[${primitives.map(([key, value]) => `${key}=${value}`).join(", ")}]`
3079
+ }
3080
+
3081
+ function filetype(input?: string) {
3082
+ if (!input) return "none"
3083
+ const ext = path.extname(input)
3084
+ const language = LANGUAGE_EXTENSIONS[ext]
3085
+ if (["typescriptreact", "javascriptreact", "javascript"].includes(language)) return "typescript"
3086
+ return language
3087
+ }