orynacode-ai 1.16.2

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 (938) hide show
  1. package/AGENTS.md +131 -0
  2. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  3. package/Dockerfile +18 -0
  4. package/README.md +15 -0
  5. package/bin/orynacode +199 -0
  6. package/bunfig.toml +7 -0
  7. package/git +0 -0
  8. package/migration/20260511173437_session-metadata/migration.sql +1 -0
  9. package/migration/20260511173437_session-metadata/snapshot.json +1500 -0
  10. package/package.json +155 -0
  11. package/parsers-config.ts +386 -0
  12. package/script/bench-search.ts +115 -0
  13. package/script/bench-test-suite.ts +52 -0
  14. package/script/build.ts +244 -0
  15. package/script/generate.ts +14 -0
  16. package/script/httpapi-exercise.ts +1 -0
  17. package/script/postinstall.mjs +189 -0
  18. package/script/profile-test-files.ts +42 -0
  19. package/script/publish.ts +213 -0
  20. package/script/run-workspace-server +106 -0
  21. package/script/schema.ts +77 -0
  22. package/script/time.ts +6 -0
  23. package/script/trace-imports.ts +153 -0
  24. package/specs/effect/error-boundaries-plan.md +235 -0
  25. package/specs/effect/errors.md +207 -0
  26. package/specs/effect/facades.md +218 -0
  27. package/specs/effect/guide.md +247 -0
  28. package/specs/effect/instance-context.md +13 -0
  29. package/specs/effect/loose-ends.md +30 -0
  30. package/specs/effect/migration.md +62 -0
  31. package/specs/effect/routes.md +61 -0
  32. package/specs/effect/schema.md +88 -0
  33. package/specs/effect/server-package.md +58 -0
  34. package/specs/effect/todo.md +241 -0
  35. package/specs/effect/tools.md +88 -0
  36. package/specs/openapi-translation-cleanup.md +204 -0
  37. package/specs/tui-plugins.md +544 -0
  38. package/specs/v2/api.ts +67 -0
  39. package/specs/v2/message-shape.md +136 -0
  40. package/specs/v2/notifications.md +13 -0
  41. package/specs/v2/tui-command-shim.md +67 -0
  42. package/src/account/account.ts +459 -0
  43. package/src/account/repo.ts +170 -0
  44. package/src/account/schema.ts +99 -0
  45. package/src/account/url.ts +8 -0
  46. package/src/acp/agent.ts +95 -0
  47. package/src/acp/config-option.ts +203 -0
  48. package/src/acp/content.ts +250 -0
  49. package/src/acp/directory.ts +210 -0
  50. package/src/acp/error.ts +90 -0
  51. package/src/acp/event.ts +344 -0
  52. package/src/acp/permission.ts +145 -0
  53. package/src/acp/profile.ts +42 -0
  54. package/src/acp/service.ts +1062 -0
  55. package/src/acp/session.ts +231 -0
  56. package/src/acp/tool.ts +317 -0
  57. package/src/acp/usage.ts +239 -0
  58. package/src/agent/agent.ts +433 -0
  59. package/src/agent/generate.txt +75 -0
  60. package/src/agent/prompt/compaction.txt +9 -0
  61. package/src/agent/prompt/explore.txt +18 -0
  62. package/src/agent/prompt/summary.txt +11 -0
  63. package/src/agent/prompt/title.txt +44 -0
  64. package/src/agent/subagent-permissions.ts +35 -0
  65. package/src/audio.d.ts +14 -0
  66. package/src/auth/index.ts +96 -0
  67. package/src/background/job.ts +36 -0
  68. package/src/bus/global.ts +22 -0
  69. package/src/cli/bootstrap.ts +11 -0
  70. package/src/cli/cmd/account.ts +264 -0
  71. package/src/cli/cmd/acp.ts +76 -0
  72. package/src/cli/cmd/agent.ts +259 -0
  73. package/src/cli/cmd/cmd.ts +7 -0
  74. package/src/cli/cmd/db.ts +62 -0
  75. package/src/cli/cmd/debug/agent.handler.ts +193 -0
  76. package/src/cli/cmd/debug/agent.ts +27 -0
  77. package/src/cli/cmd/debug/config.ts +14 -0
  78. package/src/cli/cmd/debug/file.ts +87 -0
  79. package/src/cli/cmd/debug/index.ts +87 -0
  80. package/src/cli/cmd/debug/lsp.ts +51 -0
  81. package/src/cli/cmd/debug/ripgrep.ts +99 -0
  82. package/src/cli/cmd/debug/scrap.ts +18 -0
  83. package/src/cli/cmd/debug/skill.ts +15 -0
  84. package/src/cli/cmd/debug/snapshot.ts +50 -0
  85. package/src/cli/cmd/debug/startup.ts +11 -0
  86. package/src/cli/cmd/debug/v2.ts +46 -0
  87. package/src/cli/cmd/export.ts +292 -0
  88. package/src/cli/cmd/generate.ts +54 -0
  89. package/src/cli/cmd/github.handler.ts +1593 -0
  90. package/src/cli/cmd/github.shared.ts +30 -0
  91. package/src/cli/cmd/github.ts +42 -0
  92. package/src/cli/cmd/import.ts +224 -0
  93. package/src/cli/cmd/mcp.ts +846 -0
  94. package/src/cli/cmd/models.ts +66 -0
  95. package/src/cli/cmd/plug.ts +230 -0
  96. package/src/cli/cmd/pr.ts +115 -0
  97. package/src/cli/cmd/prompt-display.ts +48 -0
  98. package/src/cli/cmd/providers.ts +506 -0
  99. package/src/cli/cmd/run/demo.ts +1274 -0
  100. package/src/cli/cmd/run/entry.body.ts +194 -0
  101. package/src/cli/cmd/run/footer.command.tsx +899 -0
  102. package/src/cli/cmd/run/footer.menu.tsx +306 -0
  103. package/src/cli/cmd/run/footer.permission.tsx +475 -0
  104. package/src/cli/cmd/run/footer.prompt.tsx +1207 -0
  105. package/src/cli/cmd/run/footer.question.tsx +579 -0
  106. package/src/cli/cmd/run/footer.subagent.tsx +171 -0
  107. package/src/cli/cmd/run/footer.ts +1092 -0
  108. package/src/cli/cmd/run/footer.view.tsx +935 -0
  109. package/src/cli/cmd/run/otel.ts +117 -0
  110. package/src/cli/cmd/run/permission.shared.ts +256 -0
  111. package/src/cli/cmd/run/prompt.shared.ts +147 -0
  112. package/src/cli/cmd/run/question.shared.ts +340 -0
  113. package/src/cli/cmd/run/runtime.boot.ts +210 -0
  114. package/src/cli/cmd/run/runtime.lifecycle.ts +369 -0
  115. package/src/cli/cmd/run/runtime.queue.ts +347 -0
  116. package/src/cli/cmd/run/runtime.shared.ts +17 -0
  117. package/src/cli/cmd/run/runtime.stdin.ts +37 -0
  118. package/src/cli/cmd/run/runtime.ts +879 -0
  119. package/src/cli/cmd/run/scrollback.shared.ts +92 -0
  120. package/src/cli/cmd/run/scrollback.surface.ts +435 -0
  121. package/src/cli/cmd/run/scrollback.writer.tsx +335 -0
  122. package/src/cli/cmd/run/session-data.ts +1113 -0
  123. package/src/cli/cmd/run/session-replay.ts +301 -0
  124. package/src/cli/cmd/run/session.shared.ts +196 -0
  125. package/src/cli/cmd/run/splash.ts +310 -0
  126. package/src/cli/cmd/run/stream.transport.ts +1465 -0
  127. package/src/cli/cmd/run/stream.ts +175 -0
  128. package/src/cli/cmd/run/subagent-data.ts +844 -0
  129. package/src/cli/cmd/run/theme.ts +603 -0
  130. package/src/cli/cmd/run/tool.ts +1489 -0
  131. package/src/cli/cmd/run/trace.ts +94 -0
  132. package/src/cli/cmd/run/types.ts +342 -0
  133. package/src/cli/cmd/run/variant.shared.ts +215 -0
  134. package/src/cli/cmd/run.ts +879 -0
  135. package/src/cli/cmd/serve.ts +24 -0
  136. package/src/cli/cmd/session.ts +147 -0
  137. package/src/cli/cmd/stats.ts +393 -0
  138. package/src/cli/cmd/tui/app.tsx +1113 -0
  139. package/src/cli/cmd/tui/attach.ts +103 -0
  140. package/src/cli/cmd/tui/attention.ts +262 -0
  141. package/src/cli/cmd/tui/component/bg-pulse-render.ts +436 -0
  142. package/src/cli/cmd/tui/component/bg-pulse.tsx +99 -0
  143. package/src/cli/cmd/tui/component/border.tsx +21 -0
  144. package/src/cli/cmd/tui/component/command-palette.tsx +79 -0
  145. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  146. package/src/cli/cmd/tui/component/dialog-console-org.tsx +103 -0
  147. package/src/cli/cmd/tui/component/dialog-mcp.tsx +85 -0
  148. package/src/cli/cmd/tui/component/dialog-model.tsx +185 -0
  149. package/src/cli/cmd/tui/component/dialog-move-session.tsx +240 -0
  150. package/src/cli/cmd/tui/component/dialog-provider.tsx +687 -0
  151. package/src/cli/cmd/tui/component/dialog-retry-action.tsx +160 -0
  152. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +99 -0
  153. package/src/cli/cmd/tui/component/dialog-session-list.tsx +318 -0
  154. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  155. package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
  156. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  157. package/src/cli/cmd/tui/component/dialog-status.tsx +168 -0
  158. package/src/cli/cmd/tui/component/dialog-tag.tsx +47 -0
  159. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  160. package/src/cli/cmd/tui/component/dialog-variant.tsx +39 -0
  161. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +313 -0
  162. package/src/cli/cmd/tui/component/dialog-workspace-file-changes.tsx +144 -0
  163. package/src/cli/cmd/tui/component/dialog-workspace-list.tsx +112 -0
  164. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +69 -0
  165. package/src/cli/cmd/tui/component/error-component.tsx +81 -0
  166. package/src/cli/cmd/tui/component/logo.tsx +885 -0
  167. package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
  168. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +799 -0
  169. package/src/cli/cmd/tui/component/prompt/cwd.ts +0 -0
  170. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  171. package/src/cli/cmd/tui/component/prompt/history.tsx +117 -0
  172. package/src/cli/cmd/tui/component/prompt/index.tsx +1725 -0
  173. package/src/cli/cmd/tui/component/prompt/move.tsx +192 -0
  174. package/src/cli/cmd/tui/component/prompt/part.ts +31 -0
  175. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  176. package/src/cli/cmd/tui/component/prompt/traits.ts +35 -0
  177. package/src/cli/cmd/tui/component/prompt/workspace.tsx +137 -0
  178. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  179. package/src/cli/cmd/tui/component/startup-loading.tsx +63 -0
  180. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  181. package/src/cli/cmd/tui/component/use-connected.tsx +9 -0
  182. package/src/cli/cmd/tui/component/workspace-label.tsx +19 -0
  183. package/src/cli/cmd/tui/config/cwd.ts +5 -0
  184. package/src/cli/cmd/tui/config/keybind.ts +467 -0
  185. package/src/cli/cmd/tui/config/tui-migrate.ts +154 -0
  186. package/src/cli/cmd/tui/config/tui-schema.ts +88 -0
  187. package/src/cli/cmd/tui/config/tui.ts +308 -0
  188. package/src/cli/cmd/tui/context/agent.tsx +11 -0
  189. package/src/cli/cmd/tui/context/aggregate-failures.ts +51 -0
  190. package/src/cli/cmd/tui/context/args.tsx +15 -0
  191. package/src/cli/cmd/tui/context/directory.ts +15 -0
  192. package/src/cli/cmd/tui/context/editor-zed.ts +287 -0
  193. package/src/cli/cmd/tui/context/editor.ts +469 -0
  194. package/src/cli/cmd/tui/context/event.ts +38 -0
  195. package/src/cli/cmd/tui/context/exit.tsx +42 -0
  196. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  197. package/src/cli/cmd/tui/context/kv.tsx +76 -0
  198. package/src/cli/cmd/tui/context/local.tsx +510 -0
  199. package/src/cli/cmd/tui/context/path-format.tsx +39 -0
  200. package/src/cli/cmd/tui/context/project.tsx +111 -0
  201. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  202. package/src/cli/cmd/tui/context/route.tsx +52 -0
  203. package/src/cli/cmd/tui/context/sdk.tsx +142 -0
  204. package/src/cli/cmd/tui/context/sync-v2.tsx +447 -0
  205. package/src/cli/cmd/tui/context/sync.tsx +628 -0
  206. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  207. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  208. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  209. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +230 -0
  210. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +230 -0
  211. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  212. package/src/cli/cmd/tui/context/theme/cobalt2.json +225 -0
  213. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  214. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  215. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  216. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  217. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  218. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  219. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  220. package/src/cli/cmd/tui/context/theme/lucent-orng.json +234 -0
  221. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  222. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  223. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  224. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  225. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  226. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  227. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  228. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  229. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  230. package/src/cli/cmd/tui/context/theme/oryna.json +95 -0
  231. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  232. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  233. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  234. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  235. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  236. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  237. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  238. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  239. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  240. package/src/cli/cmd/tui/context/theme.tsx +1341 -0
  241. package/src/cli/cmd/tui/context/thinking.ts +67 -0
  242. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  243. package/src/cli/cmd/tui/event.ts +53 -0
  244. package/src/cli/cmd/tui/feature-plugins/home/footer.tsx +98 -0
  245. package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +288 -0
  246. package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +59 -0
  247. package/src/cli/cmd/tui/feature-plugins/session/dialog.tsx +356 -0
  248. package/src/cli/cmd/tui/feature-plugins/session/index.tsx +32 -0
  249. package/src/cli/cmd/tui/feature-plugins/session/preview-pane.tsx +288 -0
  250. package/src/cli/cmd/tui/feature-plugins/session/util.tsx +54 -0
  251. package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +65 -0
  252. package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +70 -0
  253. package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +96 -0
  254. package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  255. package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  256. package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +49 -0
  257. package/src/cli/cmd/tui/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
  258. package/src/cli/cmd/tui/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
  259. package/src/cli/cmd/tui/feature-plugins/system/diff-viewer-ui.tsx +103 -0
  260. package/src/cli/cmd/tui/feature-plugins/system/diff-viewer.tsx +1058 -0
  261. package/src/cli/cmd/tui/feature-plugins/system/notifications.ts +94 -0
  262. package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +269 -0
  263. package/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx +1184 -0
  264. package/src/cli/cmd/tui/feature-plugins/system/which-key.tsx +608 -0
  265. package/src/cli/cmd/tui/keymap.tsx +283 -0
  266. package/src/cli/cmd/tui/layer.ts +6 -0
  267. package/src/cli/cmd/tui/plugin/api.tsx +390 -0
  268. package/src/cli/cmd/tui/plugin/command-shim.ts +109 -0
  269. package/src/cli/cmd/tui/plugin/internal.ts +42 -0
  270. package/src/cli/cmd/tui/plugin/runtime.ts +1131 -0
  271. package/src/cli/cmd/tui/plugin/slots.tsx +60 -0
  272. package/src/cli/cmd/tui/routes/home/session-destination.tsx +39 -0
  273. package/src/cli/cmd/tui/routes/home.tsx +149 -0
  274. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  275. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +108 -0
  276. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  277. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  278. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  279. package/src/cli/cmd/tui/routes/session/index.tsx +2629 -0
  280. package/src/cli/cmd/tui/routes/session/permission.tsx +729 -0
  281. package/src/cli/cmd/tui/routes/session/question.tsx +514 -0
  282. package/src/cli/cmd/tui/routes/session/sidebar.tsx +102 -0
  283. package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +132 -0
  284. package/src/cli/cmd/tui/thread.ts +264 -0
  285. package/src/cli/cmd/tui/ui/dialog-alert.tsx +66 -0
  286. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +108 -0
  287. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +217 -0
  288. package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
  289. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +126 -0
  290. package/src/cli/cmd/tui/ui/dialog-select.tsx +712 -0
  291. package/src/cli/cmd/tui/ui/dialog.tsx +218 -0
  292. package/src/cli/cmd/tui/ui/link.tsx +34 -0
  293. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  294. package/src/cli/cmd/tui/ui/toast.tsx +102 -0
  295. package/src/cli/cmd/tui/util/audio.ts +58 -0
  296. package/src/cli/cmd/tui/util/clipboard.ts +181 -0
  297. package/src/cli/cmd/tui/util/collapse-tool-output.ts +19 -0
  298. package/src/cli/cmd/tui/util/editor.ts +43 -0
  299. package/src/cli/cmd/tui/util/layout.ts +25 -0
  300. package/src/cli/cmd/tui/util/model.ts +23 -0
  301. package/src/cli/cmd/tui/util/provider-origin.ts +7 -0
  302. package/src/cli/cmd/tui/util/revert-diff.ts +18 -0
  303. package/src/cli/cmd/tui/util/scroll.ts +25 -0
  304. package/src/cli/cmd/tui/util/selection.ts +73 -0
  305. package/src/cli/cmd/tui/util/signal.ts +41 -0
  306. package/src/cli/cmd/tui/util/transcript.ts +112 -0
  307. package/src/cli/cmd/tui/validate-session.ts +29 -0
  308. package/src/cli/cmd/tui/win32.ts +130 -0
  309. package/src/cli/cmd/tui/worker.ts +99 -0
  310. package/src/cli/cmd/uninstall.ts +353 -0
  311. package/src/cli/cmd/upgrade.ts +74 -0
  312. package/src/cli/cmd/web.ts +84 -0
  313. package/src/cli/effect/prompt.ts +37 -0
  314. package/src/cli/effect-cmd.ts +96 -0
  315. package/src/cli/error.ts +118 -0
  316. package/src/cli/heap.ts +59 -0
  317. package/src/cli/logo.ts +21 -0
  318. package/src/cli/network.ts +64 -0
  319. package/src/cli/ui.ts +132 -0
  320. package/src/cli/upgrade.ts +53 -0
  321. package/src/command/index.ts +181 -0
  322. package/src/command/template/initialize.txt +66 -0
  323. package/src/command/template/review.txt +101 -0
  324. package/src/config/agent.ts +68 -0
  325. package/src/config/command.ts +45 -0
  326. package/src/config/config.ts +679 -0
  327. package/src/config/entry-name.ts +19 -0
  328. package/src/config/managed.ts +77 -0
  329. package/src/config/markdown.ts +36 -0
  330. package/src/config/parse.ts +79 -0
  331. package/src/config/paths.ts +45 -0
  332. package/src/config/plugin.ts +79 -0
  333. package/src/config/reference.ts +48 -0
  334. package/src/config/variable.ts +91 -0
  335. package/src/control-plane/adapters/index.ts +41 -0
  336. package/src/control-plane/adapters/worktree.ts +96 -0
  337. package/src/control-plane/dev/README.md +19 -0
  338. package/src/control-plane/dev/debug-workspace-plugin.ts +73 -0
  339. package/src/control-plane/types.ts +59 -0
  340. package/src/control-plane/util.ts +39 -0
  341. package/src/control-plane/workspace-adapter-runtime.ts +51 -0
  342. package/src/control-plane/workspace-context.ts +26 -0
  343. package/src/control-plane/workspace.ts +1075 -0
  344. package/src/effect/app-runtime.ts +133 -0
  345. package/src/effect/bootstrap-runtime.ts +23 -0
  346. package/src/effect/bridge.ts +84 -0
  347. package/src/effect/config-service.ts +67 -0
  348. package/src/effect/instance-ref.ts +11 -0
  349. package/src/effect/instance-registry.ts +12 -0
  350. package/src/effect/instance-state.ts +72 -0
  351. package/src/effect/promise.ts +17 -0
  352. package/src/effect/run-service.ts +47 -0
  353. package/src/effect/runner.ts +217 -0
  354. package/src/effect/runtime-flags.ts +76 -0
  355. package/src/env/index.ts +40 -0
  356. package/src/event-v2-bridge.ts +76 -0
  357. package/src/format/formatter.ts +404 -0
  358. package/src/format/index.ts +212 -0
  359. package/src/git/index.ts +347 -0
  360. package/src/id/id.ts +80 -0
  361. package/src/ide/index.ts +70 -0
  362. package/src/image/image.ts +177 -0
  363. package/src/index.ts +208 -0
  364. package/src/installation/index.ts +349 -0
  365. package/src/lsp/client.ts +686 -0
  366. package/src/lsp/diagnostic.ts +29 -0
  367. package/src/lsp/language.ts +121 -0
  368. package/src/lsp/launch.ts +21 -0
  369. package/src/lsp/lsp.ts +517 -0
  370. package/src/lsp/server.ts +2064 -0
  371. package/src/markdown.d.ts +4 -0
  372. package/src/mcp/auth.ts +171 -0
  373. package/src/mcp/index.ts +982 -0
  374. package/src/mcp/oauth-callback.ts +232 -0
  375. package/src/mcp/oauth-provider.ts +217 -0
  376. package/src/node.ts +5 -0
  377. package/src/oryna/agent.ts +112 -0
  378. package/src/oryna/reply-service.ts +8 -0
  379. package/src/patch/index.ts +689 -0
  380. package/src/permission/arity.ts +163 -0
  381. package/src/permission/evaluate.ts +1 -0
  382. package/src/permission/index.ts +230 -0
  383. package/src/plugin/azure.ts +26 -0
  384. package/src/plugin/cloudflare.ts +76 -0
  385. package/src/plugin/digitalocean.ts +391 -0
  386. package/src/plugin/github-copilot/copilot.ts +417 -0
  387. package/src/plugin/github-copilot/models.ts +246 -0
  388. package/src/plugin/index.ts +323 -0
  389. package/src/plugin/install.ts +439 -0
  390. package/src/plugin/loader.ts +237 -0
  391. package/src/plugin/meta.ts +188 -0
  392. package/src/plugin/openai/README.md +31 -0
  393. package/src/plugin/openai/codex.ts +647 -0
  394. package/src/plugin/openai/ws-pool.ts +290 -0
  395. package/src/plugin/openai/ws.ts +381 -0
  396. package/src/plugin/oryna.ts +349 -0
  397. package/src/plugin/shared.ts +323 -0
  398. package/src/plugin/xai.ts +742 -0
  399. package/src/project/bootstrap-service.ts +9 -0
  400. package/src/project/bootstrap.ts +80 -0
  401. package/src/project/instance-context.ts +24 -0
  402. package/src/project/instance-layer.ts +11 -0
  403. package/src/project/instance-runtime.ts +16 -0
  404. package/src/project/instance-store.ts +207 -0
  405. package/src/project/project.ts +520 -0
  406. package/src/project/vcs.ts +435 -0
  407. package/src/provider/auth.ts +230 -0
  408. package/src/provider/error.ts +188 -0
  409. package/src/provider/model-status.ts +8 -0
  410. package/src/provider/provider.ts +2009 -0
  411. package/src/provider/transform.ts +1363 -0
  412. package/src/pty-preparation.ts +30 -0
  413. package/src/question/index.ts +229 -0
  414. package/src/question/schema.ts +10 -0
  415. package/src/reference/reference.ts +239 -0
  416. package/src/reference/repository-cache.ts +320 -0
  417. package/src/server/auth.ts +48 -0
  418. package/src/server/cors.ts +34 -0
  419. package/src/server/event.ts +13 -0
  420. package/src/server/global-lifecycle.ts +37 -0
  421. package/src/server/init-projectors.ts +3 -0
  422. package/src/server/mdns.ts +60 -0
  423. package/src/server/projectors.ts +1 -0
  424. package/src/server/proxy-util.ts +48 -0
  425. package/src/server/routes/instance/httpapi/AGENTS.md +39 -0
  426. package/src/server/routes/instance/httpapi/api.ts +78 -0
  427. package/src/server/routes/instance/httpapi/errors.ts +193 -0
  428. package/src/server/routes/instance/httpapi/groups/config.ts +65 -0
  429. package/src/server/routes/instance/httpapi/groups/control-plane.ts +35 -0
  430. package/src/server/routes/instance/httpapi/groups/control.ts +76 -0
  431. package/src/server/routes/instance/httpapi/groups/event.ts +29 -0
  432. package/src/server/routes/instance/httpapi/groups/experimental.ts +260 -0
  433. package/src/server/routes/instance/httpapi/groups/file.ts +172 -0
  434. package/src/server/routes/instance/httpapi/groups/global.ts +138 -0
  435. package/src/server/routes/instance/httpapi/groups/instance.ts +206 -0
  436. package/src/server/routes/instance/httpapi/groups/mcp.ts +156 -0
  437. package/src/server/routes/instance/httpapi/groups/metadata.ts +18 -0
  438. package/src/server/routes/instance/httpapi/groups/permission.ts +61 -0
  439. package/src/server/routes/instance/httpapi/groups/project-copy.ts +88 -0
  440. package/src/server/routes/instance/httpapi/groups/project.ts +93 -0
  441. package/src/server/routes/instance/httpapi/groups/provider.ts +101 -0
  442. package/src/server/routes/instance/httpapi/groups/pty.ts +172 -0
  443. package/src/server/routes/instance/httpapi/groups/query.ts +12 -0
  444. package/src/server/routes/instance/httpapi/groups/question.ts +74 -0
  445. package/src/server/routes/instance/httpapi/groups/session.ts +462 -0
  446. package/src/server/routes/instance/httpapi/groups/sync.ts +113 -0
  447. package/src/server/routes/instance/httpapi/groups/tui.ts +208 -0
  448. package/src/server/routes/instance/httpapi/groups/workspace.ts +141 -0
  449. package/src/server/routes/instance/httpapi/handlers/config.ts +151 -0
  450. package/src/server/routes/instance/httpapi/handlers/control-plane.ts +37 -0
  451. package/src/server/routes/instance/httpapi/handlers/control.ts +40 -0
  452. package/src/server/routes/instance/httpapi/handlers/event.ts +102 -0
  453. package/src/server/routes/instance/httpapi/handlers/experimental.ts +187 -0
  454. package/src/server/routes/instance/httpapi/handlers/file.ts +103 -0
  455. package/src/server/routes/instance/httpapi/handlers/global.ts +157 -0
  456. package/src/server/routes/instance/httpapi/handlers/instance.ts +110 -0
  457. package/src/server/routes/instance/httpapi/handlers/mcp.ts +111 -0
  458. package/src/server/routes/instance/httpapi/handlers/permission.ts +41 -0
  459. package/src/server/routes/instance/httpapi/handlers/project-copy.ts +157 -0
  460. package/src/server/routes/instance/httpapi/handlers/project.ts +63 -0
  461. package/src/server/routes/instance/httpapi/handlers/provider.ts +160 -0
  462. package/src/server/routes/instance/httpapi/handlers/pty.ts +258 -0
  463. package/src/server/routes/instance/httpapi/handlers/question.ts +54 -0
  464. package/src/server/routes/instance/httpapi/handlers/session-errors.ts +21 -0
  465. package/src/server/routes/instance/httpapi/handlers/session.ts +442 -0
  466. package/src/server/routes/instance/httpapi/handlers/sync.ts +95 -0
  467. package/src/server/routes/instance/httpapi/handlers/tui.ts +131 -0
  468. package/src/server/routes/instance/httpapi/handlers/workspace.ts +102 -0
  469. package/src/server/routes/instance/httpapi/lifecycle.ts +57 -0
  470. package/src/server/routes/instance/httpapi/middleware/authorization.ts +147 -0
  471. package/src/server/routes/instance/httpapi/middleware/compression.ts +64 -0
  472. package/src/server/routes/instance/httpapi/middleware/cors-vary.ts +29 -0
  473. package/src/server/routes/instance/httpapi/middleware/error.ts +36 -0
  474. package/src/server/routes/instance/httpapi/middleware/fence.ts +25 -0
  475. package/src/server/routes/instance/httpapi/middleware/instance-context.ts +43 -0
  476. package/src/server/routes/instance/httpapi/middleware/proxy.ts +108 -0
  477. package/src/server/routes/instance/httpapi/middleware/schema-error.ts +42 -0
  478. package/src/server/routes/instance/httpapi/middleware/workspace-routing.ts +250 -0
  479. package/src/server/routes/instance/httpapi/public.ts +535 -0
  480. package/src/server/routes/instance/httpapi/server.ts +277 -0
  481. package/src/server/routes/instance/httpapi/websocket-tracker.ts +57 -0
  482. package/src/server/server.ts +218 -0
  483. package/src/server/shared/fence.ts +68 -0
  484. package/src/server/shared/pty-ticket.ts +15 -0
  485. package/src/server/shared/public-ui.ts +12 -0
  486. package/src/server/shared/tui-control.ts +28 -0
  487. package/src/server/shared/ui.ts +108 -0
  488. package/src/server/shared/workspace-routing.ts +38 -0
  489. package/src/session/compaction.ts +609 -0
  490. package/src/session/instruction.ts +237 -0
  491. package/src/session/llm/AGENTS.md +90 -0
  492. package/src/session/llm/ai-sdk.ts +288 -0
  493. package/src/session/llm/native-request.ts +196 -0
  494. package/src/session/llm/native-runtime.ts +195 -0
  495. package/src/session/llm/request.ts +215 -0
  496. package/src/session/llm.ts +402 -0
  497. package/src/session/message-error.ts +14 -0
  498. package/src/session/message-v2.ts +745 -0
  499. package/src/session/message.ts +148 -0
  500. package/src/session/overflow.ts +34 -0
  501. package/src/session/processor.ts +1063 -0
  502. package/src/session/prompt/anthropic.txt +105 -0
  503. package/src/session/prompt/beast.txt +147 -0
  504. package/src/session/prompt/build-switch.txt +5 -0
  505. package/src/session/prompt/codex.txt +79 -0
  506. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  507. package/src/session/prompt/default.txt +95 -0
  508. package/src/session/prompt/gemini.txt +155 -0
  509. package/src/session/prompt/gpt.txt +107 -0
  510. package/src/session/prompt/kimi.txt +95 -0
  511. package/src/session/prompt/max-steps.txt +16 -0
  512. package/src/session/prompt/plan-mode.txt +70 -0
  513. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  514. package/src/session/prompt/plan.txt +26 -0
  515. package/src/session/prompt/reference.ts +72 -0
  516. package/src/session/prompt/trinity.txt +97 -0
  517. package/src/session/prompt.ts +1755 -0
  518. package/src/session/reminders.ts +92 -0
  519. package/src/session/retry.ts +201 -0
  520. package/src/session/revert.ts +153 -0
  521. package/src/session/run-state.ts +153 -0
  522. package/src/session/schema.ts +26 -0
  523. package/src/session/session.ts +1116 -0
  524. package/src/session/status.ts +94 -0
  525. package/src/session/summary.ts +162 -0
  526. package/src/session/system.ts +84 -0
  527. package/src/session/todo.ts +87 -0
  528. package/src/session/tools.ts +211 -0
  529. package/src/share/session.ts +58 -0
  530. package/src/share/share-next.ts +379 -0
  531. package/src/shell/shell.ts +215 -0
  532. package/src/skill/discovery.ts +115 -0
  533. package/src/skill/index.ts +357 -0
  534. package/src/snapshot/index.ts +759 -0
  535. package/src/sql.d.ts +4 -0
  536. package/src/storage/schema.ts +5 -0
  537. package/src/storage/storage.ts +329 -0
  538. package/src/sync/README.md +179 -0
  539. package/src/sync/schema.ts +11 -0
  540. package/src/temporary.ts +33 -0
  541. package/src/tool/apply_patch.ts +313 -0
  542. package/src/tool/apply_patch.txt +33 -0
  543. package/src/tool/edit.ts +737 -0
  544. package/src/tool/edit.txt +10 -0
  545. package/src/tool/external-directory.ts +49 -0
  546. package/src/tool/glob.ts +84 -0
  547. package/src/tool/glob.txt +6 -0
  548. package/src/tool/grep.ts +140 -0
  549. package/src/tool/grep.txt +8 -0
  550. package/src/tool/invalid.ts +21 -0
  551. package/src/tool/json-schema.ts +164 -0
  552. package/src/tool/lsp.ts +113 -0
  553. package/src/tool/lsp.txt +24 -0
  554. package/src/tool/mcp-websearch.ts +96 -0
  555. package/src/tool/plan-enter.txt +14 -0
  556. package/src/tool/plan-exit.txt +13 -0
  557. package/src/tool/plan.ts +79 -0
  558. package/src/tool/question.ts +44 -0
  559. package/src/tool/question.txt +10 -0
  560. package/src/tool/read.ts +392 -0
  561. package/src/tool/read.txt +14 -0
  562. package/src/tool/registry.ts +475 -0
  563. package/src/tool/reply.ts +29 -0
  564. package/src/tool/schema.ts +14 -0
  565. package/src/tool/shell/id.ts +19 -0
  566. package/src/tool/shell/prompt.ts +307 -0
  567. package/src/tool/shell/shell.txt +21 -0
  568. package/src/tool/shell.ts +660 -0
  569. package/src/tool/skill.ts +72 -0
  570. package/src/tool/skill.txt +5 -0
  571. package/src/tool/task.ts +338 -0
  572. package/src/tool/task.txt +19 -0
  573. package/src/tool/todo.ts +57 -0
  574. package/src/tool/todowrite.txt +44 -0
  575. package/src/tool/tool.ts +183 -0
  576. package/src/tool/truncate.ts +160 -0
  577. package/src/tool/truncation-dir.ts +4 -0
  578. package/src/tool/webfetch.ts +192 -0
  579. package/src/tool/webfetch.txt +13 -0
  580. package/src/tool/websearch.ts +143 -0
  581. package/src/tool/websearch.txt +14 -0
  582. package/src/tool/write.ts +104 -0
  583. package/src/tool/write.txt +8 -0
  584. package/src/util/archive.ts +17 -0
  585. package/src/util/bom.ts +27 -0
  586. package/src/util/data-url.ts +9 -0
  587. package/src/util/defer.ts +10 -0
  588. package/src/util/effect-http-client.ts +11 -0
  589. package/src/util/error.ts +88 -0
  590. package/src/util/filesystem.ts +251 -0
  591. package/src/util/format.ts +20 -0
  592. package/src/util/iife.ts +3 -0
  593. package/src/util/lan-scan.ts +90 -0
  594. package/src/util/lazy.ts +20 -0
  595. package/src/util/local-context.ts +25 -0
  596. package/src/util/locale.ts +86 -0
  597. package/src/util/media.ts +26 -0
  598. package/src/util/process.ts +176 -0
  599. package/src/util/proxy-env.ts +72 -0
  600. package/src/util/queue.ts +32 -0
  601. package/src/util/record.ts +3 -0
  602. package/src/util/repository.ts +232 -0
  603. package/src/util/rpc.ts +66 -0
  604. package/src/util/signal.ts +12 -0
  605. package/src/util/timeout.ts +13 -0
  606. package/src/util/token.ts +1 -0
  607. package/src/util/wildcard.ts +59 -0
  608. package/src/worktree/index.ts +645 -0
  609. package/sst-env.d.ts +10 -0
  610. package/test/AGENTS.md +204 -0
  611. package/test/EFFECT_TEST_MIGRATION.md +169 -0
  612. package/test/account/repo.test.ts +353 -0
  613. package/test/account/service.test.ts +453 -0
  614. package/test/acp/config-option.test.ts +229 -0
  615. package/test/acp/content.test.ts +201 -0
  616. package/test/acp/directory.test.ts +186 -0
  617. package/test/acp/error.test.ts +67 -0
  618. package/test/acp/event.test.ts +711 -0
  619. package/test/acp/permission.test.ts +273 -0
  620. package/test/acp/service-session.test.ts +1174 -0
  621. package/test/acp/session.test.ts +200 -0
  622. package/test/acp/tool.test.ts +210 -0
  623. package/test/acp/usage.test.ts +315 -0
  624. package/test/agent/agent.test.ts +710 -0
  625. package/test/agent/plan-mode-subagent-bypass.test.ts +213 -0
  626. package/test/agent/plugin-agent-regression.test.ts +62 -0
  627. package/test/auth/auth.test.ts +77 -0
  628. package/test/background/job.test.ts +243 -0
  629. package/test/cli/account.test.ts +30 -0
  630. package/test/cli/acp/acp-test-client.ts +97 -0
  631. package/test/cli/acp/config-options.test.ts +103 -0
  632. package/test/cli/acp/helpers.ts +96 -0
  633. package/test/cli/acp/initialize-auth.test.ts +61 -0
  634. package/test/cli/acp/lifecycle.test.ts +118 -0
  635. package/test/cli/acp/prompt-content.test.ts +97 -0
  636. package/test/cli/acp/skills.test.ts +38 -0
  637. package/test/cli/cmd/tui/aggregate-failures.test.ts +93 -0
  638. package/test/cli/cmd/tui/attention.test.ts +484 -0
  639. package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
  640. package/test/cli/cmd/tui/model-options.test.ts +30 -0
  641. package/test/cli/cmd/tui/notifications.test.ts +267 -0
  642. package/test/cli/cmd/tui/prompt-history.test.ts +44 -0
  643. package/test/cli/cmd/tui/prompt-part.test.ts +77 -0
  644. package/test/cli/cmd/tui/prompt-traits.test.ts +29 -0
  645. package/test/cli/cmd/tui/provider-options.test.ts +29 -0
  646. package/test/cli/cmd/tui/sync-fixture.tsx +64 -0
  647. package/test/cli/cmd/tui/sync-live-hydration.test.tsx +278 -0
  648. package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +47 -0
  649. package/test/cli/cmd/tui/sync.test.tsx +70 -0
  650. package/test/cli/effect-cmd-instance-als.test.ts +39 -0
  651. package/test/cli/error.test.ts +95 -0
  652. package/test/cli/github-action.test.ts +199 -0
  653. package/test/cli/github-remote.test.ts +90 -0
  654. package/test/cli/help/__snapshots__/help-snapshots.test.ts.snap +631 -0
  655. package/test/cli/help/help-snapshots.test.ts +137 -0
  656. package/test/cli/import.test.ts +54 -0
  657. package/test/cli/mcp-add.test.ts +74 -0
  658. package/test/cli/plugin-auth-picker.test.ts +120 -0
  659. package/test/cli/run/entry.body.test.ts +536 -0
  660. package/test/cli/run/footer.menu.test.ts +43 -0
  661. package/test/cli/run/footer.view.test.tsx +927 -0
  662. package/test/cli/run/permission.shared.test.ts +144 -0
  663. package/test/cli/run/prompt.shared.test.ts +133 -0
  664. package/test/cli/run/question.shared.test.ts +115 -0
  665. package/test/cli/run/run-process.test.ts +84 -0
  666. package/test/cli/run/runtime.boot.test.ts +282 -0
  667. package/test/cli/run/runtime.queue.test.ts +465 -0
  668. package/test/cli/run/runtime.stdin.test.ts +71 -0
  669. package/test/cli/run/scrollback.surface.test.ts +1048 -0
  670. package/test/cli/run/session-data.test.ts +595 -0
  671. package/test/cli/run/session-replay.test.ts +456 -0
  672. package/test/cli/run/session.shared.test.ts +247 -0
  673. package/test/cli/run/stream.test.ts +56 -0
  674. package/test/cli/run/stream.transport.test.ts +2363 -0
  675. package/test/cli/run/subagent-data.test.ts +456 -0
  676. package/test/cli/run/theme.test.ts +152 -0
  677. package/test/cli/run/variant.shared.test.ts +217 -0
  678. package/test/cli/serve/serve-process.test.ts +61 -0
  679. package/test/cli/smokes/read-only.test.ts +115 -0
  680. package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +72 -0
  681. package/test/cli/tui/app-lifecycle.test.ts +261 -0
  682. package/test/cli/tui/dialog-prompt.test.tsx +146 -0
  683. package/test/cli/tui/diff-viewer-file-tree-utils.test.ts +323 -0
  684. package/test/cli/tui/diff-viewer-file-tree.test.tsx +197 -0
  685. package/test/cli/tui/diff-viewer.test.tsx +230 -0
  686. package/test/cli/tui/editor-context-zed.test.ts +384 -0
  687. package/test/cli/tui/editor-context.test.tsx +288 -0
  688. package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +232 -0
  689. package/test/cli/tui/keymap.test.tsx +136 -0
  690. package/test/cli/tui/plugin-add.test.ts +110 -0
  691. package/test/cli/tui/plugin-install.test.ts +87 -0
  692. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  693. package/test/cli/tui/plugin-loader-entrypoint.test.ts +485 -0
  694. package/test/cli/tui/plugin-loader-pure.test.ts +72 -0
  695. package/test/cli/tui/plugin-loader.test.ts +1332 -0
  696. package/test/cli/tui/plugin-toggle.test.ts +264 -0
  697. package/test/cli/tui/prompt-submit-race.test.ts +98 -0
  698. package/test/cli/tui/revert-diff.test.ts +35 -0
  699. package/test/cli/tui/slot-replace.test.tsx +50 -0
  700. package/test/cli/tui/sync-v2.test.tsx +558 -0
  701. package/test/cli/tui/theme-store.test.ts +76 -0
  702. package/test/cli/tui/thinking.test.ts +36 -0
  703. package/test/cli/tui/thread.test.ts +28 -0
  704. package/test/cli/tui/transcript.test.ts +426 -0
  705. package/test/cli/tui/use-event.test.tsx +145 -0
  706. package/test/config/agent-color.test.ts +47 -0
  707. package/test/config/config.test.ts +1991 -0
  708. package/test/config/entry-name.test.ts +57 -0
  709. package/test/config/fixtures/empty-frontmatter.md +4 -0
  710. package/test/config/fixtures/frontmatter.md +28 -0
  711. package/test/config/fixtures/markdown-header.md +11 -0
  712. package/test/config/fixtures/no-frontmatter.md +1 -0
  713. package/test/config/fixtures/weird-model-id.md +13 -0
  714. package/test/config/lsp.test.ts +69 -0
  715. package/test/config/markdown.test.ts +228 -0
  716. package/test/config/plugin.test.ts +0 -0
  717. package/test/config/tui.test.ts +878 -0
  718. package/test/control-plane/adapters.test.ts +71 -0
  719. package/test/control-plane/workspace.test.ts +1704 -0
  720. package/test/effect/app-runtime-logger.test.ts +105 -0
  721. package/test/effect/config-service.test.ts +65 -0
  722. package/test/effect/instance-state.test.ts +391 -0
  723. package/test/effect/run-service.test.ts +89 -0
  724. package/test/effect/runner.test.ts +514 -0
  725. package/test/effect/runtime-flags.test.ts +373 -0
  726. package/test/fake/account.ts +9 -0
  727. package/test/fake/auth.ts +8 -0
  728. package/test/fake/npm.ts +8 -0
  729. package/test/fake/provider.ts +82 -0
  730. package/test/fake/skill.ts +8 -0
  731. package/test/filesystem/filesystem.test.ts +319 -0
  732. package/test/fixture/agent-plugin.constants.ts +6 -0
  733. package/test/fixture/agent-plugin.ts +12 -0
  734. package/test/fixture/config.ts +23 -0
  735. package/test/fixture/db.ts +11 -0
  736. package/test/fixture/fixture.test.ts +26 -0
  737. package/test/fixture/fixture.ts +224 -0
  738. package/test/fixture/flag.ts +20 -0
  739. package/test/fixture/flock-worker.ts +72 -0
  740. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  741. package/test/fixture/plug-worker.ts +93 -0
  742. package/test/fixture/plugin-meta-worker.ts +19 -0
  743. package/test/fixture/plugin.ts +10 -0
  744. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  745. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  746. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  747. package/test/fixture/skills/index.json +6 -0
  748. package/test/fixture/tui-plugin.ts +355 -0
  749. package/test/fixture/tui-runtime.ts +64 -0
  750. package/test/fixture/tui-sdk.ts +82 -0
  751. package/test/fixture/workspace.ts +30 -0
  752. package/test/fixtures/recordings/session/native-anthropic-tool-loop.json +49 -0
  753. package/test/fixtures/recordings/session/native-openai-oauth-tool-loop.json +45 -0
  754. package/test/fixtures/recordings/session/native-zen-tool-loop.json +49 -0
  755. package/test/format/format.test.ts +228 -0
  756. package/test/git/git.test.ts +178 -0
  757. package/test/ide/ide.test.ts +82 -0
  758. package/test/image/fixtures/picture-5mb-base64.png +0 -0
  759. package/test/image/image.test.ts +123 -0
  760. package/test/installation/installation.test.ts +230 -0
  761. package/test/lib/cli-process.ts +459 -0
  762. package/test/lib/effect.ts +177 -0
  763. package/test/lib/filesystem.ts +10 -0
  764. package/test/lib/llm-server.ts +771 -0
  765. package/test/lib/snapshot.ts +73 -0
  766. package/test/lib/test-provider.ts +37 -0
  767. package/test/lib/websocket.ts +46 -0
  768. package/test/lsp/client.test.ts +493 -0
  769. package/test/lsp/index.test.ts +232 -0
  770. package/test/lsp/launch.test.ts +22 -0
  771. package/test/lsp/lifecycle.test.ts +160 -0
  772. package/test/mcp/auth.test.ts +78 -0
  773. package/test/mcp/headers.test.ts +126 -0
  774. package/test/mcp/lifecycle.test.ts +888 -0
  775. package/test/mcp/oauth-auto-connect.test.ts +236 -0
  776. package/test/mcp/oauth-browser.test.ts +228 -0
  777. package/test/mcp/oauth-callback.test.ts +34 -0
  778. package/test/mcp/oauth-provider.test.ts +61 -0
  779. package/test/patch/patch.test.ts +383 -0
  780. package/test/permission/arity.test.ts +33 -0
  781. package/test/permission/next.test.ts +1176 -0
  782. package/test/permission-task.test.ts +318 -0
  783. package/test/plugin/auth-override.test.ts +105 -0
  784. package/test/plugin/cloudflare.test.ts +68 -0
  785. package/test/plugin/codex.test.ts +247 -0
  786. package/test/plugin/github-copilot-models.test.ts +332 -0
  787. package/test/plugin/install-concurrency.test.ts +140 -0
  788. package/test/plugin/install.test.ts +570 -0
  789. package/test/plugin/loader-shared.test.ts +1303 -0
  790. package/test/plugin/meta.test.ts +137 -0
  791. package/test/plugin/openai-rollout.test.ts +17 -0
  792. package/test/plugin/openai-ws.test.ts +877 -0
  793. package/test/plugin/shared.test.ts +88 -0
  794. package/test/plugin/trigger.test.ts +120 -0
  795. package/test/plugin/workspace-adapter.test.ts +137 -0
  796. package/test/plugin/xai.test.ts +634 -0
  797. package/test/preload.ts +95 -0
  798. package/test/project/instance-bootstrap.test.ts +110 -0
  799. package/test/project/instance.test.ts +245 -0
  800. package/test/project/migrate-global.test.ts +170 -0
  801. package/test/project/project-directory.test.ts +169 -0
  802. package/test/project/project.test.ts +818 -0
  803. package/test/project/vcs.test.ts +336 -0
  804. package/test/project/worktree-remove.test.ts +126 -0
  805. package/test/project/worktree.test.ts +320 -0
  806. package/test/provider/amazon-bedrock.test.ts +360 -0
  807. package/test/provider/cf-ai-gateway-e2e.test.ts +132 -0
  808. package/test/provider/digitalocean.test.ts +123 -0
  809. package/test/provider/gitlab-duo.test.ts +412 -0
  810. package/test/provider/header-timeout.test.ts +233 -0
  811. package/test/provider/model-status.test.ts +61 -0
  812. package/test/provider/provider.test.ts +1793 -0
  813. package/test/provider/transform.test.ts +3937 -0
  814. package/test/pty/pty-shell.test.ts +102 -0
  815. package/test/question/question.test.ts +465 -0
  816. package/test/reference/reference.test.ts +310 -0
  817. package/test/server/AGENTS.md +15 -0
  818. package/test/server/auth.test.ts +59 -0
  819. package/test/server/global-bus.ts +31 -0
  820. package/test/server/global-session-list.test.ts +107 -0
  821. package/test/server/httpapi-authorization.test.ts +174 -0
  822. package/test/server/httpapi-compression.test.ts +154 -0
  823. package/test/server/httpapi-config.test.ts +113 -0
  824. package/test/server/httpapi-control-plane.test.ts +63 -0
  825. package/test/server/httpapi-cors-vary.test.ts +66 -0
  826. package/test/server/httpapi-cors.test.ts +122 -0
  827. package/test/server/httpapi-error-middleware.test.ts +96 -0
  828. package/test/server/httpapi-event.test.ts +97 -0
  829. package/test/server/httpapi-exercise/assertions.ts +64 -0
  830. package/test/server/httpapi-exercise/backend.ts +144 -0
  831. package/test/server/httpapi-exercise/dsl.ts +210 -0
  832. package/test/server/httpapi-exercise/environment.ts +40 -0
  833. package/test/server/httpapi-exercise/index.ts +1535 -0
  834. package/test/server/httpapi-exercise/report.ts +66 -0
  835. package/test/server/httpapi-exercise/routing.ts +96 -0
  836. package/test/server/httpapi-exercise/runner.ts +267 -0
  837. package/test/server/httpapi-exercise/runtime.ts +52 -0
  838. package/test/server/httpapi-exercise/types.ts +123 -0
  839. package/test/server/httpapi-experimental.test.ts +300 -0
  840. package/test/server/httpapi-file.test.ts +76 -0
  841. package/test/server/httpapi-global.test.ts +66 -0
  842. package/test/server/httpapi-instance-context.test.ts +347 -0
  843. package/test/server/httpapi-instance-route-auth.test.ts +84 -0
  844. package/test/server/httpapi-instance.test.ts +265 -0
  845. package/test/server/httpapi-layer.ts +33 -0
  846. package/test/server/httpapi-listen.test.ts +415 -0
  847. package/test/server/httpapi-mcp-oauth.test.ts +73 -0
  848. package/test/server/httpapi-mcp.test.ts +226 -0
  849. package/test/server/httpapi-mdns.test.ts +82 -0
  850. package/test/server/httpapi-promptasync-context.test.ts +222 -0
  851. package/test/server/httpapi-provider.test.ts +403 -0
  852. package/test/server/httpapi-pty.test.ts +275 -0
  853. package/test/server/httpapi-public-openapi.test.ts +297 -0
  854. package/test/server/httpapi-query-schema-drift.test.ts +330 -0
  855. package/test/server/httpapi-schema-error-body.test.ts +165 -0
  856. package/test/server/httpapi-sdk.test.ts +909 -0
  857. package/test/server/httpapi-session.test.ts +1013 -0
  858. package/test/server/httpapi-sync.test.ts +154 -0
  859. package/test/server/httpapi-ui.test.ts +456 -0
  860. package/test/server/httpapi-v2-location.test.ts +85 -0
  861. package/test/server/httpapi-workspace-routing.test.ts +554 -0
  862. package/test/server/httpapi-workspace.test.ts +515 -0
  863. package/test/server/negative-tokens-regression.test.ts +83 -0
  864. package/test/server/project-copy.test.ts +101 -0
  865. package/test/server/project-init-git.test.ts +117 -0
  866. package/test/server/proxy-util.test.ts +113 -0
  867. package/test/server/sdk-error-shape.test.ts +84 -0
  868. package/test/server/sdk-v1-smoke.test.ts +60 -0
  869. package/test/server/session-actions.test.ts +112 -0
  870. package/test/server/session-diff-missing-patch.test.ts +99 -0
  871. package/test/server/session-list.test.ts +314 -0
  872. package/test/server/session-messages.test.ts +182 -0
  873. package/test/server/session-select.test.ts +69 -0
  874. package/test/server/workspace-proxy.test.ts +181 -0
  875. package/test/server/workspace-routing.test.ts +94 -0
  876. package/test/server/worktree-endpoint-repro.test.ts +307 -0
  877. package/test/session/compaction.test.ts +1835 -0
  878. package/test/session/instruction.test.ts +256 -0
  879. package/test/session/llm-native-recorded.test.ts +433 -0
  880. package/test/session/llm-native.test.ts +760 -0
  881. package/test/session/llm.test.ts +1932 -0
  882. package/test/session/message-v2.test.ts +1661 -0
  883. package/test/session/messages-pagination.test.ts +1059 -0
  884. package/test/session/processor-effect.test.ts +1101 -0
  885. package/test/session/prompt.test.ts +2318 -0
  886. package/test/session/retry.test.ts +439 -0
  887. package/test/session/revert-compact.test.ts +642 -0
  888. package/test/session/schema-decoding.test.ts +313 -0
  889. package/test/session/session-schema.test.ts +78 -0
  890. package/test/session/session.test.ts +251 -0
  891. package/test/session/snapshot-tool-race.test.ts +280 -0
  892. package/test/session/structured-output-integration.test.ts +235 -0
  893. package/test/session/structured-output.test.ts +387 -0
  894. package/test/session/system.test.ts +84 -0
  895. package/test/share/share-next.test.ts +344 -0
  896. package/test/shell/shell.test.ts +99 -0
  897. package/test/skill/discovery.test.ts +139 -0
  898. package/test/skill/skill.test.ts +571 -0
  899. package/test/snapshot/snapshot.test.ts +1121 -0
  900. package/test/storage/storage.test.ts +296 -0
  901. package/test/storage/workspace-time-migration.test.ts +50 -0
  902. package/test/tool/__snapshots__/parameters.test.ts.snap +484 -0
  903. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  904. package/test/tool/apply_patch.test.ts +533 -0
  905. package/test/tool/edit.test.ts +578 -0
  906. package/test/tool/external-directory.test.ts +155 -0
  907. package/test/tool/fixtures/large-image.png +0 -0
  908. package/test/tool/fixtures/models-api.json +117299 -0
  909. package/test/tool/glob.test.ts +188 -0
  910. package/test/tool/grep.test.ts +266 -0
  911. package/test/tool/lsp.test.ts +181 -0
  912. package/test/tool/parameters.test.ts +293 -0
  913. package/test/tool/question.test.ts +138 -0
  914. package/test/tool/read.test.ts +654 -0
  915. package/test/tool/registry.test.ts +539 -0
  916. package/test/tool/shell.test.ts +1238 -0
  917. package/test/tool/skill.test.ts +132 -0
  918. package/test/tool/task.test.ts +901 -0
  919. package/test/tool/tool-define.test.ts +153 -0
  920. package/test/tool/truncation.test.ts +266 -0
  921. package/test/tool/webfetch.test.ts +113 -0
  922. package/test/tool/websearch.test.ts +99 -0
  923. package/test/tool/write.test.ts +276 -0
  924. package/test/util/data-url.test.ts +14 -0
  925. package/test/util/error.test.ts +64 -0
  926. package/test/util/filesystem.test.ts +656 -0
  927. package/test/util/format.test.ts +59 -0
  928. package/test/util/glob.test.ts +164 -0
  929. package/test/util/iife.test.ts +36 -0
  930. package/test/util/lazy.test.ts +50 -0
  931. package/test/util/log.test.ts +77 -0
  932. package/test/util/module.test.ts +59 -0
  933. package/test/util/process.test.ts +128 -0
  934. package/test/util/repository.test.ts +93 -0
  935. package/test/util/timeout.test.ts +21 -0
  936. package/test/util/wildcard.test.ts +90 -0
  937. package/test/v2/session-message-updater.test.ts +270 -0
  938. package/tsconfig.json +17 -0
@@ -0,0 +1,2629 @@
1
+ import {
2
+ batch,
3
+ createContext,
4
+ createEffect,
5
+ createMemo,
6
+ createSignal,
7
+ For,
8
+ Match,
9
+ on,
10
+ onMount,
11
+ Show,
12
+ Switch,
13
+ untrack,
14
+ useContext,
15
+ } from "solid-js"
16
+ import { Dynamic } from "solid-js/web"
17
+ import path from "path"
18
+ import { useRoute, useRouteData } from "@tui/context/route"
19
+ import { useProject } from "@tui/context/project"
20
+ import { useSync } from "@tui/context/sync"
21
+ import { useEvent } from "@tui/context/event"
22
+ import { SplitBorder } from "@tui/component/border"
23
+ import { Spinner } from "@tui/component/spinner"
24
+ import { createSyntaxStyleMemo, generateSubtleSyntax, selectedForeground, useTheme } from "@tui/context/theme"
25
+ import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
26
+ import { Prompt, type PromptRef } from "@tui/component/prompt"
27
+ import type {
28
+ AssistantMessage,
29
+ Part,
30
+ Provider,
31
+ ToolPart,
32
+ UserMessage,
33
+ TextPart,
34
+ ReasoningPart,
35
+ } from "@opencode-ai/sdk/v2"
36
+ import { useLocal } from "@tui/context/local"
37
+ import { Locale } from "@/util/locale"
38
+ import type { Tool } from "@/tool/tool"
39
+ import type { ReadTool } from "@/tool/read"
40
+ import type { WriteTool } from "@/tool/write"
41
+ import { ShellTool } from "@/tool/shell"
42
+ import { ShellID } from "@/tool/shell/id"
43
+ import type { GlobTool } from "@/tool/glob"
44
+ import { TodoWriteTool } from "@/tool/todo"
45
+ import type { GrepTool } from "@/tool/grep"
46
+ import type { EditTool } from "@/tool/edit"
47
+ import type { ApplyPatchTool } from "@/tool/apply_patch"
48
+ import type { WebFetchTool } from "@/tool/webfetch"
49
+ import { webSearchProviderLabel, type WebSearchTool } from "@/tool/websearch"
50
+ import type { TaskTool } from "@/tool/task"
51
+ import type { QuestionTool } from "@/tool/question"
52
+ import type { SkillTool } from "@/tool/skill"
53
+ import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
54
+ import { useSDK } from "@tui/context/sdk"
55
+ import { useEditorContext } from "@tui/context/editor"
56
+ import { useDialog } from "../../ui/dialog"
57
+ import { DialogAlert } from "../../ui/dialog-alert"
58
+ import { TodoItem } from "../../component/todo-item"
59
+ import { DialogMessage } from "./dialog-message"
60
+ import type { PromptInfo } from "../../component/prompt/history"
61
+ import { DialogConfirm } from "@tui/ui/dialog-confirm"
62
+ import { DialogTimeline } from "./dialog-timeline"
63
+ import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
64
+ import { DialogSessionRename } from "../../component/dialog-session-rename"
65
+ import { Sidebar } from "./sidebar"
66
+ import { SubagentFooter } from "./subagent-footer.tsx"
67
+ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
68
+ import parsers from "../../../../../../parsers-config.ts"
69
+ import * as Clipboard from "../../util/clipboard"
70
+ import { errorMessage } from "@/util/error"
71
+ import { Toast, useToast } from "../../ui/toast"
72
+ import { useKV } from "../../context/kv.tsx"
73
+ import * as Editor from "../../util/editor"
74
+ import stripAnsi from "strip-ansi"
75
+ import { usePromptRef } from "../../context/prompt"
76
+ import { useExit } from "../../context/exit"
77
+ import { Filesystem } from "@/util/filesystem"
78
+ import { PermissionPrompt } from "./permission"
79
+ import { QuestionPrompt } from "./question"
80
+ import { DialogExportOptions } from "../../ui/dialog-export-options"
81
+ import * as Model from "../../util/model"
82
+ import { formatTranscript } from "../../util/transcript"
83
+ import { setPreLayoutSiblingMargin } from "../../util/layout"
84
+ import { UI } from "@/cli/ui.ts"
85
+ import { useTuiConfig } from "../../context/tui-config"
86
+ import { nextThinkingMode, reasoningSummary, useThinkingMode, type ThinkingMode } from "../../context/thinking"
87
+ import { getScrollAcceleration } from "../../util/scroll"
88
+ import { collapseToolOutput } from "../../util/collapse-tool-output"
89
+ import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
90
+ import { DialogRetryAction } from "../../component/dialog-retry-action"
91
+ import { SessionRetry } from "@/session/retry"
92
+ import { getRevertDiffFiles } from "../../util/revert-diff"
93
+ import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap"
94
+ import { PathFormatterProvider, usePathFormatter } from "../../context/path-format"
95
+ import { agentStatus } from "../../context/agent"
96
+ import { start as startAgent, setMessageHandler, stop as stopAgent } from "@/oryna/agent"
97
+
98
+ addDefaultParsers(parsers.parsers)
99
+
100
+ const GO_UPSELL_FREE_TIER_LAST_SEEN_AT = "go_upsell_last_seen_at"
101
+ const GO_UPSELL_FREE_TIER_DONT_SHOW = "go_upsell_dont_show"
102
+ const GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT = "go_upsell_account_rate_limit_last_seen_at"
103
+ const GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW = "go_upsell_account_rate_limit_dont_show"
104
+ const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs
105
+ const GO_UPSELL_PROVIDERS = new Set(["opencode", "opencode-go"])
106
+
107
+ function goUpsellKeys(action: SessionRetry.Retryable["action"]) {
108
+ if (!action) return
109
+ if (!GO_UPSELL_PROVIDERS.has(action.provider)) return
110
+ if (action.reason === "free_tier_limit") {
111
+ return {
112
+ lastSeenAt: GO_UPSELL_FREE_TIER_LAST_SEEN_AT,
113
+ dontShow: GO_UPSELL_FREE_TIER_DONT_SHOW,
114
+ }
115
+ }
116
+ if (action.reason === "account_rate_limit") {
117
+ return {
118
+ lastSeenAt: GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT,
119
+ dontShow: GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW,
120
+ }
121
+ }
122
+ }
123
+
124
+ const sessionBindingCommands = [
125
+ "session.share",
126
+ "session.rename",
127
+ "session.timeline",
128
+ "session.fork",
129
+ "session.compact",
130
+ "session.unshare",
131
+ "session.undo",
132
+ "session.redo",
133
+ "session.sidebar.toggle",
134
+ "session.toggle.conceal",
135
+ "session.toggle.timestamps",
136
+ "session.toggle.thinking",
137
+ "session.toggle.actions",
138
+ "session.toggle.scrollbar",
139
+ "session.toggle.generic_tool_output",
140
+ "session.first",
141
+ "session.last",
142
+ "session.messages_last_user",
143
+ "session.message.next",
144
+ "session.message.previous",
145
+ "messages.copy",
146
+ "session.copy",
147
+ "session.export",
148
+ "session.child.first",
149
+ "session.parent",
150
+ "session.child.next",
151
+ "session.child.previous",
152
+ ] as const
153
+
154
+ const sessionGlobalBindingCommands = [
155
+ "session.page.up",
156
+ "session.page.down",
157
+ "session.line.up",
158
+ "session.line.down",
159
+ "session.half.page.up",
160
+ "session.half.page.down",
161
+ ] as const
162
+
163
+ const sessionGlobalUnfocusedBindingCommands = ["session.first", "session.last"] as const
164
+
165
+ const context = createContext<{
166
+ width: number
167
+ sessionID: string
168
+ conceal: () => boolean
169
+ thinkingMode: () => ThinkingMode
170
+ showThinking: () => boolean
171
+ showTimestamps: () => boolean
172
+ showDetails: () => boolean
173
+ showGenericToolOutput: () => boolean
174
+ userMessageIDs: () => ReadonlySet<string>
175
+ diffWrapMode: () => "word" | "none"
176
+ providers: () => ReadonlyMap<string, Provider>
177
+ sync: ReturnType<typeof useSync>
178
+ tui: ReturnType<typeof useTuiConfig>
179
+ }>()
180
+
181
+ function use() {
182
+ const ctx = useContext(context)
183
+ if (!ctx) throw new Error("useContext must be used within a Session component")
184
+ return ctx
185
+ }
186
+
187
+ export function Session() {
188
+ const route = useRouteData("session")
189
+ const { navigate } = useRoute()
190
+ const sync = useSync()
191
+ const event = useEvent()
192
+ const project = useProject()
193
+ const tuiConfig = useTuiConfig()
194
+ const kv = useKV()
195
+ const { theme } = useTheme()
196
+ const promptRef = usePromptRef()
197
+ const session = createMemo(() => sync.session.get(route.sessionID))
198
+ const children = createMemo(() => {
199
+ const parentID = session()?.parentID ?? session()?.id
200
+ return sync.data.session
201
+ .filter((x) => x.parentID === parentID || x.id === parentID)
202
+ .toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
203
+ })
204
+ const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
205
+ const foregroundTasks = createMemo(() =>
206
+ messages().flatMap((message) =>
207
+ (sync.data.part[message.id] ?? []).filter(
208
+ (part): part is ToolPart =>
209
+ part.type === "tool" &&
210
+ part.tool === "task" &&
211
+ part.state.status === "running" &&
212
+ part.state.metadata?.background !== true,
213
+ ),
214
+ ),
215
+ )
216
+ const userMessageIDs = createMemo(
217
+ () =>
218
+ new Set(
219
+ messages()
220
+ .filter((message) => message.role === "user")
221
+ .map((message) => message.id),
222
+ ),
223
+ )
224
+ const permissions = createMemo(() => {
225
+ if (session()?.parentID) return []
226
+ return children().flatMap((x) => sync.data.permission[x.id] ?? [])
227
+ })
228
+ const questions = createMemo(() => {
229
+ if (session()?.parentID) return []
230
+ return children().flatMap((x) => sync.data.question[x.id] ?? [])
231
+ })
232
+ const visible = createMemo(() => !session()?.parentID && permissions().length === 0 && questions().length === 0)
233
+ const disabled = createMemo(() => permissions().length > 0 || questions().length > 0)
234
+
235
+ const pending = createMemo(() => {
236
+ const completed = messages().findLast((x) => x.role === "assistant" && x.time.completed)?.id
237
+ return messages().findLast((x) => x.role === "assistant" && !x.time.completed && (!completed || x.id > completed))
238
+ ?.id
239
+ })
240
+
241
+ const lastAssistant = createMemo(() => {
242
+ return messages().findLast((x) => x.role === "assistant")
243
+ })
244
+
245
+ const dimensions = useTerminalDimensions()
246
+ const [sidebar, setSidebar] = kv.signal<"auto" | "hide">("sidebar", "auto")
247
+ const [sidebarOpen, setSidebarOpen] = createSignal(false)
248
+ const [conceal, setConceal] = createSignal(true)
249
+ const thinking = useThinkingMode()
250
+ const thinkingMode = thinking.mode
251
+ const showThinking = createMemo(() => true)
252
+ const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
253
+ const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
254
+ const [showAssistantMetadata, _setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
255
+ const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
256
+ const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
257
+ const [_animationsEnabled, _setAnimationsEnabled] = kv.signal("animations_enabled", true)
258
+ const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
259
+
260
+ const wide = createMemo(() => dimensions().width > 120)
261
+ const sidebarVisible = createMemo(() => {
262
+ if (session()?.parentID) return false
263
+ if (sidebarOpen()) return true
264
+ if (sidebar() === "auto" && wide()) return true
265
+ return false
266
+ })
267
+ const showTimestamps = createMemo(() => timestamps() === "show")
268
+ const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
269
+ const providers = createMemo(() => Model.index(sync.data.provider))
270
+
271
+ const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
272
+ const toast = useToast()
273
+ const sdk = useSDK()
274
+ const editor = useEditorContext()
275
+
276
+ createEffect(() => {
277
+ const sessionID = route.sessionID
278
+ void (async () => {
279
+ const previousWorkspace = untrack(() => project.workspace.current())
280
+ const result = await sdk.client.session.get({ sessionID }, { throwOnError: true })
281
+ if (!result.data) {
282
+ toast.show({
283
+ message: `Session not found: ${sessionID}`,
284
+ variant: "error",
285
+ duration: 5000,
286
+ })
287
+ navigate({ type: "home" })
288
+ return
289
+ }
290
+
291
+ if (result.data.workspaceID !== previousWorkspace) {
292
+ project.workspace.set(result.data.workspaceID)
293
+
294
+ // Sync all the data for this workspace. Note that this
295
+ // workspace may not exist anymore which is why this is not
296
+ // fatal. If it doesn't we still want to show the session
297
+ // (which will be non-interactive)
298
+ try {
299
+ await sync.bootstrap({ fatal: false })
300
+ } catch {}
301
+ }
302
+ editor.reconnect(result.data.directory)
303
+ await sync.session.sync(sessionID)
304
+ if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
305
+ })().catch((error) => {
306
+ if (route.sessionID !== sessionID) return
307
+ toast.show({
308
+ message: errorMessage(error),
309
+ variant: "error",
310
+ duration: 5000,
311
+ })
312
+ navigate({ type: "home" })
313
+ })
314
+ })
315
+
316
+ let lastSwitch: string | undefined = undefined
317
+ event.on("message.part.updated", (evt) => {
318
+ const part = evt.properties.part
319
+ if (part.type !== "tool") return
320
+ if (part.sessionID !== route.sessionID) return
321
+ if (part.state.status !== "completed") return
322
+ if (part.id === lastSwitch) return
323
+
324
+ if (part.tool === "plan_exit") {
325
+ local.agent.set("build")
326
+ lastSwitch = part.id
327
+ } else if (part.tool === "plan_enter") {
328
+ local.agent.set("plan")
329
+ lastSwitch = part.id
330
+ }
331
+ })
332
+
333
+ let seeded = false
334
+ let scroll: ScrollBoxRenderable
335
+ let prompt: PromptRef | undefined
336
+ const bind = (r: PromptRef | undefined) => {
337
+ prompt = r
338
+ promptRef.set(r)
339
+ if (seeded || !route.prompt || !r) return
340
+ seeded = true
341
+ r.set(route.prompt)
342
+ }
343
+ const keymap = useOpencodeKeymap()
344
+ const dialog = useDialog()
345
+ const renderer = useRenderer()
346
+
347
+ event.on("session.status", (evt) => {
348
+ if (evt.properties.sessionID !== route.sessionID) return
349
+ if (evt.properties.status.type !== "retry") return
350
+ if (!evt.properties.status.action) return
351
+ if (dialog.stack.length > 0) return
352
+
353
+ const keys = goUpsellKeys(evt.properties.status.action)
354
+ if (!keys) return
355
+
356
+ const seen = kv.get(keys.lastSeenAt)
357
+ if (typeof seen === "number" && Date.now() - seen < GO_UPSELL_WINDOW) return
358
+
359
+ if (kv.get(keys.dontShow)) return
360
+
361
+ void DialogRetryAction.show(dialog, evt.properties.status.action).then((dontShowAgain) => {
362
+ if (dontShowAgain) kv.set(keys.dontShow, true)
363
+ kv.set(keys.lastSeenAt, Date.now())
364
+ })
365
+ })
366
+
367
+ const exit = useExit()
368
+
369
+ createEffect(() => {
370
+ const title = Locale.truncate(session()?.title ?? "", 50)
371
+ const pad = (text: string) => text.padEnd(10, " ")
372
+ const weak = (text: string) => UI.Style.TEXT_DIM + pad(text) + UI.Style.TEXT_NORMAL
373
+ const logo = UI.logo(" ").split(/\r?\n/)
374
+ return exit.message.set(
375
+ [
376
+ `${logo[0] ?? ""}`,
377
+ `${logo[1] ?? ""}`,
378
+ `${logo[2] ?? ""}`,
379
+ `${logo[3] ?? ""}`,
380
+ ``,
381
+ ` ${weak("Session")}${UI.Style.TEXT_NORMAL_BOLD}${title}${UI.Style.TEXT_NORMAL}`,
382
+ ` ${weak("Continue")}${UI.Style.TEXT_NORMAL_BOLD}opencode -s ${session()?.id}${UI.Style.TEXT_NORMAL}`,
383
+ ``,
384
+ ].join("\n"),
385
+ )
386
+ })
387
+
388
+ // Helper: Find next visible message boundary in direction
389
+ const findNextVisibleMessage = (direction: "next" | "prev"): string | null => {
390
+ const children = scroll.getChildren()
391
+ const messagesList = messages()
392
+ const scrollTop = scroll.y
393
+
394
+ // Get visible messages sorted by position, filtering for valid non-synthetic, non-ignored content
395
+ const visibleMessages = children
396
+ .filter((c) => {
397
+ if (!c.id) return false
398
+ const message = messagesList.find((m) => m.id === c.id)
399
+ if (!message) return false
400
+
401
+ // Check if message has valid non-synthetic, non-ignored text parts
402
+ const parts = sync.data.part[message.id]
403
+ if (!parts || !Array.isArray(parts)) return false
404
+
405
+ return parts.some((part) => part && part.type === "text" && !part.synthetic && !part.ignored)
406
+ })
407
+ .sort((a, b) => a.y - b.y)
408
+
409
+ if (visibleMessages.length === 0) return null
410
+
411
+ if (direction === "next") {
412
+ // Find first message below current position
413
+ return visibleMessages.find((c) => c.y > scrollTop + 10)?.id ?? null
414
+ }
415
+ // Find last message above current position
416
+ return [...visibleMessages].reverse().find((c) => c.y < scrollTop - 10)?.id ?? null
417
+ }
418
+
419
+ // Helper: Scroll to message in direction or fallback to page scroll
420
+ const scrollToMessage = (direction: "next" | "prev", dialog: ReturnType<typeof useDialog>) => {
421
+ const targetID = findNextVisibleMessage(direction)
422
+
423
+ if (!targetID) {
424
+ scroll.scrollBy(direction === "next" ? scroll.height : -scroll.height)
425
+ dialog.clear()
426
+ return
427
+ }
428
+
429
+ const child = scroll.getChildren().find((c) => c.id === targetID)
430
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
431
+ dialog.clear()
432
+ }
433
+
434
+ function toBottom() {
435
+ setTimeout(() => {
436
+ if (!scroll || scroll.isDestroyed) return
437
+ scroll.scrollTo(scroll.scrollHeight)
438
+ }, 50)
439
+ }
440
+
441
+ const local = useLocal()
442
+
443
+ function enterChild(sessionID: string) {
444
+ navigate({
445
+ type: "session",
446
+ sessionID,
447
+ })
448
+ const status = sync.data.session_status[sessionID]
449
+ if (status?.type === "retry") void DialogAlert.show(dialog, "Retry Error", status.message)
450
+ }
451
+
452
+ function moveFirstChild() {
453
+ if (children().length === 1) return
454
+ const next = children().find((x) => !!x.parentID)
455
+ if (next) enterChild(next.id)
456
+ }
457
+
458
+ function moveChild(direction: number) {
459
+ if (children().length === 1) return
460
+
461
+ const sessions = children().filter((x) => !!x.parentID)
462
+ let next = sessions.findIndex((x) => x.id === session()?.id) - direction
463
+
464
+ if (next >= sessions.length) next = 0
465
+ if (next < 0) next = sessions.length - 1
466
+ if (sessions[next]) enterChild(sessions[next].id)
467
+ }
468
+
469
+ function childSessionHandler(func: () => void) {
470
+ return () => {
471
+ if (!session()?.parentID || dialog.stack.length > 0) return
472
+ func()
473
+ }
474
+ }
475
+
476
+ const sessionCommandList = createMemo(() => [
477
+ {
478
+ title: session()?.share?.url ? "Copy share link" : "Share session",
479
+ value: "session.share",
480
+ suggested: route.type === "session",
481
+ category: "Session",
482
+ enabled: sync.data.config.share !== "disabled",
483
+ slash: {
484
+ name: "share",
485
+ },
486
+ run: async () => {
487
+ const copy = (url: string) =>
488
+ Clipboard.copy(url)
489
+ .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
490
+ .catch(() => toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }))
491
+ const url = session()?.share?.url
492
+ if (url) {
493
+ await copy(url)
494
+ dialog.clear()
495
+ return
496
+ }
497
+ if (!kv.get("share_consent", false)) {
498
+ const ok = await DialogConfirm.show(dialog, "Share Session", "Are you sure you want to share it?")
499
+ if (ok !== true) return
500
+ kv.set("share_consent", true)
501
+ }
502
+ await sdk.client.session
503
+ .share({
504
+ sessionID: route.sessionID,
505
+ })
506
+ .then((res) => copy(res.data!.share!.url))
507
+ .catch((error) => {
508
+ toast.show({
509
+ message: error instanceof Error ? error.message : "Failed to share session",
510
+ variant: "error",
511
+ })
512
+ })
513
+ dialog.clear()
514
+ },
515
+ },
516
+ {
517
+ title: "Rename session",
518
+ value: "session.rename",
519
+ category: "Session",
520
+ slash: {
521
+ name: "rename",
522
+ },
523
+ run: () => {
524
+ dialog.replace(() => <DialogSessionRename session={route.sessionID} />)
525
+ },
526
+ },
527
+ {
528
+ title: "Jump to message",
529
+ value: "session.timeline",
530
+ category: "Session",
531
+ slash: {
532
+ name: "timeline",
533
+ },
534
+ run: () => {
535
+ dialog.replace(() => (
536
+ <DialogTimeline
537
+ onMove={(messageID) => {
538
+ const child = scroll.getChildren().find((child) => {
539
+ return child.id === messageID
540
+ })
541
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
542
+ }}
543
+ sessionID={route.sessionID}
544
+ setPrompt={(promptInfo) => prompt?.set(promptInfo)}
545
+ />
546
+ ))
547
+ },
548
+ },
549
+ {
550
+ title: "Fork session",
551
+ value: "session.fork",
552
+ category: "Session",
553
+ slash: {
554
+ name: "fork",
555
+ },
556
+ run: () => {
557
+ dialog.replace(() => (
558
+ <DialogForkFromTimeline
559
+ onMove={(messageID) => {
560
+ if (!messageID) return
561
+ const child = scroll.getChildren().find((child) => {
562
+ return child.id === messageID
563
+ })
564
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
565
+ }}
566
+ sessionID={route.sessionID}
567
+ />
568
+ ))
569
+ },
570
+ },
571
+ {
572
+ title: "Compact session",
573
+ value: "session.compact",
574
+ category: "Session",
575
+ slash: {
576
+ name: "compact",
577
+ aliases: ["summarize"],
578
+ },
579
+ run: () => {
580
+ const selectedModel = local.model.current()
581
+ if (!selectedModel) {
582
+ toast.show({
583
+ variant: "warning",
584
+ message: "Connect a provider to summarize this session",
585
+ duration: 3000,
586
+ })
587
+ return
588
+ }
589
+ void sdk.client.session.summarize({
590
+ sessionID: route.sessionID,
591
+ modelID: selectedModel.modelID,
592
+ providerID: selectedModel.providerID,
593
+ })
594
+ dialog.clear()
595
+ },
596
+ },
597
+ {
598
+ title: "Unshare session",
599
+ value: "session.unshare",
600
+ category: "Session",
601
+ enabled: !!session()?.share?.url,
602
+ slash: {
603
+ name: "unshare",
604
+ },
605
+ run: async () => {
606
+ await sdk.client.session
607
+ .unshare({
608
+ sessionID: route.sessionID,
609
+ })
610
+ .then(() => toast.show({ message: "Session unshared successfully", variant: "success" }))
611
+ .catch((error) => {
612
+ toast.show({
613
+ message: error instanceof Error ? error.message : "Failed to unshare session",
614
+ variant: "error",
615
+ })
616
+ })
617
+ dialog.clear()
618
+ },
619
+ },
620
+ {
621
+ title: "Undo previous message",
622
+ value: "session.undo",
623
+ category: "Session",
624
+ slash: {
625
+ name: "undo",
626
+ },
627
+ run: async () => {
628
+ const status = sync.data.session_status?.[route.sessionID]
629
+ if (status?.type !== "idle") await sdk.client.session.abort({ sessionID: route.sessionID }).catch(() => {})
630
+ const revert = session()?.revert?.messageID
631
+ const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user")
632
+ if (!message) return
633
+ void sdk.client.session
634
+ .revert({
635
+ sessionID: route.sessionID,
636
+ messageID: message.id,
637
+ })
638
+ .then(() => {
639
+ toBottom()
640
+ })
641
+ const parts = sync.data.part[message.id]
642
+ prompt?.set(
643
+ parts.reduce(
644
+ (agg, part) => {
645
+ if (part.type === "text") {
646
+ if (!part.synthetic) agg.input += part.text
647
+ }
648
+ if (part.type === "file") agg.parts.push(part)
649
+ return agg
650
+ },
651
+ { input: "", parts: [] as PromptInfo["parts"] },
652
+ ),
653
+ )
654
+ dialog.clear()
655
+ },
656
+ },
657
+ {
658
+ title: "Redo",
659
+ value: "session.redo",
660
+ category: "Session",
661
+ enabled: !!session()?.revert?.messageID,
662
+ slash: {
663
+ name: "redo",
664
+ },
665
+ run: () => {
666
+ dialog.clear()
667
+ const messageID = session()?.revert?.messageID
668
+ if (!messageID) return
669
+ const message = messages().find((x) => x.role === "user" && x.id > messageID)
670
+ if (!message) {
671
+ void sdk.client.session.unrevert({
672
+ sessionID: route.sessionID,
673
+ })
674
+ prompt?.set({ input: "", parts: [] })
675
+ return
676
+ }
677
+ void sdk.client.session.revert({
678
+ sessionID: route.sessionID,
679
+ messageID: message.id,
680
+ })
681
+ },
682
+ },
683
+ {
684
+ title: sidebarVisible() ? "Hide sidebar" : "Show sidebar",
685
+ value: "session.sidebar.toggle",
686
+ category: "Session",
687
+ run: () => {
688
+ batch(() => {
689
+ const isVisible = sidebarVisible()
690
+ setSidebar(() => (isVisible ? "hide" : "auto"))
691
+ setSidebarOpen(!isVisible)
692
+ })
693
+ dialog.clear()
694
+ },
695
+ },
696
+ {
697
+ title: conceal() ? "Disable code concealment" : "Enable code concealment",
698
+ value: "session.toggle.conceal",
699
+ category: "Session",
700
+ run: () => {
701
+ setConceal((prev) => !prev)
702
+ dialog.clear()
703
+ },
704
+ },
705
+ {
706
+ title: showTimestamps() ? "Hide timestamps" : "Show timestamps",
707
+ value: "session.toggle.timestamps",
708
+ category: "Session",
709
+ slash: {
710
+ name: "timestamps",
711
+ aliases: ["toggle-timestamps"],
712
+ },
713
+ run: () => {
714
+ setTimestamps((prev) => (prev === "show" ? "hide" : "show"))
715
+ dialog.clear()
716
+ },
717
+ },
718
+ {
719
+ title: (() => {
720
+ const next = nextThinkingMode(thinkingMode())
721
+ if (next === "hide") return "Collapse thinking"
722
+ return "Expand thinking"
723
+ })(),
724
+ value: "session.toggle.thinking",
725
+ category: "Session",
726
+ slash: {
727
+ name: "thinking",
728
+ aliases: ["toggle-thinking"],
729
+ },
730
+ run: () => {
731
+ thinking.set(nextThinkingMode(thinkingMode()))
732
+ dialog.clear()
733
+ },
734
+ },
735
+ {
736
+ title: showDetails() ? "Hide tool details" : "Show tool details",
737
+ value: "session.toggle.actions",
738
+ category: "Session",
739
+ run: () => {
740
+ setShowDetails((prev) => !prev)
741
+ dialog.clear()
742
+ },
743
+ },
744
+ {
745
+ title: "Toggle session scrollbar",
746
+ value: "session.toggle.scrollbar",
747
+ category: "Session",
748
+ run: () => {
749
+ setShowScrollbar((prev) => !prev)
750
+ dialog.clear()
751
+ },
752
+ },
753
+ {
754
+ title: showGenericToolOutput() ? "Hide generic tool output" : "Show generic tool output",
755
+ value: "session.toggle.generic_tool_output",
756
+ category: "Session",
757
+ run: () => {
758
+ setShowGenericToolOutput((prev) => !prev)
759
+ dialog.clear()
760
+ },
761
+ },
762
+ {
763
+ title: "Page up",
764
+ value: "session.page.up",
765
+ category: "Session",
766
+ hidden: true,
767
+ run: () => {
768
+ scroll.scrollBy(-scroll.height / 2)
769
+ dialog.clear()
770
+ },
771
+ },
772
+ {
773
+ title: "Page down",
774
+ value: "session.page.down",
775
+ category: "Session",
776
+ hidden: true,
777
+ run: () => {
778
+ scroll.scrollBy(scroll.height / 2)
779
+ dialog.clear()
780
+ },
781
+ },
782
+ {
783
+ title: "Line up",
784
+ value: "session.line.up",
785
+ category: "Session",
786
+ hidden: true,
787
+ run: () => {
788
+ scroll.scrollBy(-1)
789
+ dialog.clear()
790
+ },
791
+ },
792
+ {
793
+ title: "Line down",
794
+ value: "session.line.down",
795
+ category: "Session",
796
+ hidden: true,
797
+ run: () => {
798
+ scroll.scrollBy(1)
799
+ dialog.clear()
800
+ },
801
+ },
802
+ {
803
+ title: "Half page up",
804
+ value: "session.half.page.up",
805
+ category: "Session",
806
+ hidden: true,
807
+ run: () => {
808
+ scroll.scrollBy(-scroll.height / 4)
809
+ dialog.clear()
810
+ },
811
+ },
812
+ {
813
+ title: "Half page down",
814
+ value: "session.half.page.down",
815
+ category: "Session",
816
+ hidden: true,
817
+ run: () => {
818
+ scroll.scrollBy(scroll.height / 4)
819
+ dialog.clear()
820
+ },
821
+ },
822
+ {
823
+ title: "First message",
824
+ value: "session.first",
825
+ category: "Session",
826
+ hidden: true,
827
+ run: () => {
828
+ scroll.scrollTo(0)
829
+ dialog.clear()
830
+ },
831
+ },
832
+ {
833
+ title: "Last message",
834
+ value: "session.last",
835
+ category: "Session",
836
+ hidden: true,
837
+ run: () => {
838
+ scroll.scrollTo(scroll.scrollHeight)
839
+ dialog.clear()
840
+ },
841
+ },
842
+ {
843
+ title: "Jump to last user message",
844
+ value: "session.messages_last_user",
845
+ category: "Session",
846
+ hidden: true,
847
+ run: () => {
848
+ const messages = sync.data.message[route.sessionID]
849
+ if (!messages || !messages.length) return
850
+
851
+ // Find the most recent user message with non-ignored, non-synthetic text parts
852
+ for (let i = messages.length - 1; i >= 0; i--) {
853
+ const message = messages[i]
854
+ if (!message || message.role !== "user") continue
855
+
856
+ const parts = sync.data.part[message.id]
857
+ if (!parts || !Array.isArray(parts)) continue
858
+
859
+ const hasValidTextPart = parts.some(
860
+ (part) => part && part.type === "text" && !part.synthetic && !part.ignored,
861
+ )
862
+
863
+ if (hasValidTextPart) {
864
+ const child = scroll.getChildren().find((child) => {
865
+ return child.id === message.id
866
+ })
867
+ if (child) scroll.scrollBy(child.y - scroll.y - 1)
868
+ break
869
+ }
870
+ }
871
+ },
872
+ },
873
+ {
874
+ title: "Next message",
875
+ value: "session.message.next",
876
+ category: "Session",
877
+ hidden: true,
878
+ run: () => scrollToMessage("next", dialog),
879
+ },
880
+ {
881
+ title: "Previous message",
882
+ value: "session.message.previous",
883
+ category: "Session",
884
+ hidden: true,
885
+ run: () => scrollToMessage("prev", dialog),
886
+ },
887
+ {
888
+ title: "Copy last assistant message",
889
+ value: "messages.copy",
890
+ category: "Session",
891
+ run: () => {
892
+ const revertID = session()?.revert?.messageID
893
+ const lastAssistantMessage = messages().findLast(
894
+ (msg) => msg.role === "assistant" && (!revertID || msg.id < revertID),
895
+ )
896
+ if (!lastAssistantMessage) {
897
+ toast.show({ message: "No assistant messages found", variant: "error" })
898
+ dialog.clear()
899
+ return
900
+ }
901
+
902
+ const parts = sync.data.part[lastAssistantMessage.id] ?? []
903
+ const textParts = parts.filter((part) => part.type === "text")
904
+ if (textParts.length === 0) {
905
+ toast.show({ message: "No text parts found in last assistant message", variant: "error" })
906
+ dialog.clear()
907
+ return
908
+ }
909
+
910
+ const text = textParts
911
+ .map((part) => part.text)
912
+ .join("\n")
913
+ .trim()
914
+ if (!text) {
915
+ toast.show({
916
+ message: "No text content found in last assistant message",
917
+ variant: "error",
918
+ })
919
+ dialog.clear()
920
+ return
921
+ }
922
+
923
+ Clipboard.copy(text)
924
+ .then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" }))
925
+ .catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" }))
926
+ dialog.clear()
927
+ },
928
+ },
929
+ {
930
+ title: "Copy session transcript",
931
+ value: "session.copy",
932
+ category: "Session",
933
+ slash: {
934
+ name: "copy",
935
+ },
936
+ run: async () => {
937
+ try {
938
+ const sessionData = session()
939
+ if (!sessionData) return
940
+ const sessionMessages = messages()
941
+ const transcript = formatTranscript(
942
+ sessionData,
943
+ sessionMessages.map((msg) => ({ info: msg, parts: sync.data.part[msg.id] ?? [] })),
944
+ {
945
+ thinking: showThinking(),
946
+ toolDetails: showDetails(),
947
+ assistantMetadata: showAssistantMetadata(),
948
+ providers: sync.data.provider,
949
+ },
950
+ )
951
+ await Clipboard.copy(transcript)
952
+ toast.show({ message: "Session transcript copied to clipboard!", variant: "success" })
953
+ } catch {
954
+ toast.show({ message: "Failed to copy session transcript", variant: "error" })
955
+ }
956
+ dialog.clear()
957
+ },
958
+ },
959
+ {
960
+ title: "Export session transcript",
961
+ value: "session.export",
962
+ category: "Session",
963
+ slash: {
964
+ name: "export",
965
+ },
966
+ run: async () => {
967
+ try {
968
+ const sessionData = session()
969
+ if (!sessionData) return
970
+ const sessionMessages = messages()
971
+
972
+ const defaultFilename = `session-${sessionData.id.slice(0, 8)}.md`
973
+
974
+ const options = await DialogExportOptions.show(
975
+ dialog,
976
+ defaultFilename,
977
+ showThinking(),
978
+ showDetails(),
979
+ showAssistantMetadata(),
980
+ false,
981
+ )
982
+
983
+ if (options === null) return
984
+
985
+ const transcript = formatTranscript(
986
+ sessionData,
987
+ sessionMessages.map((msg) => ({ info: msg, parts: sync.data.part[msg.id] ?? [] })),
988
+ {
989
+ thinking: options.thinking,
990
+ toolDetails: options.toolDetails,
991
+ assistantMetadata: options.assistantMetadata,
992
+ providers: sync.data.provider,
993
+ },
994
+ )
995
+
996
+ if (options.openWithoutSaving) {
997
+ // Just open in editor without saving
998
+ await Editor.open({
999
+ value: transcript,
1000
+ renderer,
1001
+ cwd:
1002
+ (project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) ||
1003
+ project.instance.directory() ||
1004
+ process.cwd(),
1005
+ })
1006
+ } else {
1007
+ const exportDir = process.cwd()
1008
+ const filename = options.filename.trim()
1009
+ const filepath = path.join(exportDir, filename)
1010
+
1011
+ await Filesystem.write(filepath, transcript)
1012
+
1013
+ // Open with EDITOR if available
1014
+ const result = await Editor.open({
1015
+ value: transcript,
1016
+ renderer,
1017
+ cwd:
1018
+ (project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) ||
1019
+ project.instance.directory() ||
1020
+ process.cwd(),
1021
+ })
1022
+ if (result !== undefined) {
1023
+ await Filesystem.write(filepath, result)
1024
+ }
1025
+
1026
+ toast.show({ message: `Session exported to ${filename}`, variant: "success" })
1027
+ }
1028
+ } catch {
1029
+ toast.show({ message: "Failed to export session", variant: "error" })
1030
+ }
1031
+ dialog.clear()
1032
+ },
1033
+ },
1034
+ {
1035
+ title: "Background subagents",
1036
+ value: "session.background",
1037
+ category: "Session",
1038
+ hidden: true,
1039
+ enabled: foregroundTasks().length > 0,
1040
+ run: () => {
1041
+ void sdk.client.experimental.session.background({
1042
+ sessionID: route.sessionID,
1043
+ workspace: project.workspace.current(),
1044
+ })
1045
+ dialog.clear()
1046
+ },
1047
+ },
1048
+ {
1049
+ title: "Go to child session",
1050
+ value: "session.child.first",
1051
+ category: "Session",
1052
+ hidden: true,
1053
+ run: () => {
1054
+ dialog.clear()
1055
+ moveFirstChild()
1056
+ },
1057
+ },
1058
+ {
1059
+ title: "Go to parent session",
1060
+ value: "session.parent",
1061
+ category: "Session",
1062
+ hidden: true,
1063
+ enabled: !!session()?.parentID,
1064
+ run: childSessionHandler(() => {
1065
+ const parentID = session()?.parentID
1066
+ if (parentID) {
1067
+ navigate({
1068
+ type: "session",
1069
+ sessionID: parentID,
1070
+ })
1071
+ }
1072
+ dialog.clear()
1073
+ }),
1074
+ },
1075
+ {
1076
+ title: "Next child session",
1077
+ value: "session.child.next",
1078
+ category: "Session",
1079
+ hidden: true,
1080
+ enabled: !!session()?.parentID,
1081
+ run: childSessionHandler(() => {
1082
+ dialog.clear()
1083
+ moveChild(1)
1084
+ }),
1085
+ },
1086
+ {
1087
+ title: "Previous child session",
1088
+ value: "session.child.previous",
1089
+ category: "Session",
1090
+ hidden: true,
1091
+ enabled: !!session()?.parentID,
1092
+ run: childSessionHandler(() => {
1093
+ dialog.clear()
1094
+ moveChild(-1)
1095
+ }),
1096
+ },
1097
+ ])
1098
+
1099
+ const sessionCommands = createMemo(() =>
1100
+ sessionCommandList().map((command) => ({
1101
+ namespace: "palette",
1102
+ name: command.value,
1103
+ desc: "description" in command ? command.description : undefined,
1104
+ slashName: "slash" in command ? command.slash?.name : undefined,
1105
+ slashAliases: "slash" in command ? command.slash?.aliases : undefined,
1106
+ ...command,
1107
+ })),
1108
+ )
1109
+
1110
+ useBindings(() => ({
1111
+ commands: sessionCommands(),
1112
+ }))
1113
+
1114
+ useBindings(() => ({
1115
+ bindings: tuiConfig.keybinds.gather("session.global", sessionGlobalBindingCommands),
1116
+ }))
1117
+
1118
+ useBindings(() => ({
1119
+ enabled: () => renderer.currentFocusedEditor === null,
1120
+ bindings: tuiConfig.keybinds.gather("session.global.unfocused", sessionGlobalUnfocusedBindingCommands),
1121
+ }))
1122
+
1123
+ useBindings(() => ({
1124
+ mode: OPENCODE_BASE_MODE,
1125
+ bindings: tuiConfig.keybinds.gather("session", sessionBindingCommands),
1126
+ }))
1127
+
1128
+ useBindings(() => ({
1129
+ mode: OPENCODE_BASE_MODE,
1130
+ enabled: foregroundTasks().length > 0,
1131
+ priority: 1,
1132
+ bindings: tuiConfig.keybinds.get("session.background"),
1133
+ }))
1134
+
1135
+ const revertInfo = createMemo(() => session()?.revert)
1136
+ const revertMessageID = createMemo(() => revertInfo()?.messageID)
1137
+
1138
+ const revertDiffFiles = createMemo(() => getRevertDiffFiles(revertInfo()?.diff ?? ""))
1139
+
1140
+ const revertRevertedMessages = createMemo(() => {
1141
+ const messageID = revertMessageID()
1142
+ if (!messageID) return []
1143
+ return messages().filter((x) => x.id >= messageID && x.role === "user")
1144
+ })
1145
+
1146
+ const revert = createMemo(() => {
1147
+ const info = revertInfo()
1148
+ if (!info) return
1149
+ if (!info.messageID) return
1150
+ return {
1151
+ messageID: info.messageID,
1152
+ reverted: revertRevertedMessages(),
1153
+ diff: info.diff,
1154
+ diffFiles: revertDiffFiles(),
1155
+ }
1156
+ })
1157
+
1158
+ // snap to bottom when session changes
1159
+ createEffect(on(() => route.sessionID, toBottom))
1160
+
1161
+ createEffect(() => {
1162
+ const dir = session()?.directory
1163
+ if (dir) process.env.ORYNA_AGENT_SESSION_ID = route.sessionID || ""
1164
+ })
1165
+
1166
+ // register message handler once
1167
+ setMessageHandler(async (content, from) => {
1168
+ let sessionID = process.env.ORYNA_AGENT_SESSION_ID
1169
+ if (!sessionID) {
1170
+ const created = await sdk.client.session.create({})
1171
+ sessionID = created.data!.id
1172
+ process.env.ORYNA_AGENT_SESSION_ID = sessionID
1173
+ navigate({ type: "session", sessionID })
1174
+ }
1175
+ const model = local.model.current()
1176
+ await sdk.client.session.prompt({
1177
+ sessionID,
1178
+ parts: [{
1179
+ type: "text",
1180
+ text: `[Collaboration from ${from}]\n${content}\n\nComplete the task above. When finished, call the "reply" tool to report results.`,
1181
+ }],
1182
+ ...(model ? { model: { providerID: model.providerID, modelID: model.modelID } } : {}),
1183
+ })
1184
+ })
1185
+
1186
+ // connect/disconnect WS based on selected model
1187
+ let lastWasOrynaLocal = false
1188
+ createEffect(() => {
1189
+ const model = local.model.current()
1190
+ const isOrynaLocal = model?.providerID === "oryna-local"
1191
+ if (isOrynaLocal && !lastWasOrynaLocal) {
1192
+ startAgent()
1193
+ } else if (!isOrynaLocal && lastWasOrynaLocal) {
1194
+ stopAgent()
1195
+ }
1196
+ lastWasOrynaLocal = isOrynaLocal
1197
+ })
1198
+
1199
+ return (
1200
+ <PathFormatterProvider path={session()?.directory}>
1201
+ <context.Provider
1202
+ value={{
1203
+ get width() {
1204
+ return contentWidth()
1205
+ },
1206
+ sessionID: route.sessionID,
1207
+ conceal,
1208
+ thinkingMode,
1209
+ showThinking,
1210
+ showTimestamps,
1211
+ showDetails,
1212
+ showGenericToolOutput,
1213
+ userMessageIDs,
1214
+ diffWrapMode,
1215
+ providers,
1216
+ sync,
1217
+ tui: tuiConfig,
1218
+ }}
1219
+ >
1220
+ <box flexDirection="row" flexGrow={1} minHeight={0}>
1221
+ <box flexGrow={1} minHeight={0} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1}>
1222
+ <Show when={session()}>
1223
+ <scrollbox
1224
+ ref={(r) => (scroll = r)}
1225
+ viewportOptions={{
1226
+ paddingRight: showScrollbar() ? 1 : 0,
1227
+ }}
1228
+ verticalScrollbarOptions={{
1229
+ paddingLeft: 1,
1230
+ visible: showScrollbar(),
1231
+ trackOptions: {
1232
+ backgroundColor: theme.backgroundElement,
1233
+ foregroundColor: theme.border,
1234
+ },
1235
+ }}
1236
+ stickyScroll={true}
1237
+ stickyStart="bottom"
1238
+ flexGrow={1}
1239
+ scrollAcceleration={scrollAcceleration()}
1240
+ >
1241
+ <box height={1} />
1242
+ <For each={messages()}>
1243
+ {(message, index) => (
1244
+ <Switch>
1245
+ <Match when={message.id === revert()?.messageID}>
1246
+ {(function () {
1247
+ const redoShortcut = useCommandShortcut("session.redo")
1248
+ const [hover, setHover] = createSignal(false)
1249
+ const dialog = useDialog()
1250
+
1251
+ const handleUnrevert = async () => {
1252
+ const confirmed = await DialogConfirm.show(
1253
+ dialog,
1254
+ "Confirm Redo",
1255
+ "Are you sure you want to restore the reverted messages?",
1256
+ )
1257
+ if (confirmed) {
1258
+ keymap.dispatchCommand("session.redo")
1259
+ }
1260
+ }
1261
+
1262
+ return (
1263
+ <box
1264
+ onMouseOver={() => setHover(true)}
1265
+ onMouseOut={() => setHover(false)}
1266
+ onMouseUp={handleUnrevert}
1267
+ marginTop={1}
1268
+ flexShrink={0}
1269
+ border={["left"]}
1270
+ customBorderChars={SplitBorder.customBorderChars}
1271
+ borderColor={theme.backgroundPanel}
1272
+ >
1273
+ <box
1274
+ paddingTop={1}
1275
+ paddingBottom={1}
1276
+ paddingLeft={2}
1277
+ backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
1278
+ >
1279
+ <text fg={theme.textMuted}>{revert()!.reverted.length} message reverted</text>
1280
+ <text fg={theme.textMuted}>
1281
+ <span style={{ fg: theme.text }}>{redoShortcut()}</span> or /redo to restore
1282
+ </text>
1283
+ <Show when={revert()!.diffFiles?.length}>
1284
+ <box marginTop={1}>
1285
+ <For each={revert()!.diffFiles}>
1286
+ {(file) => (
1287
+ <text fg={theme.text}>
1288
+ {file.filename}
1289
+ <Show when={file.additions > 0}>
1290
+ <span style={{ fg: theme.diffAdded }}> +{file.additions}</span>
1291
+ </Show>
1292
+ <Show when={file.deletions > 0}>
1293
+ <span style={{ fg: theme.diffRemoved }}> -{file.deletions}</span>
1294
+ </Show>
1295
+ </text>
1296
+ )}
1297
+ </For>
1298
+ </box>
1299
+ </Show>
1300
+ </box>
1301
+ </box>
1302
+ )
1303
+ })()}
1304
+ </Match>
1305
+ <Match when={revert()?.messageID && message.id >= revert()!.messageID}>
1306
+ <></>
1307
+ </Match>
1308
+ <Match when={message.role === "user"}>
1309
+ <UserMessage
1310
+ index={index()}
1311
+ onMouseUp={() => {
1312
+ if (renderer.getSelection()?.getSelectedText()) return
1313
+ dialog.replace(() => (
1314
+ <DialogMessage
1315
+ messageID={message.id}
1316
+ sessionID={route.sessionID}
1317
+ setPrompt={(promptInfo) => prompt?.set(promptInfo)}
1318
+ />
1319
+ ))
1320
+ }}
1321
+ message={message as UserMessage}
1322
+ parts={sync.data.part[message.id] ?? []}
1323
+ pending={pending()}
1324
+ />
1325
+ </Match>
1326
+ <Match when={message.role === "assistant"}>
1327
+ <AssistantMessage
1328
+ last={lastAssistant()?.id === message.id}
1329
+ message={message as AssistantMessage}
1330
+ parts={sync.data.part[message.id] ?? []}
1331
+ />
1332
+ </Match>
1333
+ </Switch>
1334
+ )}
1335
+ </For>
1336
+ </scrollbox>
1337
+ <box flexShrink={0}>
1338
+ <Show when={permissions().length > 0}>
1339
+ <PermissionPrompt
1340
+ request={permissions()[0]}
1341
+ directory={sync.session.get(permissions()[0].sessionID)?.directory}
1342
+ />
1343
+ </Show>
1344
+ <Show when={permissions().length === 0 && questions().length > 0}>
1345
+ <QuestionPrompt
1346
+ request={questions()[0]}
1347
+ directory={sync.session.get(questions()[0].sessionID)?.directory}
1348
+ />
1349
+ </Show>
1350
+ <Show when={session()?.parentID}>
1351
+ <SubagentFooter />
1352
+ </Show>
1353
+ <Show when={visible()}>
1354
+ <TuiPluginRuntime.Slot
1355
+ name="session_prompt"
1356
+ mode="replace"
1357
+ session_id={route.sessionID}
1358
+ visible={visible()}
1359
+ disabled={disabled()}
1360
+ on_submit={toBottom}
1361
+ ref={bind}
1362
+ >
1363
+ <Prompt
1364
+ visible={visible()}
1365
+ ref={bind}
1366
+ disabled={disabled()}
1367
+ onSubmit={() => {
1368
+ toBottom()
1369
+ }}
1370
+ sessionID={route.sessionID}
1371
+ right={
1372
+ <>
1373
+ <Show when={agentStatus().connected}>
1374
+ <text
1375
+ fg={agentStatus().processing ? theme.warning : theme.success}
1376
+ >
1377
+ {agentStatus().processing ? "◇" : "●"}{" "}
1378
+ </text>
1379
+ <text fg={theme.textMuted}>
1380
+ {agentStatus().processing
1381
+ ? "processing..."
1382
+ : `Collab · ${agentStatus().url}`}
1383
+ </text>
1384
+ </Show>
1385
+ <TuiPluginRuntime.Slot
1386
+ name="session_prompt_right"
1387
+ session_id={route.sessionID}
1388
+ />
1389
+ </>
1390
+ }
1391
+ />
1392
+ </TuiPluginRuntime.Slot>
1393
+ </Show>
1394
+ </box>
1395
+ </Show>
1396
+ <Toast />
1397
+ </box>
1398
+ <Show when={sidebarVisible()}>
1399
+ <Switch>
1400
+ <Match when={wide()}>
1401
+ <Sidebar sessionID={route.sessionID} />
1402
+ </Match>
1403
+ <Match when={!wide()}>
1404
+ <box
1405
+ position="absolute"
1406
+ top={0}
1407
+ left={0}
1408
+ right={0}
1409
+ bottom={0}
1410
+ alignItems="flex-end"
1411
+ backgroundColor={RGBA.fromInts(0, 0, 0, 70)}
1412
+ >
1413
+ <Sidebar sessionID={route.sessionID} />
1414
+ </box>
1415
+ </Match>
1416
+ </Switch>
1417
+ </Show>
1418
+ </box>
1419
+ </context.Provider>
1420
+ </PathFormatterProvider>
1421
+ )
1422
+ }
1423
+
1424
+ const MIME_BADGE: Record<string, string> = {
1425
+ "text/plain": "txt",
1426
+ "image/png": "img",
1427
+ "image/jpeg": "img",
1428
+ "image/gif": "img",
1429
+ "image/webp": "img",
1430
+ "application/pdf": "pdf",
1431
+ "application/x-directory": "dir",
1432
+ }
1433
+
1434
+ function UserMessage(props: {
1435
+ message: UserMessage
1436
+ parts: Part[]
1437
+ onMouseUp: () => void
1438
+ index: number
1439
+ pending?: string
1440
+ }) {
1441
+ const ctx = use()
1442
+ const local = useLocal()
1443
+ const text = createMemo(() => {
1444
+ const texts = props.parts
1445
+ .map((x) => {
1446
+ if (x.type === "text" && !x.synthetic) {
1447
+ return x.text
1448
+ }
1449
+ return null
1450
+ })
1451
+ .filter(Boolean)
1452
+ return texts.join("\n\n")
1453
+ })
1454
+ const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
1455
+ const { theme } = useTheme()
1456
+ const [hover, setHover] = createSignal(false)
1457
+ const queued = createMemo(() => props.pending && props.message.id > props.pending)
1458
+ const color = createMemo(() => local.agent.color(props.message.agent))
1459
+ const queuedFg = createMemo(() => selectedForeground(theme, color()))
1460
+ const metadataVisible = createMemo(() => queued() || ctx.showTimestamps())
1461
+
1462
+ const compaction = createMemo(() => props.parts.find((x) => x.type === "compaction"))
1463
+
1464
+ return (
1465
+ <>
1466
+ <Show when={text()}>
1467
+ <box
1468
+ id={props.message.id}
1469
+ border={["left"]}
1470
+ borderColor={color()}
1471
+ customBorderChars={SplitBorder.customBorderChars}
1472
+ marginTop={props.index === 0 ? 0 : 1}
1473
+ >
1474
+ <box
1475
+ onMouseOver={() => {
1476
+ setHover(true)
1477
+ }}
1478
+ onMouseOut={() => {
1479
+ setHover(false)
1480
+ }}
1481
+ onMouseUp={props.onMouseUp}
1482
+ paddingTop={1}
1483
+ paddingBottom={1}
1484
+ paddingLeft={2}
1485
+ backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
1486
+ flexShrink={0}
1487
+ >
1488
+ <text fg={theme.text}>{text()}</text>
1489
+ <Show when={files().length}>
1490
+ <box flexDirection="row" paddingBottom={metadataVisible() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
1491
+ <For each={files()}>
1492
+ {(file) => {
1493
+ const bg = createMemo(() => {
1494
+ if (file.mime.startsWith("image/")) return theme.accent
1495
+ if (file.mime === "application/pdf") return theme.primary
1496
+ return theme.secondary
1497
+ })
1498
+ return (
1499
+ <text fg={theme.text}>
1500
+ <span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
1501
+ <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
1502
+ </text>
1503
+ )
1504
+ }}
1505
+ </For>
1506
+ </box>
1507
+ </Show>
1508
+ <Show
1509
+ when={queued()}
1510
+ fallback={
1511
+ <Show when={ctx.showTimestamps()}>
1512
+ <text fg={theme.textMuted}>
1513
+ <span style={{ fg: theme.textMuted }}>
1514
+ {Locale.todayTimeOrDateTime(props.message.time.created)}
1515
+ </span>
1516
+ </text>
1517
+ </Show>
1518
+ }
1519
+ >
1520
+ <text fg={theme.textMuted}>
1521
+ <span style={{ bg: color(), fg: queuedFg(), bold: true }}> QUEUED </span>
1522
+ </text>
1523
+ </Show>
1524
+ </box>
1525
+ </box>
1526
+ </Show>
1527
+ <Show when={compaction()}>
1528
+ <box
1529
+ marginTop={1}
1530
+ border={["top"]}
1531
+ title=" Compaction "
1532
+ titleAlignment="center"
1533
+ borderColor={theme.borderActive}
1534
+ />
1535
+ </Show>
1536
+ </>
1537
+ )
1538
+ }
1539
+
1540
+ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean }) {
1541
+ const ctx = use()
1542
+ const local = useLocal()
1543
+ const { theme } = useTheme()
1544
+ const sync = useSync()
1545
+ const messages = createMemo(() => sync.data.message[props.message.sessionID] ?? [])
1546
+ const model = createMemo(() => Model.name(ctx.providers(), props.message.providerID, props.message.modelID))
1547
+
1548
+ const final = createMemo(() => {
1549
+ return props.message.finish && !["tool-calls", "unknown"].includes(props.message.finish)
1550
+ })
1551
+
1552
+ const duration = createMemo(() => {
1553
+ if (!final()) return 0
1554
+ if (!props.message.time.completed) return 0
1555
+ const user = messages().find((x) => x.role === "user" && x.id === props.message.parentID)
1556
+ if (!user || !user.time) return 0
1557
+ return props.message.time.completed - user.time.created
1558
+ })
1559
+
1560
+ const childShortcut = useCommandShortcut("session.child.first")
1561
+ const backgroundShortcut = useCommandShortcut("session.background")
1562
+
1563
+ return (
1564
+ <>
1565
+ <For each={props.parts}>
1566
+ {(part, index) => {
1567
+ const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
1568
+ return (
1569
+ <Show when={component()}>
1570
+ <Dynamic
1571
+ last={index() === props.parts.length - 1}
1572
+ component={component()}
1573
+ part={part as any}
1574
+ message={props.message}
1575
+ />
1576
+ </Show>
1577
+ )
1578
+ }}
1579
+ </For>
1580
+ <Show when={props.parts.some((x) => x.type === "tool" && x.tool === "task")}>
1581
+ <box paddingTop={1} paddingLeft={3}>
1582
+ <text fg={theme.text}>
1583
+ {childShortcut()}
1584
+ <span style={{ fg: theme.textMuted }}> view subagents</span>
1585
+ <Show
1586
+ when={props.parts.some(
1587
+ (x) =>
1588
+ x.type === "tool" &&
1589
+ x.tool === "task" &&
1590
+ x.state.status === "running" &&
1591
+ x.state.metadata?.background !== true,
1592
+ )}
1593
+ >
1594
+ <span style={{ fg: theme.textMuted }}> · </span>
1595
+ {backgroundShortcut()}
1596
+ <span style={{ fg: theme.textMuted }}> background</span>
1597
+ </Show>
1598
+ </text>
1599
+ </box>
1600
+ </Show>
1601
+ <Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
1602
+ <box
1603
+ border={["left"]}
1604
+ paddingTop={1}
1605
+ paddingBottom={1}
1606
+ paddingLeft={2}
1607
+ marginTop={1}
1608
+ backgroundColor={theme.backgroundPanel}
1609
+ customBorderChars={SplitBorder.customBorderChars}
1610
+ borderColor={theme.error}
1611
+ >
1612
+ <text fg={theme.textMuted}>{props.message.error?.data.message}</text>
1613
+ </box>
1614
+ </Show>
1615
+ <Switch>
1616
+ <Match when={props.last || final() || props.message.error?.name === "MessageAbortedError"}>
1617
+ <box paddingLeft={3}>
1618
+ <text marginTop={1}>
1619
+ <span
1620
+ style={{
1621
+ fg:
1622
+ props.message.error?.name === "MessageAbortedError"
1623
+ ? theme.textMuted
1624
+ : local.agent.color(props.message.agent),
1625
+ }}
1626
+ >
1627
+ ▣{" "}
1628
+ </span>{" "}
1629
+ <span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>
1630
+ <span style={{ fg: theme.textMuted }}> · {model()}</span>
1631
+ <Show when={duration()}>
1632
+ <span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
1633
+ </Show>
1634
+ <Show when={props.message.error?.name === "MessageAbortedError"}>
1635
+ <span style={{ fg: theme.textMuted }}> · interrupted</span>
1636
+ </Show>
1637
+ </text>
1638
+ </box>
1639
+ </Match>
1640
+ </Switch>
1641
+ </>
1642
+ )
1643
+ }
1644
+
1645
+ const PART_MAPPING = {
1646
+ text: TextPart,
1647
+ tool: ToolPart,
1648
+ reasoning: ReasoningPart,
1649
+ }
1650
+
1651
+ const INLINE_TOOL_ICON_WIDTH = 2
1652
+
1653
+ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) {
1654
+ const { theme } = useTheme()
1655
+ const ctx = use()
1656
+ // Collapsed by default in hide mode: a single line throughout, so the
1657
+ // layout never shifts. Click to open the full markdown block, click to close.
1658
+ const [expanded, setExpanded] = createSignal(false)
1659
+
1660
+ const content = createMemo(() => {
1661
+ // OpenRouter encrypts some reasoning blocks; drop the placeholder.
1662
+ return props.part.text.replace("[REDACTED]", "").trim()
1663
+ })
1664
+ // Reasoning is finalized when the server sets `time.end` (see processor.ts).
1665
+ // Flips independently of the parent message completing.
1666
+ const isDone = createMemo(() => props.part.time.end !== undefined)
1667
+ const inMinimal = createMemo(() => ctx.thinkingMode() === "hide")
1668
+ const duration = createMemo(() => {
1669
+ const end = props.part.time.end
1670
+ return end === undefined ? 0 : Math.max(0, end - props.part.time.start)
1671
+ })
1672
+ const summary = createMemo(() => reasoningSummary(content()))
1673
+ const syntax = createSyntaxStyleMemo(() => generateSubtleSyntax(theme))
1674
+
1675
+ const toggle = () => {
1676
+ if (!inMinimal()) return
1677
+ setExpanded((prev) => !prev)
1678
+ }
1679
+
1680
+ return (
1681
+ <Show when={content()}>
1682
+ <box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0}>
1683
+ <box onMouseUp={toggle}>
1684
+ <ReasoningHeader
1685
+ toggleable={inMinimal()}
1686
+ open={!inMinimal() || expanded()}
1687
+ done={isDone()}
1688
+ title={summary().title}
1689
+ duration={isDone() ? Locale.duration(duration()) : undefined}
1690
+ />
1691
+ </box>
1692
+ <Show when={(!inMinimal() || expanded()) && summary().body}>
1693
+ <box paddingLeft={inMinimal() ? 2 : 0} marginTop={1}>
1694
+ <code
1695
+ filetype="markdown"
1696
+ drawUnstyledText={false}
1697
+ streaming={true}
1698
+ syntaxStyle={syntax()}
1699
+ content={summary().body}
1700
+ conceal={ctx.conceal()}
1701
+ fg={theme.textMuted}
1702
+ />
1703
+ </box>
1704
+ </Show>
1705
+ </box>
1706
+ </Show>
1707
+ )
1708
+ }
1709
+
1710
+ function ReasoningHeader(props: {
1711
+ toggleable: boolean
1712
+ open: boolean
1713
+ done: boolean
1714
+ title: string | null
1715
+ duration?: string
1716
+ }) {
1717
+ const { theme } = useTheme()
1718
+ const fg = () =>
1719
+ props.open
1720
+ ? RGBA.fromValues(theme.warning.r, theme.warning.g, theme.warning.b, theme.thinkingOpacity)
1721
+ : theme.warning
1722
+
1723
+ return (
1724
+ <Switch>
1725
+ <Match when={!props.done}>
1726
+ <box flexDirection="row">
1727
+ <Spinner color={fg()}>{props.title ? "Thinking: " + props.title : "Thinking"}</Spinner>
1728
+ </box>
1729
+ </Match>
1730
+ <Match when={true}>
1731
+ <text fg={fg()} wrapMode="none">
1732
+ <Show when={props.toggleable}>
1733
+ <span>{props.open ? "- " : "+ "}</span>
1734
+ </Show>
1735
+ <span>Thought</span>
1736
+ <Show when={props.title || props.duration}>
1737
+ <span>: </span>
1738
+ </Show>
1739
+ <Show when={props.title}>
1740
+ <span>{props.title}</span>
1741
+ </Show>
1742
+ <Show when={props.duration}>
1743
+ <span>
1744
+ {props.title ? " · " : ""}
1745
+ {props.duration}
1746
+ </span>
1747
+ </Show>
1748
+ </text>
1749
+ </Match>
1750
+ </Switch>
1751
+ )
1752
+ }
1753
+
1754
+ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) {
1755
+ const ctx = use()
1756
+ const { theme, syntax } = useTheme()
1757
+ return (
1758
+ <Show when={props.part.text.trim()}>
1759
+ <box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
1760
+ <markdown
1761
+ syntaxStyle={syntax()}
1762
+ streaming={true}
1763
+ internalBlockMode="top-level"
1764
+ content={props.part.text.trim()}
1765
+ tableOptions={{ style: "grid" }}
1766
+ conceal={ctx.conceal()}
1767
+ fg={theme.markdownText}
1768
+ bg={theme.background}
1769
+ />
1770
+ </box>
1771
+ </Show>
1772
+ )
1773
+ }
1774
+
1775
+ // Pending messages moved to individual tool pending functions
1776
+
1777
+ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMessage }) {
1778
+ const ctx = use()
1779
+ const sync = useSync()
1780
+
1781
+ // Hide tool if showDetails is false and tool completed successfully
1782
+ const shouldHide = createMemo(() => {
1783
+ if (ctx.showDetails()) return false
1784
+ if (props.part.state.status !== "completed") return false
1785
+ return true
1786
+ })
1787
+
1788
+ const toolprops = {
1789
+ get metadata() {
1790
+ return props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
1791
+ },
1792
+ get input() {
1793
+ return props.part.state.input ?? {}
1794
+ },
1795
+ get output() {
1796
+ return props.part.state.status === "completed" ? props.part.state.output : undefined
1797
+ },
1798
+ get permission() {
1799
+ const permissions = sync.data.permission[props.message.sessionID] ?? []
1800
+ const permissionIndex = permissions.findIndex((x) => x.tool?.callID === props.part.callID)
1801
+ return permissions[permissionIndex]
1802
+ },
1803
+ get tool() {
1804
+ return props.part.tool
1805
+ },
1806
+ get part() {
1807
+ return props.part
1808
+ },
1809
+ }
1810
+
1811
+ return (
1812
+ <Show when={!shouldHide()}>
1813
+ <Switch>
1814
+ <Match when={props.part.tool === ShellID.ToolID}>
1815
+ <Shell {...toolprops} />
1816
+ </Match>
1817
+ <Match when={props.part.tool === "glob"}>
1818
+ <Glob {...toolprops} />
1819
+ </Match>
1820
+ <Match when={props.part.tool === "read"}>
1821
+ <Read {...toolprops} />
1822
+ </Match>
1823
+ <Match when={props.part.tool === "grep"}>
1824
+ <Grep {...toolprops} />
1825
+ </Match>
1826
+ <Match when={props.part.tool === "webfetch"}>
1827
+ <WebFetch {...toolprops} />
1828
+ </Match>
1829
+ <Match when={props.part.tool === "websearch"}>
1830
+ <WebSearch {...toolprops} />
1831
+ </Match>
1832
+ <Match when={props.part.tool === "write"}>
1833
+ <Write {...toolprops} />
1834
+ </Match>
1835
+ <Match when={props.part.tool === "edit"}>
1836
+ <Edit {...toolprops} />
1837
+ </Match>
1838
+ <Match when={props.part.tool === "task"}>
1839
+ <Task {...toolprops} />
1840
+ </Match>
1841
+ <Match when={props.part.tool === "apply_patch"}>
1842
+ <ApplyPatch {...toolprops} />
1843
+ </Match>
1844
+ <Match when={props.part.tool === "todowrite"}>
1845
+ <TodoWrite {...toolprops} />
1846
+ </Match>
1847
+ <Match when={props.part.tool === "question"}>
1848
+ <Question {...toolprops} />
1849
+ </Match>
1850
+ <Match when={props.part.tool === "skill"}>
1851
+ <Skill {...toolprops} />
1852
+ </Match>
1853
+ <Match when={true}>
1854
+ <GenericTool {...toolprops} />
1855
+ </Match>
1856
+ </Switch>
1857
+ </Show>
1858
+ )
1859
+ }
1860
+
1861
+ type ToolProps<T> = {
1862
+ input: Partial<Tool.InferParameters<T>>
1863
+ metadata: Partial<Tool.InferMetadata<T>>
1864
+ permission: Record<string, any>
1865
+ tool: string
1866
+ output?: string
1867
+ part: ToolPart
1868
+ }
1869
+ function GenericTool(props: ToolProps<any>) {
1870
+ const { theme } = useTheme()
1871
+ const ctx = use()
1872
+ const output = createMemo(() => props.output?.trim() ?? "")
1873
+ const [expanded, setExpanded] = createSignal(false)
1874
+ const maxLines = 3
1875
+ const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6))
1876
+ const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars()))
1877
+ const limited = createMemo(() => {
1878
+ if (expanded() || !collapsed().overflow) return output()
1879
+ return collapsed().output
1880
+ })
1881
+
1882
+ return (
1883
+ <Show
1884
+ when={props.output && ctx.showGenericToolOutput()}
1885
+ fallback={
1886
+ <InlineTool icon="⚙" pending="Writing command..." complete={true} part={props.part}>
1887
+ {props.tool} {input(props.input)}
1888
+ </InlineTool>
1889
+ }
1890
+ >
1891
+ <BlockTool
1892
+ title={`# ${props.tool} ${input(props.input)}`}
1893
+ part={props.part}
1894
+ onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}
1895
+ >
1896
+ <box gap={1}>
1897
+ <text fg={theme.text}>{limited()}</text>
1898
+ <Show when={collapsed().overflow}>
1899
+ <text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
1900
+ </Show>
1901
+ </box>
1902
+ </BlockTool>
1903
+ </Show>
1904
+ )
1905
+ }
1906
+
1907
+ function InlineTool(props: {
1908
+ icon: string
1909
+ iconColor?: RGBA
1910
+ color?: RGBA
1911
+ complete: any
1912
+ pending: string
1913
+ spinner?: boolean
1914
+ subagent?: boolean
1915
+ children: JSX.Element
1916
+ part: ToolPart
1917
+ onClick?: () => void
1918
+ }) {
1919
+ const { theme } = useTheme()
1920
+ const ctx = use()
1921
+ const sync = useSync()
1922
+ const renderer = useRenderer()
1923
+ const [hover, setHover] = createSignal(false)
1924
+ const [errorExpanded, setErrorExpanded] = createSignal(false)
1925
+
1926
+ const permission = createMemo(() => {
1927
+ const callID = sync.data.permission[ctx.sessionID]?.at(0)?.tool?.callID
1928
+ if (!callID) return false
1929
+ return callID === props.part.callID
1930
+ })
1931
+
1932
+ const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error : undefined))
1933
+
1934
+ const denied = createMemo(
1935
+ () =>
1936
+ error()?.includes("QuestionRejectedError") ||
1937
+ error()?.includes("rejected permission") ||
1938
+ error()?.includes("specified a rule") ||
1939
+ error()?.includes("user dismissed"),
1940
+ )
1941
+
1942
+ const failed = createMemo(() => Boolean(error() && !denied()))
1943
+ const clickable = createMemo(() => Boolean(props.onClick || failed()))
1944
+ const fg = createMemo(() => {
1945
+ if (props.color) return props.color
1946
+ if (permission()) return theme.warning
1947
+ if (failed()) return theme.error
1948
+ if (hover() && props.onClick) return theme.text
1949
+ if (props.complete) return theme.textMuted
1950
+ return theme.text
1951
+ })
1952
+
1953
+ return (
1954
+ <InlineToolRow
1955
+ id={`tool-inline-${props.subagent ? "subagent-" : ""}${props.part.id}`}
1956
+ icon={props.icon}
1957
+ iconColor={props.iconColor}
1958
+ color={fg()}
1959
+ errorColor={theme.error}
1960
+ failed={failed()}
1961
+ denied={Boolean(denied())}
1962
+ error={error()}
1963
+ errorExpanded={errorExpanded()}
1964
+ complete={props.complete}
1965
+ pending={props.pending}
1966
+ spinner={props.spinner}
1967
+ subagent={props.subagent}
1968
+ separateAfter={(id) => id !== undefined && ctx.userMessageIDs().has(id)}
1969
+ onMouseOver={() => clickable() && setHover(true)}
1970
+ onMouseOut={() => setHover(false)}
1971
+ onMouseUp={() => {
1972
+ if (renderer.getSelection()?.getSelectedText()) return
1973
+ if (failed()) {
1974
+ setErrorExpanded((value) => !value)
1975
+ return
1976
+ }
1977
+ props.onClick?.()
1978
+ }}
1979
+ >
1980
+ {props.children}
1981
+ </InlineToolRow>
1982
+ )
1983
+ }
1984
+
1985
+ export function InlineToolRow(props: {
1986
+ id?: string
1987
+ icon: string
1988
+ iconColor?: RGBA
1989
+ color?: RGBA
1990
+ errorColor?: RGBA
1991
+ failed?: boolean
1992
+ denied?: boolean
1993
+ error?: string
1994
+ errorExpanded?: boolean
1995
+ complete: any
1996
+ pending: string
1997
+ spinner?: boolean
1998
+ subagent?: boolean
1999
+ children: JSX.Element
2000
+ separateAfter?: (id: string | undefined) => boolean
2001
+ onMouseOver?: () => void
2002
+ onMouseOut?: () => void
2003
+ onMouseUp?: () => void
2004
+ }) {
2005
+ return (
2006
+ <box
2007
+ id={props.id}
2008
+ paddingLeft={3}
2009
+ onMouseOver={props.onMouseOver}
2010
+ onMouseOut={props.onMouseOut}
2011
+ onMouseUp={props.onMouseUp}
2012
+ ref={(el: BoxRenderable) => {
2013
+ setPreLayoutSiblingMargin(el, (previous) => {
2014
+ const previousInline = previous?.id.startsWith("tool-inline-") ?? false
2015
+ const previousSubagent = previous?.id.startsWith("tool-inline-subagent-") ?? false
2016
+ return previous?.id.startsWith("text-") ||
2017
+ previous?.id.startsWith("tool-block-") ||
2018
+ (previousInline && previousSubagent !== Boolean(props.subagent)) ||
2019
+ props.separateAfter?.(previous?.id)
2020
+ ? 1
2021
+ : 0
2022
+ })
2023
+ }}
2024
+ >
2025
+ <Switch>
2026
+ <Match when={props.spinner}>
2027
+ <Spinner color={props.color} children={props.children} />
2028
+ </Match>
2029
+ <Match when={true}>
2030
+ <Show
2031
+ fallback={
2032
+ <text
2033
+ paddingLeft={3}
2034
+ fg={props.color}
2035
+ attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
2036
+ >
2037
+ ~ {props.pending}
2038
+ </text>
2039
+ }
2040
+ when={props.complete}
2041
+ >
2042
+ <box flexDirection="row">
2043
+ <text
2044
+ width={INLINE_TOOL_ICON_WIDTH}
2045
+ fg={props.failed ? props.errorColor : (props.iconColor ?? props.color)}
2046
+ attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
2047
+ >
2048
+ {props.icon}
2049
+ </text>
2050
+ <text
2051
+ flexGrow={1}
2052
+ fg={props.failed ? props.errorColor : props.color}
2053
+ attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
2054
+ >
2055
+ {props.children}
2056
+ </text>
2057
+ </box>
2058
+ </Show>
2059
+ </Match>
2060
+ </Switch>
2061
+ <Show when={props.failed && props.errorExpanded}>
2062
+ <box paddingLeft={INLINE_TOOL_ICON_WIDTH}>
2063
+ <text fg={props.errorColor}>{props.error}</text>
2064
+ </box>
2065
+ </Show>
2066
+ </box>
2067
+ )
2068
+ }
2069
+
2070
+ function BlockTool(props: {
2071
+ title: string
2072
+ children: JSX.Element
2073
+ onClick?: () => void
2074
+ part?: ToolPart
2075
+ spinner?: boolean
2076
+ }) {
2077
+ const { theme } = useTheme()
2078
+ const renderer = useRenderer()
2079
+ const [hover, setHover] = createSignal(false)
2080
+ const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
2081
+ return (
2082
+ <box
2083
+ id={props.part ? "tool-block-" + props.part.id : undefined}
2084
+ border={["left"]}
2085
+ paddingTop={1}
2086
+ paddingBottom={1}
2087
+ paddingLeft={2}
2088
+ marginTop={1}
2089
+ gap={1}
2090
+ backgroundColor={hover() ? theme.backgroundMenu : theme.backgroundPanel}
2091
+ customBorderChars={SplitBorder.customBorderChars}
2092
+ borderColor={theme.background}
2093
+ onMouseOver={() => props.onClick && setHover(true)}
2094
+ onMouseOut={() => setHover(false)}
2095
+ onMouseUp={() => {
2096
+ if (renderer.getSelection()?.getSelectedText()) return
2097
+ props.onClick?.()
2098
+ }}
2099
+ >
2100
+ <Show
2101
+ when={props.spinner}
2102
+ fallback={
2103
+ <text paddingLeft={3} fg={theme.textMuted}>
2104
+ {props.title}
2105
+ </text>
2106
+ }
2107
+ >
2108
+ <Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
2109
+ </Show>
2110
+ {props.children}
2111
+ <Show when={error()}>
2112
+ <text fg={theme.error}>{error()}</text>
2113
+ </Show>
2114
+ </box>
2115
+ )
2116
+ }
2117
+
2118
+ function Shell(props: ToolProps<typeof ShellTool>) {
2119
+ const { theme } = useTheme()
2120
+ const pathFormatter = usePathFormatter()
2121
+ const ctx = use()
2122
+ const isRunning = createMemo(() => props.part.state.status === "running")
2123
+ const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
2124
+ const [expanded, setExpanded] = createSignal(false)
2125
+ const maxLines = 10
2126
+ const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6))
2127
+ const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars()))
2128
+ const limited = createMemo(() => {
2129
+ if (expanded() || !collapsed().overflow) return output()
2130
+ return collapsed().output
2131
+ })
2132
+
2133
+ const workdirDisplay = createMemo(() => {
2134
+ const workdir = props.input.workdir
2135
+ if (!workdir || workdir === ".") return undefined
2136
+ return pathFormatter.format(workdir)
2137
+ })
2138
+
2139
+ const title = createMemo(() => {
2140
+ const desc = props.input.description ?? "Shell"
2141
+ const wd = workdirDisplay()
2142
+ if (!wd) return `# ${desc}`
2143
+ if (desc.includes(wd)) return `# ${desc}`
2144
+ return `# ${desc} in ${wd}`
2145
+ })
2146
+
2147
+ return (
2148
+ <Switch>
2149
+ <Match when={props.metadata.output !== undefined}>
2150
+ <BlockTool
2151
+ title={title()}
2152
+ part={props.part}
2153
+ spinner={isRunning()}
2154
+ onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}
2155
+ >
2156
+ <box gap={1}>
2157
+ <text fg={theme.text}>$ {props.input.command}</text>
2158
+ <Show when={output()}>
2159
+ <text fg={theme.text}>{limited()}</text>
2160
+ </Show>
2161
+ <Show when={collapsed().overflow}>
2162
+ <text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
2163
+ </Show>
2164
+ </box>
2165
+ </BlockTool>
2166
+ </Match>
2167
+ <Match when={true}>
2168
+ <InlineTool icon="$" pending="Writing command..." complete={props.input.command} part={props.part}>
2169
+ {props.input.command}
2170
+ </InlineTool>
2171
+ </Match>
2172
+ </Switch>
2173
+ )
2174
+ }
2175
+
2176
+ function Write(props: ToolProps<typeof WriteTool>) {
2177
+ const { theme, syntax } = useTheme()
2178
+ const pathFormatter = usePathFormatter()
2179
+ const code = createMemo(() => {
2180
+ if (!props.input.content) return ""
2181
+ return props.input.content
2182
+ })
2183
+
2184
+ return (
2185
+ <Switch>
2186
+ <Match when={props.metadata.diagnostics !== undefined}>
2187
+ <BlockTool title={"# Wrote " + pathFormatter.format(props.input.filePath)} part={props.part}>
2188
+ <line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
2189
+ <code
2190
+ conceal={false}
2191
+ fg={theme.text}
2192
+ filetype={filetype(props.input.filePath!)}
2193
+ syntaxStyle={syntax()}
2194
+ content={code()}
2195
+ />
2196
+ </line_number>
2197
+ <Diagnostics diagnostics={props.metadata.diagnostics} filePath={props.input.filePath ?? ""} />
2198
+ </BlockTool>
2199
+ </Match>
2200
+ <Match when={true}>
2201
+ <InlineTool icon="←" pending="Preparing write..." complete={props.input.filePath} part={props.part}>
2202
+ Write {pathFormatter.format(props.input.filePath)}
2203
+ </InlineTool>
2204
+ </Match>
2205
+ </Switch>
2206
+ )
2207
+ }
2208
+
2209
+ function Glob(props: ToolProps<typeof GlobTool>) {
2210
+ const pathFormatter = usePathFormatter()
2211
+ return (
2212
+ <InlineTool icon="✱" pending="Finding files..." complete={props.input.pattern} part={props.part}>
2213
+ Glob "{props.input.pattern}" <Show when={props.input.path}>in {pathFormatter.format(props.input.path)} </Show>
2214
+ <Show when={props.metadata.count}>
2215
+ ({props.metadata.count} {props.metadata.count === 1 ? "match" : "matches"})
2216
+ </Show>
2217
+ </InlineTool>
2218
+ )
2219
+ }
2220
+
2221
+ function Read(props: ToolProps<typeof ReadTool>) {
2222
+ const { theme } = useTheme()
2223
+ const pathFormatter = usePathFormatter()
2224
+ const isRunning = createMemo(() => props.part.state.status === "running")
2225
+ const loaded = createMemo(() => {
2226
+ if (props.part.state.status !== "completed") return []
2227
+ if (props.part.state.time.compacted) return []
2228
+ const value = props.metadata.loaded
2229
+ if (!value || !Array.isArray(value)) return []
2230
+ return value.filter((p): p is string => typeof p === "string")
2231
+ })
2232
+ return (
2233
+ <>
2234
+ <InlineTool
2235
+ icon="→"
2236
+ pending="Reading file..."
2237
+ complete={props.input.filePath}
2238
+ spinner={isRunning()}
2239
+ part={props.part}
2240
+ >
2241
+ Read {pathFormatter.format(props.input.filePath)} {input(props.input, ["filePath"])}
2242
+ </InlineTool>
2243
+ <For each={loaded()}>
2244
+ {(filepath, index) => (
2245
+ <box id={`tool-inline-loaded-${props.part.id}-${index()}`} paddingLeft={3}>
2246
+ <text paddingLeft={3} fg={theme.textMuted}>
2247
+ ↳ Loaded {pathFormatter.format(filepath)}
2248
+ </text>
2249
+ </box>
2250
+ )}
2251
+ </For>
2252
+ </>
2253
+ )
2254
+ }
2255
+
2256
+ function Grep(props: ToolProps<typeof GrepTool>) {
2257
+ const pathFormatter = usePathFormatter()
2258
+ return (
2259
+ <InlineTool icon="✱" pending="Searching content..." complete={props.input.pattern} part={props.part}>
2260
+ Grep "{props.input.pattern}" <Show when={props.input.path}>in {pathFormatter.format(props.input.path)} </Show>
2261
+ <Show when={props.metadata.matches}>
2262
+ ({props.metadata.matches} {props.metadata.matches === 1 ? "match" : "matches"})
2263
+ </Show>
2264
+ </InlineTool>
2265
+ )
2266
+ }
2267
+
2268
+ function WebFetch(props: ToolProps<typeof WebFetchTool>) {
2269
+ return (
2270
+ <InlineTool icon="%" pending="Fetching from the web..." complete={props.input.url} part={props.part}>
2271
+ WebFetch {props.input.url}
2272
+ </InlineTool>
2273
+ )
2274
+ }
2275
+
2276
+ function WebSearch(props: ToolProps<typeof WebSearchTool>) {
2277
+ const metadata = () => props.metadata as { numResults?: number; provider?: unknown }
2278
+ return (
2279
+ <InlineTool icon="◈" pending="Searching web..." complete={props.input.query} part={props.part}>
2280
+ {webSearchProviderLabel(metadata().provider)} "{props.input.query}"{" "}
2281
+ <Show when={metadata().numResults}>({metadata().numResults} results)</Show>
2282
+ </InlineTool>
2283
+ )
2284
+ }
2285
+
2286
+ function Task(props: ToolProps<typeof TaskTool>) {
2287
+ const { theme } = useTheme()
2288
+ const { navigate } = useRoute()
2289
+ const sync = useSync()
2290
+ const dialog = useDialog()
2291
+
2292
+ onMount(() => {
2293
+ if (props.metadata.sessionId && !sync.data.message[props.metadata.sessionId]?.length)
2294
+ void sync.session.sync(props.metadata.sessionId)
2295
+ })
2296
+
2297
+ const messages = createMemo(() => sync.data.message[props.metadata.sessionId ?? ""] ?? [])
2298
+
2299
+ const tools = createMemo(() => {
2300
+ return messages().flatMap((msg) =>
2301
+ (sync.data.part[msg.id] ?? [])
2302
+ .filter((part): part is ToolPart => part.type === "tool")
2303
+ .map((part) => ({ tool: part.tool, state: part.state })),
2304
+ )
2305
+ })
2306
+
2307
+ const current = createMemo(() =>
2308
+ tools().findLast((x) => (x.state.status === "running" || x.state.status === "completed") && x.state.title),
2309
+ )
2310
+
2311
+ const status = createMemo(() => sync.data.session_status[props.metadata.sessionId ?? ""])
2312
+ const isRunning = createMemo(() => {
2313
+ const value = status()
2314
+ return (
2315
+ props.part.state.status === "running" ||
2316
+ (props.metadata.background === true && value !== undefined && value.type !== "idle")
2317
+ )
2318
+ })
2319
+ const retry = createMemo(() => {
2320
+ const value = status()
2321
+ if (value?.type !== "retry") return
2322
+ return value
2323
+ })
2324
+
2325
+ const duration = createMemo(() => {
2326
+ const first = messages().find((x) => x.role === "user")?.time.created
2327
+ const assistant = messages().findLast((x) => x.role === "assistant")?.time.completed
2328
+ if (!first || !assistant) return 0
2329
+ return assistant - first
2330
+ })
2331
+
2332
+ const content = createMemo(() => {
2333
+ if (!props.input.description) return ""
2334
+ let content = [
2335
+ formatSubagentTitle(
2336
+ Locale.titlecase(props.input.subagent_type ?? "General"),
2337
+ props.input.description,
2338
+ props.metadata.background === true,
2339
+ ),
2340
+ ]
2341
+
2342
+ const retrying = retry()
2343
+ if (isRunning() && retrying) {
2344
+ content.push(`↳ ${formatSubagentRetry(retrying.attempt, Locale.truncate(retrying.message, 80))}`)
2345
+ } else if (isRunning() && tools().length > 0) {
2346
+ if (current()) {
2347
+ const state = current()!.state
2348
+ const title = state.status === "running" || state.status === "completed" ? state.title : undefined
2349
+ content.push(`↳ ${Locale.titlecase(current()!.tool)} ${title}`)
2350
+ } else content.push(`↳ ${formatSubagentToolcalls(tools().length)}`)
2351
+ }
2352
+
2353
+ if (!isRunning() && props.part.state.status === "completed") {
2354
+ content.push(`↳ ${formatCompletedSubagentDetail(tools().length, Locale.duration(duration()))}`)
2355
+ }
2356
+
2357
+ return content.join("\n")
2358
+ })
2359
+
2360
+ return (
2361
+ <InlineTool
2362
+ icon={props.part.state.status === "completed" ? "✓" : "│"}
2363
+ subagent={true}
2364
+ color={retry() ? theme.error : undefined}
2365
+ spinner={isRunning()}
2366
+ complete={props.input.description}
2367
+ pending="Delegating..."
2368
+ part={props.part}
2369
+ onClick={() => {
2370
+ if (props.metadata.sessionId) {
2371
+ navigate({ type: "session", sessionID: props.metadata.sessionId })
2372
+ }
2373
+ const status = retry()
2374
+ if (status) void DialogAlert.show(dialog, "Retry Error", status.message)
2375
+ }}
2376
+ >
2377
+ {content()}
2378
+ </InlineTool>
2379
+ )
2380
+ }
2381
+
2382
+ export function formatSubagentToolcalls(count: number) {
2383
+ return `${count} toolcall${count === 1 ? "" : "s"}`
2384
+ }
2385
+
2386
+ export function formatSubagentTitle(agent: string, description: string, background: boolean) {
2387
+ return `${agent} Task${background ? " (background)" : ""} — ${description}`
2388
+ }
2389
+
2390
+ export function formatSubagentRetry(attempt: number, message: string) {
2391
+ return `Retrying (attempt ${attempt}) · ${message}`
2392
+ }
2393
+
2394
+ export function formatCompletedSubagentDetail(toolcalls: number, duration: string) {
2395
+ if (toolcalls === 0) return duration
2396
+ return `${formatSubagentToolcalls(toolcalls)} · ${duration}`
2397
+ }
2398
+
2399
+ function Edit(props: ToolProps<typeof EditTool>) {
2400
+ const ctx = use()
2401
+ const { theme, syntax } = useTheme()
2402
+ const pathFormatter = usePathFormatter()
2403
+
2404
+ const view = createMemo(() => {
2405
+ const diffStyle = ctx.tui.diff_style
2406
+ if (diffStyle === "stacked") return "unified"
2407
+ // Default to "auto" behavior
2408
+ return ctx.width > 120 ? "split" : "unified"
2409
+ })
2410
+
2411
+ const ft = createMemo(() => filetype(props.input.filePath))
2412
+
2413
+ const diffContent = createMemo(() => props.metadata.diff)
2414
+
2415
+ return (
2416
+ <Switch>
2417
+ <Match when={props.metadata.diff !== undefined}>
2418
+ <BlockTool title={"← Edit " + pathFormatter.format(props.input.filePath)} part={props.part}>
2419
+ <box paddingLeft={1}>
2420
+ <diff
2421
+ diff={diffContent()}
2422
+ view={view()}
2423
+ filetype={ft()}
2424
+ syntaxStyle={syntax()}
2425
+ showLineNumbers={true}
2426
+ width="100%"
2427
+ wrapMode={ctx.diffWrapMode()}
2428
+ fg={theme.text}
2429
+ addedBg={theme.diffAddedBg}
2430
+ removedBg={theme.diffRemovedBg}
2431
+ contextBg={theme.diffContextBg}
2432
+ addedSignColor={theme.diffHighlightAdded}
2433
+ removedSignColor={theme.diffHighlightRemoved}
2434
+ lineNumberFg={theme.diffLineNumber}
2435
+ lineNumberBg={theme.diffContextBg}
2436
+ addedLineNumberBg={theme.diffAddedLineNumberBg}
2437
+ removedLineNumberBg={theme.diffRemovedLineNumberBg}
2438
+ />
2439
+ </box>
2440
+ <Diagnostics diagnostics={props.metadata.diagnostics} filePath={props.input.filePath ?? ""} />
2441
+ </BlockTool>
2442
+ </Match>
2443
+ <Match when={true}>
2444
+ <InlineTool icon="←" pending="Preparing edit..." complete={props.input.filePath} part={props.part}>
2445
+ Edit {pathFormatter.format(props.input.filePath)} {input({ replaceAll: props.input.replaceAll })}
2446
+ </InlineTool>
2447
+ </Match>
2448
+ </Switch>
2449
+ )
2450
+ }
2451
+
2452
+ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
2453
+ const ctx = use()
2454
+ const { theme, syntax } = useTheme()
2455
+ const pathFormatter = usePathFormatter()
2456
+
2457
+ const files = createMemo(() => props.metadata.files ?? [])
2458
+
2459
+ const view = createMemo(() => {
2460
+ const diffStyle = ctx.tui.diff_style
2461
+ if (diffStyle === "stacked") return "unified"
2462
+ return ctx.width > 120 ? "split" : "unified"
2463
+ })
2464
+
2465
+ function Diff(p: { diff: string; filePath: string }) {
2466
+ return (
2467
+ <box paddingLeft={1}>
2468
+ <diff
2469
+ diff={p.diff}
2470
+ view={view()}
2471
+ filetype={filetype(p.filePath)}
2472
+ syntaxStyle={syntax()}
2473
+ showLineNumbers={true}
2474
+ width="100%"
2475
+ wrapMode={ctx.diffWrapMode()}
2476
+ fg={theme.text}
2477
+ addedBg={theme.diffAddedBg}
2478
+ removedBg={theme.diffRemovedBg}
2479
+ contextBg={theme.diffContextBg}
2480
+ addedSignColor={theme.diffHighlightAdded}
2481
+ removedSignColor={theme.diffHighlightRemoved}
2482
+ lineNumberFg={theme.diffLineNumber}
2483
+ lineNumberBg={theme.diffContextBg}
2484
+ addedLineNumberBg={theme.diffAddedLineNumberBg}
2485
+ removedLineNumberBg={theme.diffRemovedLineNumberBg}
2486
+ />
2487
+ </box>
2488
+ )
2489
+ }
2490
+
2491
+ function title(file: { type: string; relativePath: string; filePath: string; deletions: number }) {
2492
+ if (file.type === "delete") return "# Deleted " + file.relativePath
2493
+ if (file.type === "add") return "# Created " + file.relativePath
2494
+ if (file.type === "move") return "# Moved " + pathFormatter.format(file.filePath) + " → " + file.relativePath
2495
+ return "← Patched " + file.relativePath
2496
+ }
2497
+
2498
+ return (
2499
+ <Switch>
2500
+ <Match when={files().length > 0}>
2501
+ <For each={files()}>
2502
+ {(file) => (
2503
+ <BlockTool title={title(file)} part={props.part}>
2504
+ <Show
2505
+ when={file.type !== "delete"}
2506
+ fallback={
2507
+ <text fg={theme.diffRemoved}>
2508
+ -{file.deletions} line{file.deletions !== 1 ? "s" : ""}
2509
+ </text>
2510
+ }
2511
+ >
2512
+ <Diff diff={file.patch} filePath={file.filePath} />
2513
+ <Diagnostics diagnostics={props.metadata.diagnostics} filePath={file.movePath ?? file.filePath} />
2514
+ </Show>
2515
+ </BlockTool>
2516
+ )}
2517
+ </For>
2518
+ </Match>
2519
+ <Match when={true}>
2520
+ <InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}>
2521
+ Patch
2522
+ </InlineTool>
2523
+ </Match>
2524
+ </Switch>
2525
+ )
2526
+ }
2527
+
2528
+ function TodoWrite(props: ToolProps<typeof TodoWriteTool>) {
2529
+ return (
2530
+ <Switch>
2531
+ <Match when={props.metadata.todos?.length}>
2532
+ <BlockTool title="# Todos" part={props.part}>
2533
+ <box>
2534
+ <For each={props.input.todos ?? []}>
2535
+ {(todo) => <TodoItem status={todo.status} content={todo.content} />}
2536
+ </For>
2537
+ </box>
2538
+ </BlockTool>
2539
+ </Match>
2540
+ <Match when={true}>
2541
+ <InlineTool icon="⚙" pending="Updating todos..." complete={false} part={props.part}>
2542
+ Updating todos...
2543
+ </InlineTool>
2544
+ </Match>
2545
+ </Switch>
2546
+ )
2547
+ }
2548
+
2549
+ function Question(props: ToolProps<typeof QuestionTool>) {
2550
+ const { theme } = useTheme()
2551
+ const count = createMemo(() => props.input.questions?.length ?? 0)
2552
+
2553
+ function format(answer?: ReadonlyArray<string>) {
2554
+ if (!answer?.length) return "(no answer)"
2555
+ return answer.join(", ")
2556
+ }
2557
+
2558
+ return (
2559
+ <Switch>
2560
+ <Match when={props.metadata.answers}>
2561
+ <BlockTool title="# Questions" part={props.part}>
2562
+ <box gap={1}>
2563
+ <For each={props.input.questions ?? []}>
2564
+ {(q, i) => (
2565
+ <box flexDirection="column">
2566
+ <text fg={theme.textMuted}>{q.question}</text>
2567
+ <text fg={theme.text}>{format(props.metadata.answers?.[i()])}</text>
2568
+ </box>
2569
+ )}
2570
+ </For>
2571
+ </box>
2572
+ </BlockTool>
2573
+ </Match>
2574
+ <Match when={true}>
2575
+ <InlineTool icon="→" pending="Asking questions..." complete={count()} part={props.part}>
2576
+ Asked {count()} question{count() !== 1 ? "s" : ""}
2577
+ </InlineTool>
2578
+ </Match>
2579
+ </Switch>
2580
+ )
2581
+ }
2582
+
2583
+ function Skill(props: ToolProps<typeof SkillTool>) {
2584
+ return (
2585
+ <InlineTool icon="→" pending="Loading skill..." complete={props.input.name} part={props.part}>
2586
+ Skill "{props.input.name}"
2587
+ </InlineTool>
2588
+ )
2589
+ }
2590
+
2591
+ function Diagnostics(props: { diagnostics?: Record<string, Record<string, any>[]>; filePath: string }) {
2592
+ const { theme } = useTheme()
2593
+ const errors = createMemo(() => {
2594
+ const normalized = Filesystem.normalizePath(typeof props.filePath === "string" ? props.filePath : "")
2595
+ const arr = props.diagnostics?.[normalized] ?? []
2596
+ return arr.filter((x) => x.severity === 1).slice(0, 3)
2597
+ })
2598
+
2599
+ return (
2600
+ <Show when={errors().length}>
2601
+ <box>
2602
+ <For each={errors()}>
2603
+ {(diagnostic) => (
2604
+ <text fg={theme.error}>
2605
+ Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message}
2606
+ </text>
2607
+ )}
2608
+ </For>
2609
+ </box>
2610
+ </Show>
2611
+ )
2612
+ }
2613
+
2614
+ function input(input: Record<string, any>, omit?: string[]): string {
2615
+ const primitives = Object.entries(input).filter(([key, value]) => {
2616
+ if (omit?.includes(key)) return false
2617
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
2618
+ })
2619
+ if (primitives.length === 0) return ""
2620
+ return `[${primitives.map(([key, value]) => `${key}=${value}`).join(", ")}]`
2621
+ }
2622
+
2623
+ function filetype(input?: string) {
2624
+ if (typeof input !== "string" || !input) return "none"
2625
+ const ext = path.extname(input)
2626
+ const language = LANGUAGE_EXTENSIONS[ext]
2627
+ if (["typescriptreact", "javascriptreact", "javascript"].includes(language)) return "typescript"
2628
+ return language
2629
+ }