@sleepy-ai/cli 0.1.15 → 0.1.16

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