@happy-creative/iroder 1.0.0

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 (697) hide show
  1. package/.qwen/settings.json +8 -0
  2. package/.qwen/settings.json.orig +7 -0
  3. package/AGENTS.md +69 -0
  4. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  5. package/Dockerfile +20 -0
  6. package/README.md +15 -0
  7. package/bin/iroder +182 -0
  8. package/docker-compose.iroder.yml +18 -0
  9. package/drizzle.config.ts +10 -0
  10. package/git +0 -0
  11. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  12. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  13. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  14. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  15. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  16. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  17. package/migration/20260225215848_workspace/migration.sql +7 -0
  18. package/migration/20260225215848_workspace/snapshot.json +959 -0
  19. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  20. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  21. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  22. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  23. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  24. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  25. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  26. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  27. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  28. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  29. package/migration/20260323234822_events/migration.sql +13 -0
  30. package/migration/20260323234822_events/snapshot.json +1271 -0
  31. package/package.json +180 -0
  32. package/parsers-config.ts +290 -0
  33. package/script/build-node.ts +60 -0
  34. package/script/build.ts +281 -0
  35. package/script/check-migrations.ts +16 -0
  36. package/script/e2e-local-real-key.ts +197 -0
  37. package/script/fix-node-pty.ts +28 -0
  38. package/script/postinstall.mjs +131 -0
  39. package/script/publish-all.sh +68 -0
  40. package/script/publish.ts +181 -0
  41. package/script/schema.ts +63 -0
  42. package/script/seed-e2e.ts +60 -0
  43. package/script/upgrade-opentui.ts +64 -0
  44. package/specs/effect-migration.md +310 -0
  45. package/specs/tui-plugins.md +436 -0
  46. package/specs/v2/keymappings.md +10 -0
  47. package/specs/v2/message-shape.md +136 -0
  48. package/src/account/account.sql.ts +39 -0
  49. package/src/account/index.ts +488 -0
  50. package/src/account/repo.ts +166 -0
  51. package/src/account/schema.ts +119 -0
  52. package/src/account/url.ts +8 -0
  53. package/src/acp/README.md +174 -0
  54. package/src/acp/agent.ts +1847 -0
  55. package/src/acp/session.ts +116 -0
  56. package/src/acp/types.ts +24 -0
  57. package/src/agent/agent.ts +422 -0
  58. package/src/agent/generate.txt +75 -0
  59. package/src/agent/prompt/compaction.txt +15 -0
  60. package/src/agent/prompt/explore.txt +18 -0
  61. package/src/agent/prompt/summary.txt +11 -0
  62. package/src/agent/prompt/title.txt +44 -0
  63. package/src/auth/index.ts +110 -0
  64. package/src/bus/bus-event.ts +40 -0
  65. package/src/bus/global.ts +10 -0
  66. package/src/bus/index.ts +185 -0
  67. package/src/cli/bootstrap.ts +17 -0
  68. package/src/cli/cmd/account.ts +257 -0
  69. package/src/cli/cmd/acp.ts +72 -0
  70. package/src/cli/cmd/agent.ts +245 -0
  71. package/src/cli/cmd/cmd.ts +7 -0
  72. package/src/cli/cmd/db.ts +120 -0
  73. package/src/cli/cmd/debug/agent.ts +170 -0
  74. package/src/cli/cmd/debug/config.ts +16 -0
  75. package/src/cli/cmd/debug/file.ts +97 -0
  76. package/src/cli/cmd/debug/index.ts +48 -0
  77. package/src/cli/cmd/debug/lsp.ts +53 -0
  78. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  79. package/src/cli/cmd/debug/scrap.ts +16 -0
  80. package/src/cli/cmd/debug/skill.ts +16 -0
  81. package/src/cli/cmd/debug/snapshot.ts +52 -0
  82. package/src/cli/cmd/export.ts +89 -0
  83. package/src/cli/cmd/generate.ts +38 -0
  84. package/src/cli/cmd/github.ts +1647 -0
  85. package/src/cli/cmd/import.ts +207 -0
  86. package/src/cli/cmd/mcp.ts +754 -0
  87. package/src/cli/cmd/models.ts +78 -0
  88. package/src/cli/cmd/plug.ts +233 -0
  89. package/src/cli/cmd/pr.ts +127 -0
  90. package/src/cli/cmd/providers.ts +480 -0
  91. package/src/cli/cmd/run.ts +692 -0
  92. package/src/cli/cmd/serve.ts +23 -0
  93. package/src/cli/cmd/session.ts +159 -0
  94. package/src/cli/cmd/stats.ts +410 -0
  95. package/src/cli/cmd/tui/app.tsx +940 -0
  96. package/src/cli/cmd/tui/attach.ts +88 -0
  97. package/src/cli/cmd/tui/component/border.tsx +21 -0
  98. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  99. package/src/cli/cmd/tui/component/dialog-command.tsx +171 -0
  100. package/src/cli/cmd/tui/component/dialog-console-org.tsx +103 -0
  101. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +100 -0
  102. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  103. package/src/cli/cmd/tui/component/dialog-model.tsx +183 -0
  104. package/src/cli/cmd/tui/component/dialog-provider.tsx +360 -0
  105. package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
  106. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  107. package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
  108. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  109. package/src/cli/cmd/tui/component/dialog-status.tsx +168 -0
  110. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  111. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  112. package/src/cli/cmd/tui/component/dialog-variant.tsx +39 -0
  113. package/src/cli/cmd/tui/component/dialog-workspace-list.tsx +320 -0
  114. package/src/cli/cmd/tui/component/error-component.tsx +93 -0
  115. package/src/cli/cmd/tui/component/logo.tsx +85 -0
  116. package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
  117. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +672 -0
  118. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  119. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  120. package/src/cli/cmd/tui/component/prompt/index.tsx +1261 -0
  121. package/src/cli/cmd/tui/component/prompt/part.ts +16 -0
  122. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  123. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  124. package/src/cli/cmd/tui/component/startup-loading.tsx +63 -0
  125. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  126. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  127. package/src/cli/cmd/tui/component/workspace/dialog-session-list.tsx +151 -0
  128. package/src/cli/cmd/tui/context/args.tsx +15 -0
  129. package/src/cli/cmd/tui/context/directory.ts +13 -0
  130. package/src/cli/cmd/tui/context/exit.tsx +60 -0
  131. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  132. package/src/cli/cmd/tui/context/keybind.tsx +105 -0
  133. package/src/cli/cmd/tui/context/kv.tsx +52 -0
  134. package/src/cli/cmd/tui/context/local.tsx +412 -0
  135. package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
  136. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  137. package/src/cli/cmd/tui/context/route.tsx +52 -0
  138. package/src/cli/cmd/tui/context/sdk.tsx +115 -0
  139. package/src/cli/cmd/tui/context/sync.tsx +516 -0
  140. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  141. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  142. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  143. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  144. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  145. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  146. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  147. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  148. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  149. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  150. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  151. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  152. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  153. package/src/cli/cmd/tui/context/theme/iroder.json +245 -0
  154. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  155. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  156. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  157. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  158. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  159. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  160. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  161. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  162. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  163. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  164. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  165. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  166. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  167. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  168. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  169. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  170. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  171. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  172. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  173. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  174. package/src/cli/cmd/tui/context/theme.tsx +1236 -0
  175. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  176. package/src/cli/cmd/tui/event.ts +49 -0
  177. package/src/cli/cmd/tui/feature-plugins/home/footer.tsx +93 -0
  178. package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +155 -0
  179. package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +50 -0
  180. package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +63 -0
  181. package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
  182. package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
  183. package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
  184. package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +96 -0
  185. package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +48 -0
  186. package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +270 -0
  187. package/src/cli/cmd/tui/plugin/api.tsx +397 -0
  188. package/src/cli/cmd/tui/plugin/index.ts +3 -0
  189. package/src/cli/cmd/tui/plugin/internal.ts +27 -0
  190. package/src/cli/cmd/tui/plugin/runtime.ts +1031 -0
  191. package/src/cli/cmd/tui/plugin/slots.tsx +60 -0
  192. package/src/cli/cmd/tui/routes/home.tsx +84 -0
  193. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +65 -0
  194. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +110 -0
  195. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  196. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  197. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  198. package/src/cli/cmd/tui/routes/session/index.tsx +2281 -0
  199. package/src/cli/cmd/tui/routes/session/permission.tsx +691 -0
  200. package/src/cli/cmd/tui/routes/session/question.tsx +468 -0
  201. package/src/cli/cmd/tui/routes/session/sidebar.tsx +74 -0
  202. package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +131 -0
  203. package/src/cli/cmd/tui/thread.ts +241 -0
  204. package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
  205. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +89 -0
  206. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +211 -0
  207. package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
  208. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +115 -0
  209. package/src/cli/cmd/tui/ui/dialog-select.tsx +417 -0
  210. package/src/cli/cmd/tui/ui/dialog.tsx +192 -0
  211. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  212. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  213. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  214. package/src/cli/cmd/tui/util/clipboard.ts +192 -0
  215. package/src/cli/cmd/tui/util/editor.ts +37 -0
  216. package/src/cli/cmd/tui/util/model.ts +23 -0
  217. package/src/cli/cmd/tui/util/provider-origin.ts +7 -0
  218. package/src/cli/cmd/tui/util/scroll.ts +23 -0
  219. package/src/cli/cmd/tui/util/selection.ts +25 -0
  220. package/src/cli/cmd/tui/util/signal.ts +7 -0
  221. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  222. package/src/cli/cmd/tui/util/transcript.ts +112 -0
  223. package/src/cli/cmd/tui/win32.ts +129 -0
  224. package/src/cli/cmd/tui/worker.ts +195 -0
  225. package/src/cli/cmd/uninstall.ts +353 -0
  226. package/src/cli/cmd/upgrade.ts +73 -0
  227. package/src/cli/cmd/web.ts +83 -0
  228. package/src/cli/commands.ts +58 -0
  229. package/src/cli/effect/prompt.ts +25 -0
  230. package/src/cli/error.ts +50 -0
  231. package/src/cli/heap.ts +59 -0
  232. package/src/cli/llm-ready.ts +22 -0
  233. package/src/cli/logo.ts +8 -0
  234. package/src/cli/network.ts +60 -0
  235. package/src/cli/ui.ts +132 -0
  236. package/src/cli/upgrade.ts +31 -0
  237. package/src/command/index.ts +195 -0
  238. package/src/command/template/initialize.txt +66 -0
  239. package/src/command/template/review.txt +101 -0
  240. package/src/config/config.ts +1659 -0
  241. package/src/config/console-state.ts +15 -0
  242. package/src/config/markdown.ts +99 -0
  243. package/src/config/paths.ts +167 -0
  244. package/src/config/tui-migrate.ts +156 -0
  245. package/src/config/tui-schema.ts +37 -0
  246. package/src/config/tui.ts +179 -0
  247. package/src/control-plane/adaptors/index.ts +23 -0
  248. package/src/control-plane/adaptors/remote-acp.ts +35 -0
  249. package/src/control-plane/adaptors/remote-http.ts +35 -0
  250. package/src/control-plane/adaptors/worktree.ts +42 -0
  251. package/src/control-plane/schema.ts +17 -0
  252. package/src/control-plane/sse.ts +66 -0
  253. package/src/control-plane/types.ts +32 -0
  254. package/src/control-plane/workspace.sql.ts +17 -0
  255. package/src/control-plane/workspace.ts +169 -0
  256. package/src/effect/cross-spawn-spawner.ts +502 -0
  257. package/src/effect/instance-ref.ts +6 -0
  258. package/src/effect/instance-registry.ts +12 -0
  259. package/src/effect/instance-state.ts +82 -0
  260. package/src/effect/oltp.ts +34 -0
  261. package/src/effect/run-service.ts +34 -0
  262. package/src/effect/runner.ts +216 -0
  263. package/src/env/index.ts +28 -0
  264. package/src/file/ignore.ts +82 -0
  265. package/src/file/index.ts +686 -0
  266. package/src/file/protected.ts +59 -0
  267. package/src/file/ripgrep.ts +376 -0
  268. package/src/file/time.ts +133 -0
  269. package/src/file/watcher.ts +171 -0
  270. package/src/filesystem/index.ts +236 -0
  271. package/src/flag/flag.ts +171 -0
  272. package/src/format/formatter.ts +413 -0
  273. package/src/format/index.ts +203 -0
  274. package/src/git/index.ts +303 -0
  275. package/src/global/index.ts +54 -0
  276. package/src/id/id.ts +85 -0
  277. package/src/ide/index.ts +74 -0
  278. package/src/index.ts +202 -0
  279. package/src/installation/index.ts +356 -0
  280. package/src/installation/meta.ts +7 -0
  281. package/src/lsp/client.ts +252 -0
  282. package/src/lsp/index.ts +558 -0
  283. package/src/lsp/language.ts +120 -0
  284. package/src/lsp/launch.ts +21 -0
  285. package/src/lsp/server.ts +1968 -0
  286. package/src/mcp/auth.ts +173 -0
  287. package/src/mcp/index.ts +921 -0
  288. package/src/mcp/oauth-callback.ts +216 -0
  289. package/src/mcp/oauth-provider.ts +186 -0
  290. package/src/node.ts +6 -0
  291. package/src/npm/index.ts +188 -0
  292. package/src/patch/index.ts +680 -0
  293. package/src/permission/arity.ts +163 -0
  294. package/src/permission/evaluate.ts +15 -0
  295. package/src/permission/index.ts +325 -0
  296. package/src/permission/schema.ts +17 -0
  297. package/src/plugin/cloudflare.ts +67 -0
  298. package/src/plugin/codex.ts +608 -0
  299. package/src/plugin/github-copilot/copilot.ts +361 -0
  300. package/src/plugin/github-copilot/models.ts +144 -0
  301. package/src/plugin/index.ts +293 -0
  302. package/src/plugin/install.ts +439 -0
  303. package/src/plugin/loader.ts +174 -0
  304. package/src/plugin/meta.ts +188 -0
  305. package/src/plugin/shared.ts +323 -0
  306. package/src/project/bootstrap.ts +31 -0
  307. package/src/project/instance.ts +175 -0
  308. package/src/project/project.sql.ts +16 -0
  309. package/src/project/project.ts +519 -0
  310. package/src/project/schema.ts +16 -0
  311. package/src/project/state.ts +70 -0
  312. package/src/project/vcs.ts +254 -0
  313. package/src/provider/auth.ts +253 -0
  314. package/src/provider/error.ts +197 -0
  315. package/src/provider/models.ts +159 -0
  316. package/src/provider/provider.ts +1748 -0
  317. package/src/provider/schema.ts +38 -0
  318. package/src/provider/sdk/copilot/README.md +5 -0
  319. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  320. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  321. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  322. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  323. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  324. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  325. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  326. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  327. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  328. package/src/provider/sdk/copilot/index.ts +2 -0
  329. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  330. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
  331. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  332. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  333. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  334. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
  335. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1769 -0
  336. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
  337. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  338. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
  339. package/src/provider/sdk/copilot/responses/tool/file-search.ts +127 -0
  340. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +114 -0
  341. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +64 -0
  342. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
  343. package/src/provider/sdk/copilot/responses/tool/web-search.ts +102 -0
  344. package/src/provider/transform.ts +1051 -0
  345. package/src/pty/index.ts +397 -0
  346. package/src/pty/pty.bun.ts +26 -0
  347. package/src/pty/pty.node.ts +27 -0
  348. package/src/pty/pty.ts +25 -0
  349. package/src/pty/schema.ts +17 -0
  350. package/src/question/index.ts +224 -0
  351. package/src/question/schema.ts +17 -0
  352. package/src/runtime/adapters/acp/index.ts +127 -0
  353. package/src/runtime/adapters/local/index.ts +6 -0
  354. package/src/runtime/core/agent.ts +13 -0
  355. package/src/runtime/core/mcp.ts +10 -0
  356. package/src/runtime/core/ports.ts +16 -0
  357. package/src/runtime/core/service.ts +19 -0
  358. package/src/runtime/core/session-summarize.ts +52 -0
  359. package/src/runtime/core/session.ts +110 -0
  360. package/src/runtime/core/skill.ts +10 -0
  361. package/src/runtime/core/transport.ts +10 -0
  362. package/src/runtime/factory.ts +20 -0
  363. package/src/runtime/ports.ts +74 -0
  364. package/src/server/error.ts +36 -0
  365. package/src/server/event.ts +7 -0
  366. package/src/server/instance.ts +322 -0
  367. package/src/server/mdns.ts +60 -0
  368. package/src/server/middleware.ts +33 -0
  369. package/src/server/projectors.ts +28 -0
  370. package/src/server/proxy.ts +134 -0
  371. package/src/server/router.ts +161 -0
  372. package/src/server/routes/config.ts +92 -0
  373. package/src/server/routes/event.ts +83 -0
  374. package/src/server/routes/experimental.ts +379 -0
  375. package/src/server/routes/file.ts +197 -0
  376. package/src/server/routes/global.ts +312 -0
  377. package/src/server/routes/mcp.ts +226 -0
  378. package/src/server/routes/permission.ts +69 -0
  379. package/src/server/routes/project.ts +118 -0
  380. package/src/server/routes/provider.ts +171 -0
  381. package/src/server/routes/pty.ts +210 -0
  382. package/src/server/routes/question.ts +99 -0
  383. package/src/server/routes/session.ts +1011 -0
  384. package/src/server/routes/tui.ts +379 -0
  385. package/src/server/routes/workspace.ts +94 -0
  386. package/src/server/server.ts +367 -0
  387. package/src/session/compaction.ts +425 -0
  388. package/src/session/index.ts +887 -0
  389. package/src/session/instruction.ts +258 -0
  390. package/src/session/llm.ts +412 -0
  391. package/src/session/message-v2.ts +1038 -0
  392. package/src/session/message.ts +191 -0
  393. package/src/session/overflow.ts +22 -0
  394. package/src/session/processor.ts +515 -0
  395. package/src/session/projectors.ts +135 -0
  396. package/src/session/prompt/anthropic.txt +105 -0
  397. package/src/session/prompt/beast.txt +147 -0
  398. package/src/session/prompt/build-switch.txt +5 -0
  399. package/src/session/prompt/codex.txt +79 -0
  400. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  401. package/src/session/prompt/default.txt +105 -0
  402. package/src/session/prompt/gemini.txt +155 -0
  403. package/src/session/prompt/gpt.txt +107 -0
  404. package/src/session/prompt/kimi.txt +95 -0
  405. package/src/session/prompt/max-steps.txt +16 -0
  406. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  407. package/src/session/prompt/plan.txt +26 -0
  408. package/src/session/prompt/trinity.txt +97 -0
  409. package/src/session/prompt.ts +1908 -0
  410. package/src/session/retry.ts +123 -0
  411. package/src/session/revert.ts +176 -0
  412. package/src/session/schema.ts +38 -0
  413. package/src/session/session.sql.ts +103 -0
  414. package/src/session/status.ts +102 -0
  415. package/src/session/summary.ts +177 -0
  416. package/src/session/system.ts +76 -0
  417. package/src/session/todo.ts +95 -0
  418. package/src/share/share-next.ts +370 -0
  419. package/src/share/share.sql.ts +13 -0
  420. package/src/shell/shell.ts +110 -0
  421. package/src/skill/discovery.ts +116 -0
  422. package/src/skill/index.ts +289 -0
  423. package/src/snapshot/index.ts +723 -0
  424. package/src/sql.d.ts +4 -0
  425. package/src/storage/db.bun.ts +8 -0
  426. package/src/storage/db.node.ts +8 -0
  427. package/src/storage/db.ts +174 -0
  428. package/src/storage/json-migration.ts +425 -0
  429. package/src/storage/schema.sql.ts +10 -0
  430. package/src/storage/schema.ts +5 -0
  431. package/src/storage/storage.ts +353 -0
  432. package/src/sync/README.md +179 -0
  433. package/src/sync/event.sql.ts +16 -0
  434. package/src/sync/index.ts +263 -0
  435. package/src/sync/schema.ts +14 -0
  436. package/src/testing/llm-server.ts +2 -0
  437. package/src/tool/apply_patch.ts +279 -0
  438. package/src/tool/apply_patch.txt +33 -0
  439. package/src/tool/bash.ts +498 -0
  440. package/src/tool/bash.txt +117 -0
  441. package/src/tool/codesearch.ts +133 -0
  442. package/src/tool/codesearch.txt +12 -0
  443. package/src/tool/edit.ts +666 -0
  444. package/src/tool/edit.txt +10 -0
  445. package/src/tool/external-directory.ts +46 -0
  446. package/src/tool/glob.ts +78 -0
  447. package/src/tool/glob.txt +6 -0
  448. package/src/tool/grep.ts +156 -0
  449. package/src/tool/grep.txt +8 -0
  450. package/src/tool/invalid.ts +17 -0
  451. package/src/tool/ls.ts +121 -0
  452. package/src/tool/ls.txt +1 -0
  453. package/src/tool/lsp.ts +97 -0
  454. package/src/tool/lsp.txt +19 -0
  455. package/src/tool/multiedit.ts +46 -0
  456. package/src/tool/multiedit.txt +41 -0
  457. package/src/tool/plan-enter.txt +14 -0
  458. package/src/tool/plan-exit.txt +13 -0
  459. package/src/tool/plan.ts +131 -0
  460. package/src/tool/question.ts +46 -0
  461. package/src/tool/question.txt +10 -0
  462. package/src/tool/read.ts +330 -0
  463. package/src/tool/read.txt +14 -0
  464. package/src/tool/registry.ts +303 -0
  465. package/src/tool/schema.ts +17 -0
  466. package/src/tool/skill.ts +120 -0
  467. package/src/tool/task.ts +192 -0
  468. package/src/tool/task.txt +57 -0
  469. package/src/tool/todo.ts +48 -0
  470. package/src/tool/todowrite.txt +167 -0
  471. package/src/tool/tool.ts +137 -0
  472. package/src/tool/truncate.ts +144 -0
  473. package/src/tool/truncation-dir.ts +4 -0
  474. package/src/tool/webfetch.ts +210 -0
  475. package/src/tool/webfetch.txt +13 -0
  476. package/src/tool/websearch.ts +151 -0
  477. package/src/tool/websearch.txt +14 -0
  478. package/src/tool/write.ts +84 -0
  479. package/src/tool/write.txt +8 -0
  480. package/src/url/site.ts +118 -0
  481. package/src/util/abort.ts +35 -0
  482. package/src/util/archive.ts +17 -0
  483. package/src/util/color.ts +19 -0
  484. package/src/util/context.ts +25 -0
  485. package/src/util/data-url.ts +9 -0
  486. package/src/util/defer.ts +12 -0
  487. package/src/util/effect-http-client.ts +11 -0
  488. package/src/util/effect-zod.ts +98 -0
  489. package/src/util/error.ts +77 -0
  490. package/src/util/filesystem.ts +245 -0
  491. package/src/util/flock.ts +333 -0
  492. package/src/util/fn.ts +21 -0
  493. package/src/util/format.ts +20 -0
  494. package/src/util/glob.ts +34 -0
  495. package/src/util/hash.ts +7 -0
  496. package/src/util/iife.ts +3 -0
  497. package/src/util/keybind.ts +103 -0
  498. package/src/util/lazy.ts +23 -0
  499. package/src/util/locale.ts +81 -0
  500. package/src/util/lock.ts +98 -0
  501. package/src/util/log.ts +182 -0
  502. package/src/util/network.ts +9 -0
  503. package/src/util/process.ts +176 -0
  504. package/src/util/queue.ts +32 -0
  505. package/src/util/record.ts +3 -0
  506. package/src/util/rpc.ts +66 -0
  507. package/src/util/schema.ts +53 -0
  508. package/src/util/scrap.ts +10 -0
  509. package/src/util/signal.ts +12 -0
  510. package/src/util/timeout.ts +14 -0
  511. package/src/util/token.ts +7 -0
  512. package/src/util/update-schema.ts +13 -0
  513. package/src/util/which.ts +14 -0
  514. package/src/util/wildcard.ts +59 -0
  515. package/src/worktree/index.ts +612 -0
  516. package/sst-env.d.ts +10 -0
  517. package/test/AGENTS.md +81 -0
  518. package/test/account/repo.test.ts +352 -0
  519. package/test/account/service.test.ts +456 -0
  520. package/test/acp/agent-interface.test.ts +51 -0
  521. package/test/acp/event-subscription.test.ts +685 -0
  522. package/test/agent/agent.test.ts +717 -0
  523. package/test/auth/auth.test.ts +58 -0
  524. package/test/bus/bus-effect.test.ts +164 -0
  525. package/test/bus/bus-integration.test.ts +87 -0
  526. package/test/bus/bus.test.ts +219 -0
  527. package/test/cli/account.test.ts +26 -0
  528. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  529. package/test/cli/commands.test.ts +49 -0
  530. package/test/cli/error.test.ts +18 -0
  531. package/test/cli/github-action.test.ts +198 -0
  532. package/test/cli/github-remote.test.ts +80 -0
  533. package/test/cli/import.test.ts +54 -0
  534. package/test/cli/llm-ready.test.ts +49 -0
  535. package/test/cli/plugin-auth-picker.test.ts +120 -0
  536. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  537. package/test/cli/tui/plugin-add.test.ts +107 -0
  538. package/test/cli/tui/plugin-install.test.ts +89 -0
  539. package/test/cli/tui/plugin-lifecycle.test.ts +225 -0
  540. package/test/cli/tui/plugin-loader-entrypoint.test.ts +492 -0
  541. package/test/cli/tui/plugin-loader-pure.test.ts +72 -0
  542. package/test/cli/tui/plugin-loader.test.ts +752 -0
  543. package/test/cli/tui/plugin-toggle.test.ts +159 -0
  544. package/test/cli/tui/slot-replace.test.tsx +47 -0
  545. package/test/cli/tui/theme-store.test.ts +51 -0
  546. package/test/cli/tui/thread.test.ts +128 -0
  547. package/test/cli/tui/transcript.test.ts +426 -0
  548. package/test/config/agent-color.test.ts +71 -0
  549. package/test/config/config.test.ts +2364 -0
  550. package/test/config/fixtures/empty-frontmatter.md +4 -0
  551. package/test/config/fixtures/frontmatter.md +28 -0
  552. package/test/config/fixtures/markdown-header.md +11 -0
  553. package/test/config/fixtures/no-frontmatter.md +1 -0
  554. package/test/config/fixtures/weird-model-id.md +13 -0
  555. package/test/config/markdown.test.ts +228 -0
  556. package/test/config/tui.test.ts +800 -0
  557. package/test/control-plane/sse.test.ts +56 -0
  558. package/test/effect/cross-spawn-spawner.test.ts +412 -0
  559. package/test/effect/instance-state.test.ts +482 -0
  560. package/test/effect/run-service.test.ts +46 -0
  561. package/test/effect/runner.test.ts +523 -0
  562. package/test/fake/provider.ts +81 -0
  563. package/test/file/fsmonitor.test.ts +62 -0
  564. package/test/file/ignore.test.ts +10 -0
  565. package/test/file/index.test.ts +946 -0
  566. package/test/file/path-traversal.test.ts +198 -0
  567. package/test/file/ripgrep.test.ts +54 -0
  568. package/test/file/time.test.ts +445 -0
  569. package/test/file/watcher.test.ts +247 -0
  570. package/test/filesystem/filesystem.test.ts +319 -0
  571. package/test/fixture/db.ts +11 -0
  572. package/test/fixture/fixture.test.ts +26 -0
  573. package/test/fixture/fixture.ts +174 -0
  574. package/test/fixture/flock-worker.ts +72 -0
  575. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  576. package/test/fixture/plug-worker.ts +93 -0
  577. package/test/fixture/plugin-meta-worker.ts +26 -0
  578. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  579. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  580. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  581. package/test/fixture/skills/index.json +6 -0
  582. package/test/fixture/tui-plugin.ts +328 -0
  583. package/test/fixture/tui-runtime.ts +27 -0
  584. package/test/format/format.test.ts +171 -0
  585. package/test/git/git.test.ts +128 -0
  586. package/test/ide/ide.test.ts +82 -0
  587. package/test/installation/installation.test.ts +151 -0
  588. package/test/keybind.test.ts +421 -0
  589. package/test/lib/effect.ts +53 -0
  590. package/test/lib/filesystem.ts +10 -0
  591. package/test/lib/llm-server.ts +795 -0
  592. package/test/lsp/client.test.ts +95 -0
  593. package/test/lsp/index.test.ts +138 -0
  594. package/test/lsp/launch.test.ts +22 -0
  595. package/test/lsp/lifecycle.test.ts +147 -0
  596. package/test/mcp/headers.test.ts +153 -0
  597. package/test/mcp/lifecycle.test.ts +750 -0
  598. package/test/mcp/oauth-auto-connect.test.ts +199 -0
  599. package/test/mcp/oauth-browser.test.ts +249 -0
  600. package/test/memory/abort-leak.test.ts +151 -0
  601. package/test/npm.test.ts +18 -0
  602. package/test/patch/patch.test.ts +348 -0
  603. package/test/permission/arity.test.ts +33 -0
  604. package/test/permission/next.test.ts +1148 -0
  605. package/test/permission-task.test.ts +323 -0
  606. package/test/plugin/auth-override.test.ts +74 -0
  607. package/test/plugin/codex.test.ts +123 -0
  608. package/test/plugin/github-copilot-models.test.ts +117 -0
  609. package/test/plugin/install-concurrency.test.ts +140 -0
  610. package/test/plugin/install.test.ts +570 -0
  611. package/test/plugin/loader-shared.test.ts +1136 -0
  612. package/test/plugin/meta.test.ts +137 -0
  613. package/test/plugin/shared.test.ts +88 -0
  614. package/test/plugin/trigger.test.ts +111 -0
  615. package/test/preload.ts +90 -0
  616. package/test/project/migrate-global.test.ts +141 -0
  617. package/test/project/project.test.ts +459 -0
  618. package/test/project/state.test.ts +115 -0
  619. package/test/project/vcs.test.ts +228 -0
  620. package/test/project/worktree-remove.test.ts +96 -0
  621. package/test/project/worktree.test.ts +173 -0
  622. package/test/provider/amazon-bedrock.test.ts +447 -0
  623. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  624. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  625. package/test/provider/gitlab-duo.test.ts +412 -0
  626. package/test/provider/provider.test.ts +2494 -0
  627. package/test/provider/transform.test.ts +2839 -0
  628. package/test/pty/pty-output-isolation.test.ts +141 -0
  629. package/test/pty/pty-session.test.ts +92 -0
  630. package/test/pty/pty-shell.test.ts +59 -0
  631. package/test/question/question.test.ts +453 -0
  632. package/test/runtime/acp-adapter.test.ts +84 -0
  633. package/test/runtime/core-service.test.ts +16 -0
  634. package/test/runtime/session-summarize.test.ts +104 -0
  635. package/test/server/global-session-list.test.ts +89 -0
  636. package/test/server/project-init-git.test.ts +121 -0
  637. package/test/server/router.test.ts +52 -0
  638. package/test/server/session-actions.test.ts +83 -0
  639. package/test/server/session-list.test.ts +98 -0
  640. package/test/server/session-messages.test.ts +159 -0
  641. package/test/server/session-select.test.ts +84 -0
  642. package/test/session/compaction.test.ts +1239 -0
  643. package/test/session/instruction.test.ts +286 -0
  644. package/test/session/llm.test.ts +1093 -0
  645. package/test/session/message-v2.test.ts +957 -0
  646. package/test/session/messages-pagination.test.ts +885 -0
  647. package/test/session/processor-effect.test.ts +741 -0
  648. package/test/session/prompt-effect.test.ts +1339 -0
  649. package/test/session/prompt.test.ts +533 -0
  650. package/test/session/retry.test.ts +251 -0
  651. package/test/session/revert-compact.test.ts +621 -0
  652. package/test/session/session.test.ts +142 -0
  653. package/test/session/snapshot-tool-race.test.ts +242 -0
  654. package/test/session/structured-output-integration.test.ts +233 -0
  655. package/test/session/structured-output.test.ts +391 -0
  656. package/test/session/system.test.ts +59 -0
  657. package/test/share/share-next.test.ts +332 -0
  658. package/test/shell/shell.test.ts +73 -0
  659. package/test/skill/discovery.test.ts +116 -0
  660. package/test/skill/skill.test.ts +428 -0
  661. package/test/snapshot/snapshot.test.ts +1397 -0
  662. package/test/storage/db.test.ts +14 -0
  663. package/test/storage/json-migration.test.ts +832 -0
  664. package/test/storage/storage.test.ts +295 -0
  665. package/test/sync/index.test.ts +191 -0
  666. package/test/tool/apply_patch.test.ts +565 -0
  667. package/test/tool/bash.test.ts +1099 -0
  668. package/test/tool/edit.test.ts +681 -0
  669. package/test/tool/external-directory.test.ts +198 -0
  670. package/test/tool/fixtures/large-image.png +0 -0
  671. package/test/tool/fixtures/models-api.json +65179 -0
  672. package/test/tool/grep.test.ts +111 -0
  673. package/test/tool/question.test.ts +126 -0
  674. package/test/tool/read.test.ts +468 -0
  675. package/test/tool/registry.test.ts +157 -0
  676. package/test/tool/skill.test.ts +170 -0
  677. package/test/tool/task.test.ts +412 -0
  678. package/test/tool/tool-define.test.ts +49 -0
  679. package/test/tool/truncation.test.ts +161 -0
  680. package/test/tool/webfetch.test.ts +96 -0
  681. package/test/tool/write.test.ts +353 -0
  682. package/test/util/data-url.test.ts +14 -0
  683. package/test/util/effect-zod.test.ts +61 -0
  684. package/test/util/error.test.ts +38 -0
  685. package/test/util/filesystem.test.ts +656 -0
  686. package/test/util/flock.test.ts +383 -0
  687. package/test/util/format.test.ts +59 -0
  688. package/test/util/glob.test.ts +164 -0
  689. package/test/util/iife.test.ts +36 -0
  690. package/test/util/lazy.test.ts +50 -0
  691. package/test/util/lock.test.ts +72 -0
  692. package/test/util/module.test.ts +59 -0
  693. package/test/util/process.test.ts +128 -0
  694. package/test/util/timeout.test.ts +21 -0
  695. package/test/util/which.test.ts +100 -0
  696. package/test/util/wildcard.test.ts +90 -0
  697. package/tsconfig.json +23 -0
@@ -0,0 +1,1847 @@
1
+ import {
2
+ RequestError,
3
+ type Agent as ACPAgent,
4
+ type AgentSideConnection,
5
+ type AuthenticateRequest,
6
+ type AuthMethod,
7
+ type CancelNotification,
8
+ type ForkSessionRequest,
9
+ type ForkSessionResponse,
10
+ type InitializeRequest,
11
+ type InitializeResponse,
12
+ type ListSessionsRequest,
13
+ type ListSessionsResponse,
14
+ type LoadSessionRequest,
15
+ type NewSessionRequest,
16
+ type PermissionOption,
17
+ type PlanEntry,
18
+ type PromptRequest,
19
+ type ResumeSessionRequest,
20
+ type ResumeSessionResponse,
21
+ type Role,
22
+ type SessionInfo,
23
+ type SetSessionModelRequest,
24
+ type SessionConfigOption,
25
+ type SetSessionConfigOptionRequest,
26
+ type SetSessionConfigOptionResponse,
27
+ type SetSessionModeRequest,
28
+ type SetSessionModeResponse,
29
+ type ToolCallContent,
30
+ type ToolKind,
31
+ type Usage,
32
+ } from "@agentclientprotocol/sdk"
33
+
34
+ import { Log } from "../util/log"
35
+ import { pathToFileURL } from "url"
36
+ import { Filesystem } from "../util/filesystem"
37
+ import { Hash } from "../util/hash"
38
+ import { ACPSessionManager } from "./session"
39
+ import type { ACPConfig } from "./types"
40
+ import { Provider } from "../provider/provider"
41
+ import { ModelID, ProviderID } from "../provider/schema"
42
+ import { Agent as AgentModule } from "../agent/agent"
43
+ import { Installation } from "@/installation"
44
+ import { MessageV2 } from "@/session/message-v2"
45
+ import { Config } from "@/config/config"
46
+ import { Todo } from "@/session/todo"
47
+ import { z } from "zod"
48
+ import { LoadAPIKeyError } from "ai"
49
+ import type { AssistantMessage, Event, IroderClient, SessionMessageResponse, ToolPart } from "@happy-creative/sdk/v2"
50
+ import { applyPatch } from "diff"
51
+
52
+ type ModeOption = { id: string; name: string; description?: string }
53
+ type ModelOption = { modelId: string; name: string }
54
+
55
+ const DEFAULT_VARIANT_VALUE = "default"
56
+
57
+ export namespace ACP {
58
+ const log = Log.create({ service: "acp-agent" })
59
+
60
+ async function getContextLimit(
61
+ sdk: IroderClient,
62
+ providerID: ProviderID,
63
+ modelID: ModelID,
64
+ directory: string,
65
+ ): Promise<number | null> {
66
+ const providers = await sdk.config
67
+ .providers({ directory })
68
+ .then((x) => x.data?.providers ?? [])
69
+ .catch((error) => {
70
+ log.error("failed to get providers for context limit", { error })
71
+ return []
72
+ })
73
+
74
+ const provider = providers.find((p) => p.id === providerID)
75
+ const model = provider?.models[modelID]
76
+ return model?.limit.context ?? null
77
+ }
78
+
79
+ async function sendUsageUpdate(
80
+ connection: AgentSideConnection,
81
+ sdk: IroderClient,
82
+ sessionID: string,
83
+ directory: string,
84
+ ): Promise<void> {
85
+ const messages = await sdk.session
86
+ .messages({ sessionID, directory }, { throwOnError: true })
87
+ .then((x) => x.data)
88
+ .catch((error) => {
89
+ log.error("failed to fetch messages for usage update", { error })
90
+ return undefined
91
+ })
92
+
93
+ if (!messages) return
94
+
95
+ const assistantMessages = messages.filter(
96
+ (m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant",
97
+ )
98
+
99
+ const lastAssistant = assistantMessages[assistantMessages.length - 1]
100
+ if (!lastAssistant) return
101
+
102
+ const msg = lastAssistant.info
103
+ if (!msg.providerID || !msg.modelID) return
104
+ const size = await getContextLimit(sdk, ProviderID.make(msg.providerID), ModelID.make(msg.modelID), directory)
105
+
106
+ if (!size) {
107
+ // Cannot calculate usage without known context size
108
+ return
109
+ }
110
+
111
+ const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0)
112
+ const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0)
113
+
114
+ await connection
115
+ .sessionUpdate({
116
+ sessionId: sessionID,
117
+ update: {
118
+ sessionUpdate: "usage_update",
119
+ used,
120
+ size,
121
+ cost: { amount: totalCost, currency: "USD" },
122
+ },
123
+ })
124
+ .catch((error) => {
125
+ log.error("failed to send usage update", { error })
126
+ })
127
+ }
128
+
129
+ export async function init({ sdk: _sdk }: { sdk: IroderClient }) {
130
+ return {
131
+ create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
132
+ return new Agent(connection, fullConfig)
133
+ },
134
+ }
135
+ }
136
+
137
+ export class Agent implements ACPAgent {
138
+ private connection: AgentSideConnection
139
+ private config: ACPConfig
140
+ private sdk: IroderClient
141
+ private sessionManager: ACPSessionManager
142
+ private eventAbort = new AbortController()
143
+ private eventStarted = false
144
+ private bashSnapshots = new Map<string, string>()
145
+ private toolStarts = new Set<string>()
146
+ private permissionQueues = new Map<string, Promise<void>>()
147
+ private permissionOptions: PermissionOption[] = [
148
+ { optionId: "once", kind: "allow_once", name: "Allow once" },
149
+ { optionId: "always", kind: "allow_always", name: "Always allow" },
150
+ { optionId: "reject", kind: "reject_once", name: "Reject" },
151
+ ]
152
+
153
+ constructor(connection: AgentSideConnection, config: ACPConfig) {
154
+ this.connection = connection
155
+ this.config = config
156
+ this.sdk = config.sdk
157
+ this.sessionManager = new ACPSessionManager(this.sdk)
158
+ this.startEventSubscription()
159
+ }
160
+
161
+ private startEventSubscription() {
162
+ if (this.eventStarted) return
163
+ this.eventStarted = true
164
+ this.runEventSubscription().catch((error) => {
165
+ if (this.eventAbort.signal.aborted) return
166
+ log.error("event subscription failed", { error })
167
+ })
168
+ }
169
+
170
+ private async runEventSubscription() {
171
+ while (true) {
172
+ if (this.eventAbort.signal.aborted) return
173
+ const events = await this.sdk.global.event({
174
+ signal: this.eventAbort.signal,
175
+ })
176
+ for await (const event of events.stream) {
177
+ if (this.eventAbort.signal.aborted) return
178
+ const payload = (event as any)?.payload
179
+ if (!payload) continue
180
+ await this.handleEvent(payload as Event).catch((error) => {
181
+ log.error("failed to handle event", { error, type: payload.type })
182
+ })
183
+ }
184
+ }
185
+ }
186
+
187
+ private async handleEvent(event: Event) {
188
+ switch (event.type) {
189
+ case "permission.asked": {
190
+ const permission = event.properties
191
+ const session = this.sessionManager.tryGet(permission.sessionID)
192
+ if (!session) return
193
+
194
+ const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve()
195
+ const next = prev
196
+ .then(async () => {
197
+ const directory = session.cwd
198
+
199
+ const res = await this.connection
200
+ .requestPermission({
201
+ sessionId: permission.sessionID,
202
+ toolCall: {
203
+ toolCallId: permission.tool?.callID ?? permission.id,
204
+ status: "pending",
205
+ title: permission.permission,
206
+ rawInput: permission.metadata,
207
+ kind: toToolKind(permission.permission),
208
+ locations: toLocations(permission.permission, permission.metadata),
209
+ },
210
+ options: this.permissionOptions,
211
+ })
212
+ .catch(async (error) => {
213
+ log.error("failed to request permission from ACP", {
214
+ error,
215
+ permissionID: permission.id,
216
+ sessionID: permission.sessionID,
217
+ })
218
+ await this.sdk.permission.reply({
219
+ requestID: permission.id,
220
+ reply: "reject",
221
+ directory,
222
+ })
223
+ return undefined
224
+ })
225
+
226
+ if (!res) return
227
+ if (res.outcome.outcome !== "selected") {
228
+ await this.sdk.permission.reply({
229
+ requestID: permission.id,
230
+ reply: "reject",
231
+ directory,
232
+ })
233
+ return
234
+ }
235
+
236
+ if (res.outcome.optionId !== "reject" && permission.permission == "edit") {
237
+ const metadata = permission.metadata || {}
238
+ const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
239
+ const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
240
+ const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : ""
241
+ const newContent = getNewContent(content, diff)
242
+
243
+ if (newContent) {
244
+ this.connection.writeTextFile({
245
+ sessionId: session.id,
246
+ path: filepath,
247
+ content: newContent,
248
+ })
249
+ }
250
+ }
251
+
252
+ await this.sdk.permission.reply({
253
+ requestID: permission.id,
254
+ reply: res.outcome.optionId as "once" | "always" | "reject",
255
+ directory,
256
+ })
257
+ })
258
+ .catch((error) => {
259
+ log.error("failed to handle permission", { error, permissionID: permission.id })
260
+ })
261
+ .finally(() => {
262
+ if (this.permissionQueues.get(permission.sessionID) === next) {
263
+ this.permissionQueues.delete(permission.sessionID)
264
+ }
265
+ })
266
+ this.permissionQueues.set(permission.sessionID, next)
267
+ return
268
+ }
269
+
270
+ case "message.part.updated": {
271
+ log.info("message part updated", { event: event.properties })
272
+ const props = event.properties
273
+ const part = props.part
274
+ const session = this.sessionManager.tryGet(part.sessionID)
275
+ if (!session) return
276
+ const sessionId = session.id
277
+
278
+ if (part.type === "tool") {
279
+ await this.toolStart(sessionId, part)
280
+
281
+ switch (part.state.status) {
282
+ case "pending":
283
+ this.bashSnapshots.delete(part.callID)
284
+ return
285
+
286
+ case "running":
287
+ const output = this.bashOutput(part)
288
+ const content: ToolCallContent[] = []
289
+ if (output) {
290
+ const hash = Hash.fast(output)
291
+ if (part.tool === "bash") {
292
+ if (this.bashSnapshots.get(part.callID) === hash) {
293
+ await this.connection
294
+ .sessionUpdate({
295
+ sessionId,
296
+ update: {
297
+ sessionUpdate: "tool_call_update",
298
+ toolCallId: part.callID,
299
+ status: "in_progress",
300
+ kind: toToolKind(part.tool),
301
+ title: part.tool,
302
+ locations: toLocations(part.tool, part.state.input),
303
+ rawInput: part.state.input,
304
+ },
305
+ })
306
+ .catch((error) => {
307
+ log.error("failed to send tool in_progress to ACP", { error })
308
+ })
309
+ return
310
+ }
311
+ this.bashSnapshots.set(part.callID, hash)
312
+ }
313
+ content.push({
314
+ type: "content",
315
+ content: {
316
+ type: "text",
317
+ text: output,
318
+ },
319
+ })
320
+ }
321
+ await this.connection
322
+ .sessionUpdate({
323
+ sessionId,
324
+ update: {
325
+ sessionUpdate: "tool_call_update",
326
+ toolCallId: part.callID,
327
+ status: "in_progress",
328
+ kind: toToolKind(part.tool),
329
+ title: part.tool,
330
+ locations: toLocations(part.tool, part.state.input),
331
+ rawInput: part.state.input,
332
+ ...(content.length > 0 && { content }),
333
+ },
334
+ })
335
+ .catch((error) => {
336
+ log.error("failed to send tool in_progress to ACP", { error })
337
+ })
338
+ return
339
+
340
+ case "completed": {
341
+ this.toolStarts.delete(part.callID)
342
+ this.bashSnapshots.delete(part.callID)
343
+ const kind = toToolKind(part.tool)
344
+ const content: ToolCallContent[] = [
345
+ {
346
+ type: "content",
347
+ content: {
348
+ type: "text",
349
+ text: part.state.output,
350
+ },
351
+ },
352
+ ]
353
+
354
+ if (kind === "edit") {
355
+ const input = part.state.input
356
+ const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
357
+ const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
358
+ const newText =
359
+ typeof input["newString"] === "string"
360
+ ? input["newString"]
361
+ : typeof input["content"] === "string"
362
+ ? input["content"]
363
+ : ""
364
+ content.push({
365
+ type: "diff",
366
+ path: filePath,
367
+ oldText,
368
+ newText,
369
+ })
370
+ }
371
+
372
+ if (part.tool === "todowrite") {
373
+ const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
374
+ if (parsedTodos.success) {
375
+ await this.connection
376
+ .sessionUpdate({
377
+ sessionId,
378
+ update: {
379
+ sessionUpdate: "plan",
380
+ entries: parsedTodos.data.map((todo) => {
381
+ const status: PlanEntry["status"] =
382
+ todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
383
+ return {
384
+ priority: "medium",
385
+ status,
386
+ content: todo.content,
387
+ }
388
+ }),
389
+ },
390
+ })
391
+ .catch((error) => {
392
+ log.error("failed to send session update for todo", { error })
393
+ })
394
+ } else {
395
+ log.error("failed to parse todo output", { error: parsedTodos.error })
396
+ }
397
+ }
398
+
399
+ await this.connection
400
+ .sessionUpdate({
401
+ sessionId,
402
+ update: {
403
+ sessionUpdate: "tool_call_update",
404
+ toolCallId: part.callID,
405
+ status: "completed",
406
+ kind,
407
+ content,
408
+ title: part.state.title,
409
+ rawInput: part.state.input,
410
+ rawOutput: {
411
+ output: part.state.output,
412
+ metadata: part.state.metadata,
413
+ },
414
+ },
415
+ })
416
+ .catch((error) => {
417
+ log.error("failed to send tool completed to ACP", { error })
418
+ })
419
+ return
420
+ }
421
+ case "error":
422
+ this.toolStarts.delete(part.callID)
423
+ this.bashSnapshots.delete(part.callID)
424
+ await this.connection
425
+ .sessionUpdate({
426
+ sessionId,
427
+ update: {
428
+ sessionUpdate: "tool_call_update",
429
+ toolCallId: part.callID,
430
+ status: "failed",
431
+ kind: toToolKind(part.tool),
432
+ title: part.tool,
433
+ rawInput: part.state.input,
434
+ content: [
435
+ {
436
+ type: "content",
437
+ content: {
438
+ type: "text",
439
+ text: part.state.error,
440
+ },
441
+ },
442
+ ],
443
+ rawOutput: {
444
+ error: part.state.error,
445
+ metadata: part.state.metadata,
446
+ },
447
+ },
448
+ })
449
+ .catch((error) => {
450
+ log.error("failed to send tool error to ACP", { error })
451
+ })
452
+ return
453
+ }
454
+ }
455
+ if (part.type !== "text" && part.type !== "file") return
456
+ const msg = await this.sdk.session
457
+ .message(
458
+ { sessionID: part.sessionID, messageID: part.messageID, directory: session.cwd },
459
+ { throwOnError: true },
460
+ )
461
+ .then((x) => x.data)
462
+ .catch((err) => {
463
+ log.error("failed to fetch message for user chunk", { error: err })
464
+ return undefined
465
+ })
466
+ if (!msg || msg.info.role !== "user") return
467
+ await this.processMessage({ info: msg.info, parts: [part] })
468
+ return
469
+ }
470
+
471
+ case "message.part.delta": {
472
+ const props = event.properties
473
+ const session = this.sessionManager.tryGet(props.sessionID)
474
+ if (!session) return
475
+ const sessionId = session.id
476
+
477
+ const message = await this.sdk.session
478
+ .message(
479
+ {
480
+ sessionID: props.sessionID,
481
+ messageID: props.messageID,
482
+ directory: session.cwd,
483
+ },
484
+ { throwOnError: true },
485
+ )
486
+ .then((x) => x.data)
487
+ .catch((error) => {
488
+ log.error("unexpected error when fetching message", { error })
489
+ return undefined
490
+ })
491
+
492
+ if (!message || message.info.role !== "assistant") return
493
+
494
+ const part = message.parts.find((p) => p.id === props.partID)
495
+ if (!part) return
496
+
497
+ if (part.type === "text" && props.field === "text" && part.ignored !== true) {
498
+ await this.connection
499
+ .sessionUpdate({
500
+ sessionId,
501
+ update: {
502
+ sessionUpdate: "agent_message_chunk",
503
+ messageId: props.messageID,
504
+ content: {
505
+ type: "text",
506
+ text: props.delta,
507
+ },
508
+ },
509
+ })
510
+ .catch((error) => {
511
+ log.error("failed to send text delta to ACP", { error })
512
+ })
513
+ return
514
+ }
515
+
516
+ if (part.type === "reasoning" && props.field === "text") {
517
+ await this.connection
518
+ .sessionUpdate({
519
+ sessionId,
520
+ update: {
521
+ sessionUpdate: "agent_thought_chunk",
522
+ messageId: props.messageID,
523
+ content: {
524
+ type: "text",
525
+ text: props.delta,
526
+ },
527
+ },
528
+ })
529
+ .catch((error) => {
530
+ log.error("failed to send reasoning delta to ACP", { error })
531
+ })
532
+ }
533
+ return
534
+ }
535
+ }
536
+ }
537
+
538
+ async initialize(params: InitializeRequest): Promise<InitializeResponse> {
539
+ log.info("initialize", { protocolVersion: params.protocolVersion })
540
+
541
+ const authMethod: AuthMethod = {
542
+ description: "Run `iroder auth login` in the terminal",
543
+ name: "Login with iroder",
544
+ id: "iroder-login",
545
+ }
546
+
547
+ // If client supports terminal-auth capability, use that instead.
548
+ if (params.clientCapabilities?._meta?.["terminal-auth"] === true) {
549
+ authMethod._meta = {
550
+ "terminal-auth": {
551
+ command: "iroder",
552
+ args: ["auth", "login"],
553
+ label: "Iroder Login",
554
+ },
555
+ }
556
+ }
557
+
558
+ return {
559
+ protocolVersion: 1,
560
+ agentCapabilities: {
561
+ loadSession: true,
562
+ mcpCapabilities: {
563
+ http: true,
564
+ sse: true,
565
+ },
566
+ promptCapabilities: {
567
+ embeddedContext: true,
568
+ image: true,
569
+ },
570
+ sessionCapabilities: {
571
+ fork: {},
572
+ list: {},
573
+ resume: {},
574
+ },
575
+ },
576
+ authMethods: [authMethod],
577
+ agentInfo: {
578
+ name: "Iroder",
579
+ version: Installation.VERSION,
580
+ },
581
+ }
582
+ }
583
+
584
+ async authenticate(_params: AuthenticateRequest) {
585
+ throw new Error("Authentication not implemented")
586
+ }
587
+
588
+ async newSession(params: NewSessionRequest) {
589
+ const directory = params.cwd
590
+ try {
591
+ const model = await defaultModel(this.config, directory)
592
+
593
+ // Store ACP session state
594
+ const state = await this.sessionManager.create(params.cwd, params.mcpServers, model)
595
+ const sessionId = state.id
596
+
597
+ log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
598
+
599
+ const load = await this.loadSessionMode({
600
+ cwd: directory,
601
+ mcpServers: params.mcpServers,
602
+ sessionId,
603
+ })
604
+
605
+ return {
606
+ sessionId,
607
+ configOptions: load.configOptions,
608
+ models: load.models,
609
+ modes: load.modes,
610
+ _meta: load._meta,
611
+ }
612
+ } catch (e) {
613
+ const error = MessageV2.fromError(e, {
614
+ providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
615
+ })
616
+ if (LoadAPIKeyError.isInstance(error)) {
617
+ throw RequestError.authRequired()
618
+ }
619
+ throw e
620
+ }
621
+ }
622
+
623
+ async loadSession(params: LoadSessionRequest) {
624
+ const directory = params.cwd
625
+ const sessionId = params.sessionId
626
+
627
+ try {
628
+ const model = await defaultModel(this.config, directory)
629
+
630
+ // Store ACP session state
631
+ await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
632
+
633
+ log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
634
+
635
+ const result = await this.loadSessionMode({
636
+ cwd: directory,
637
+ mcpServers: params.mcpServers,
638
+ sessionId,
639
+ })
640
+
641
+ // Replay session history
642
+ const messages = await this.sdk.session
643
+ .messages(
644
+ {
645
+ sessionID: sessionId,
646
+ directory,
647
+ },
648
+ { throwOnError: true },
649
+ )
650
+ .then((x) => x.data)
651
+ .catch((err) => {
652
+ log.error("unexpected error when fetching message", { error: err })
653
+ return undefined
654
+ })
655
+
656
+ const lastUser = messages?.findLast((m) => m.info.role === "user")?.info
657
+ if (lastUser?.role === "user") {
658
+ result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}`
659
+ this.sessionManager.setModel(sessionId, {
660
+ providerID: ProviderID.make(lastUser.model.providerID),
661
+ modelID: ModelID.make(lastUser.model.modelID),
662
+ })
663
+ if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) {
664
+ result.modes.currentModeId = lastUser.agent
665
+ this.sessionManager.setMode(sessionId, lastUser.agent)
666
+ }
667
+ result.configOptions = buildConfigOptions({
668
+ currentModelId: result.models.currentModelId,
669
+ availableModels: result.models.availableModels,
670
+ modes: result.modes,
671
+ })
672
+ }
673
+
674
+ for (const msg of messages ?? []) {
675
+ log.debug("replay message", msg)
676
+ await this.processMessage(msg)
677
+ }
678
+
679
+ await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
680
+
681
+ return result
682
+ } catch (e) {
683
+ const error = MessageV2.fromError(e, {
684
+ providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
685
+ })
686
+ if (LoadAPIKeyError.isInstance(error)) {
687
+ throw RequestError.authRequired()
688
+ }
689
+ throw e
690
+ }
691
+ }
692
+
693
+ async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
694
+ try {
695
+ const cursor = params.cursor ? Number(params.cursor) : undefined
696
+ const limit = 100
697
+
698
+ const sessions = await this.sdk.session
699
+ .list(
700
+ {
701
+ directory: params.cwd ?? undefined,
702
+ roots: true,
703
+ },
704
+ { throwOnError: true },
705
+ )
706
+ .then((x) => x.data ?? [])
707
+
708
+ const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated)
709
+ const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted
710
+ const page = filtered.slice(0, limit)
711
+
712
+ const entries: SessionInfo[] = page.map((session) => ({
713
+ sessionId: session.id,
714
+ cwd: session.directory,
715
+ title: session.title,
716
+ updatedAt: new Date(session.time.updated).toISOString(),
717
+ }))
718
+
719
+ const last = page[page.length - 1]
720
+ const next = filtered.length > limit && last ? String(last.time.updated) : undefined
721
+
722
+ const response: ListSessionsResponse = {
723
+ sessions: entries,
724
+ }
725
+ if (next) response.nextCursor = next
726
+ return response
727
+ } catch (e) {
728
+ const error = MessageV2.fromError(e, {
729
+ providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
730
+ })
731
+ if (LoadAPIKeyError.isInstance(error)) {
732
+ throw RequestError.authRequired()
733
+ }
734
+ throw e
735
+ }
736
+ }
737
+
738
+ async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
739
+ const directory = params.cwd
740
+ const mcpServers = params.mcpServers ?? []
741
+
742
+ try {
743
+ const model = await defaultModel(this.config, directory)
744
+
745
+ const forked = await this.sdk.session
746
+ .fork(
747
+ {
748
+ sessionID: params.sessionId,
749
+ directory,
750
+ },
751
+ { throwOnError: true },
752
+ )
753
+ .then((x) => x.data)
754
+
755
+ if (!forked) {
756
+ throw new Error("Fork session returned no data")
757
+ }
758
+
759
+ const sessionId = forked.id
760
+ await this.sessionManager.load(sessionId, directory, mcpServers, model)
761
+
762
+ log.info("fork_session", { sessionId, mcpServers: mcpServers.length })
763
+
764
+ const mode = await this.loadSessionMode({
765
+ cwd: directory,
766
+ mcpServers,
767
+ sessionId,
768
+ })
769
+
770
+ const messages = await this.sdk.session
771
+ .messages(
772
+ {
773
+ sessionID: sessionId,
774
+ directory,
775
+ },
776
+ { throwOnError: true },
777
+ )
778
+ .then((x) => x.data)
779
+ .catch((err) => {
780
+ log.error("unexpected error when fetching message", { error: err })
781
+ return undefined
782
+ })
783
+
784
+ for (const msg of messages ?? []) {
785
+ log.debug("replay message", msg)
786
+ await this.processMessage(msg)
787
+ }
788
+
789
+ await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
790
+
791
+ return mode
792
+ } catch (e) {
793
+ const error = MessageV2.fromError(e, {
794
+ providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
795
+ })
796
+ if (LoadAPIKeyError.isInstance(error)) {
797
+ throw RequestError.authRequired()
798
+ }
799
+ throw e
800
+ }
801
+ }
802
+
803
+ async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
804
+ const directory = params.cwd
805
+ const sessionId = params.sessionId
806
+ const mcpServers = params.mcpServers ?? []
807
+
808
+ try {
809
+ const model = await defaultModel(this.config, directory)
810
+ await this.sessionManager.load(sessionId, directory, mcpServers, model)
811
+
812
+ log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
813
+
814
+ const result = await this.loadSessionMode({
815
+ cwd: directory,
816
+ mcpServers,
817
+ sessionId,
818
+ })
819
+
820
+ await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
821
+
822
+ return result
823
+ } catch (e) {
824
+ const error = MessageV2.fromError(e, {
825
+ providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
826
+ })
827
+ if (LoadAPIKeyError.isInstance(error)) {
828
+ throw RequestError.authRequired()
829
+ }
830
+ throw e
831
+ }
832
+ }
833
+
834
+ private async processMessage(message: SessionMessageResponse) {
835
+ log.debug("process message", message)
836
+ if (message.info.role !== "assistant" && message.info.role !== "user") return
837
+ const sessionId = message.info.sessionID
838
+
839
+ for (const part of message.parts) {
840
+ if (part.type === "tool") {
841
+ await this.toolStart(sessionId, part)
842
+ switch (part.state.status) {
843
+ case "pending":
844
+ this.bashSnapshots.delete(part.callID)
845
+ break
846
+ case "running":
847
+ const output = this.bashOutput(part)
848
+ const runningContent: ToolCallContent[] = []
849
+ if (output) {
850
+ runningContent.push({
851
+ type: "content",
852
+ content: {
853
+ type: "text",
854
+ text: output,
855
+ },
856
+ })
857
+ }
858
+ await this.connection
859
+ .sessionUpdate({
860
+ sessionId,
861
+ update: {
862
+ sessionUpdate: "tool_call_update",
863
+ toolCallId: part.callID,
864
+ status: "in_progress",
865
+ kind: toToolKind(part.tool),
866
+ title: part.tool,
867
+ locations: toLocations(part.tool, part.state.input),
868
+ rawInput: part.state.input,
869
+ ...(runningContent.length > 0 && { content: runningContent }),
870
+ },
871
+ })
872
+ .catch((err) => {
873
+ log.error("failed to send tool in_progress to ACP", { error: err })
874
+ })
875
+ break
876
+ case "completed":
877
+ this.toolStarts.delete(part.callID)
878
+ this.bashSnapshots.delete(part.callID)
879
+ const kind = toToolKind(part.tool)
880
+ const content: ToolCallContent[] = [
881
+ {
882
+ type: "content",
883
+ content: {
884
+ type: "text",
885
+ text: part.state.output,
886
+ },
887
+ },
888
+ ]
889
+
890
+ if (kind === "edit") {
891
+ const input = part.state.input
892
+ const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
893
+ const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
894
+ const newText =
895
+ typeof input["newString"] === "string"
896
+ ? input["newString"]
897
+ : typeof input["content"] === "string"
898
+ ? input["content"]
899
+ : ""
900
+ content.push({
901
+ type: "diff",
902
+ path: filePath,
903
+ oldText,
904
+ newText,
905
+ })
906
+ }
907
+
908
+ if (part.tool === "todowrite") {
909
+ const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
910
+ if (parsedTodos.success) {
911
+ await this.connection
912
+ .sessionUpdate({
913
+ sessionId,
914
+ update: {
915
+ sessionUpdate: "plan",
916
+ entries: parsedTodos.data.map((todo) => {
917
+ const status: PlanEntry["status"] =
918
+ todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
919
+ return {
920
+ priority: "medium",
921
+ status,
922
+ content: todo.content,
923
+ }
924
+ }),
925
+ },
926
+ })
927
+ .catch((err) => {
928
+ log.error("failed to send session update for todo", { error: err })
929
+ })
930
+ } else {
931
+ log.error("failed to parse todo output", { error: parsedTodos.error })
932
+ }
933
+ }
934
+
935
+ await this.connection
936
+ .sessionUpdate({
937
+ sessionId,
938
+ update: {
939
+ sessionUpdate: "tool_call_update",
940
+ toolCallId: part.callID,
941
+ status: "completed",
942
+ kind,
943
+ content,
944
+ title: part.state.title,
945
+ rawInput: part.state.input,
946
+ rawOutput: {
947
+ output: part.state.output,
948
+ metadata: part.state.metadata,
949
+ },
950
+ },
951
+ })
952
+ .catch((err) => {
953
+ log.error("failed to send tool completed to ACP", { error: err })
954
+ })
955
+ break
956
+ case "error":
957
+ this.toolStarts.delete(part.callID)
958
+ this.bashSnapshots.delete(part.callID)
959
+ await this.connection
960
+ .sessionUpdate({
961
+ sessionId,
962
+ update: {
963
+ sessionUpdate: "tool_call_update",
964
+ toolCallId: part.callID,
965
+ status: "failed",
966
+ kind: toToolKind(part.tool),
967
+ title: part.tool,
968
+ rawInput: part.state.input,
969
+ content: [
970
+ {
971
+ type: "content",
972
+ content: {
973
+ type: "text",
974
+ text: part.state.error,
975
+ },
976
+ },
977
+ ],
978
+ rawOutput: {
979
+ error: part.state.error,
980
+ metadata: part.state.metadata,
981
+ },
982
+ },
983
+ })
984
+ .catch((err) => {
985
+ log.error("failed to send tool error to ACP", { error: err })
986
+ })
987
+ break
988
+ }
989
+ } else if (part.type === "text") {
990
+ if (part.text) {
991
+ const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined
992
+ await this.connection
993
+ .sessionUpdate({
994
+ sessionId,
995
+ update: {
996
+ sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk",
997
+ messageId: message.info.id,
998
+ content: {
999
+ type: "text",
1000
+ text: part.text,
1001
+ ...(audience && { annotations: { audience } }),
1002
+ },
1003
+ },
1004
+ })
1005
+ .catch((err) => {
1006
+ log.error("failed to send text to ACP", { error: err })
1007
+ })
1008
+ }
1009
+ } else if (part.type === "file") {
1010
+ // Replay file attachments as appropriate ACP content blocks.
1011
+ // Iroder stores files internally as { type: "file", url, filename, mime }.
1012
+ // We convert these back to ACP blocks based on the URL scheme and MIME type:
1013
+ // - file:// URLs → resource_link
1014
+ // - data: URLs with image/* → image block
1015
+ // - data: URLs with text/* or application/json → resource with text
1016
+ // - data: URLs with other types → resource with blob
1017
+ const url = part.url
1018
+ const filename = part.filename ?? "file"
1019
+ const mime = part.mime || "application/octet-stream"
1020
+ const messageChunk = message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk"
1021
+
1022
+ if (url.startsWith("file://")) {
1023
+ // Local file reference - send as resource_link
1024
+ await this.connection
1025
+ .sessionUpdate({
1026
+ sessionId,
1027
+ update: {
1028
+ sessionUpdate: messageChunk,
1029
+ messageId: message.info.id,
1030
+ content: { type: "resource_link", uri: url, name: filename, mimeType: mime },
1031
+ },
1032
+ })
1033
+ .catch((err) => {
1034
+ log.error("failed to send resource_link to ACP", { error: err })
1035
+ })
1036
+ } else if (url.startsWith("data:")) {
1037
+ // Embedded content - parse data URL and send as appropriate block type
1038
+ const base64Match = url.match(/^data:([^;]+);base64,(.*)$/)
1039
+ const dataMime = base64Match?.[1]
1040
+ const base64Data = base64Match?.[2] ?? ""
1041
+
1042
+ const effectiveMime = dataMime || mime
1043
+
1044
+ if (effectiveMime.startsWith("image/")) {
1045
+ // Image - send as image block
1046
+ await this.connection
1047
+ .sessionUpdate({
1048
+ sessionId,
1049
+ update: {
1050
+ sessionUpdate: messageChunk,
1051
+ messageId: message.info.id,
1052
+ content: {
1053
+ type: "image",
1054
+ mimeType: effectiveMime,
1055
+ data: base64Data,
1056
+ uri: pathToFileURL(filename).href,
1057
+ },
1058
+ },
1059
+ })
1060
+ .catch((err) => {
1061
+ log.error("failed to send image to ACP", { error: err })
1062
+ })
1063
+ } else {
1064
+ // Non-image: text types get decoded, binary types stay as blob
1065
+ const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json"
1066
+ const fileUri = pathToFileURL(filename).href
1067
+ const resource = isText
1068
+ ? {
1069
+ uri: fileUri,
1070
+ mimeType: effectiveMime,
1071
+ text: Buffer.from(base64Data, "base64").toString("utf-8"),
1072
+ }
1073
+ : { uri: fileUri, mimeType: effectiveMime, blob: base64Data }
1074
+
1075
+ await this.connection
1076
+ .sessionUpdate({
1077
+ sessionId,
1078
+ update: {
1079
+ sessionUpdate: messageChunk,
1080
+ messageId: message.info.id,
1081
+ content: { type: "resource", resource },
1082
+ },
1083
+ })
1084
+ .catch((err) => {
1085
+ log.error("failed to send resource to ACP", { error: err })
1086
+ })
1087
+ }
1088
+ }
1089
+ // URLs that don't match file:// or data: are skipped (unsupported)
1090
+ } else if (part.type === "reasoning") {
1091
+ if (part.text) {
1092
+ await this.connection
1093
+ .sessionUpdate({
1094
+ sessionId,
1095
+ update: {
1096
+ sessionUpdate: "agent_thought_chunk",
1097
+ messageId: message.info.id,
1098
+ content: {
1099
+ type: "text",
1100
+ text: part.text,
1101
+ },
1102
+ },
1103
+ })
1104
+ .catch((err) => {
1105
+ log.error("failed to send reasoning to ACP", { error: err })
1106
+ })
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ private bashOutput(part: ToolPart) {
1113
+ if (part.tool !== "bash") return
1114
+ if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
1115
+ const output = part.state.metadata["output"]
1116
+ if (typeof output !== "string") return
1117
+ return output
1118
+ }
1119
+
1120
+ private async toolStart(sessionId: string, part: ToolPart) {
1121
+ if (this.toolStarts.has(part.callID)) return
1122
+ this.toolStarts.add(part.callID)
1123
+ await this.connection
1124
+ .sessionUpdate({
1125
+ sessionId,
1126
+ update: {
1127
+ sessionUpdate: "tool_call",
1128
+ toolCallId: part.callID,
1129
+ title: part.tool,
1130
+ kind: toToolKind(part.tool),
1131
+ status: "pending",
1132
+ locations: [],
1133
+ rawInput: {},
1134
+ },
1135
+ })
1136
+ .catch((error) => {
1137
+ log.error("failed to send tool pending to ACP", { error })
1138
+ })
1139
+ }
1140
+
1141
+ private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
1142
+ const agents = await this.config.sdk.app
1143
+ .agents(
1144
+ {
1145
+ directory,
1146
+ },
1147
+ { throwOnError: true },
1148
+ )
1149
+ .then((resp) => resp.data!)
1150
+
1151
+ return agents
1152
+ .filter((agent) => agent.mode !== "subagent" && !agent.hidden)
1153
+ .map((agent) => ({
1154
+ id: agent.name,
1155
+ name: agent.name,
1156
+ description: agent.description,
1157
+ }))
1158
+ }
1159
+
1160
+ private async resolveModeState(
1161
+ directory: string,
1162
+ sessionId: string,
1163
+ ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
1164
+ const availableModes = await this.loadAvailableModes(directory)
1165
+ const currentModeId =
1166
+ this.sessionManager.get(sessionId).modeId ||
1167
+ (await (async () => {
1168
+ if (!availableModes.length) return undefined
1169
+ const defaultAgentName = await AgentModule.defaultAgent()
1170
+ const resolvedModeId =
1171
+ availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
1172
+ this.sessionManager.setMode(sessionId, resolvedModeId)
1173
+ return resolvedModeId
1174
+ })())
1175
+
1176
+ return { availableModes, currentModeId }
1177
+ }
1178
+
1179
+ private async loadSessionMode(params: LoadSessionRequest) {
1180
+ const directory = params.cwd
1181
+ const model = await defaultModel(this.config, directory)
1182
+ const sessionId = params.sessionId
1183
+
1184
+ const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
1185
+ const entries = sortProvidersByName(providers)
1186
+ const availableVariants = modelVariantsFromProviders(entries, model)
1187
+ const currentVariant = this.sessionManager.getVariant(sessionId)
1188
+ if (currentVariant && !availableVariants.includes(currentVariant)) {
1189
+ this.sessionManager.setVariant(sessionId, undefined)
1190
+ }
1191
+ const availableModels = buildAvailableModels(entries, { includeVariants: true })
1192
+ const modeState = await this.resolveModeState(directory, sessionId)
1193
+ const currentModeId = modeState.currentModeId
1194
+ const modes = currentModeId
1195
+ ? {
1196
+ availableModes: modeState.availableModes,
1197
+ currentModeId,
1198
+ }
1199
+ : undefined
1200
+
1201
+ const commands = await this.config.sdk.command
1202
+ .list(
1203
+ {
1204
+ directory,
1205
+ },
1206
+ { throwOnError: true },
1207
+ )
1208
+ .then((resp) => resp.data!)
1209
+
1210
+ const availableCommands = commands.map((command) => ({
1211
+ name: command.name,
1212
+ description: command.description ?? "",
1213
+ }))
1214
+ const names = new Set(availableCommands.map((c) => c.name))
1215
+ if (!names.has("compact"))
1216
+ availableCommands.push({
1217
+ name: "compact",
1218
+ description: "compact the session",
1219
+ })
1220
+
1221
+ const mcpServers: Record<string, Config.Mcp> = {}
1222
+ for (const server of params.mcpServers) {
1223
+ if ("type" in server) {
1224
+ mcpServers[server.name] = {
1225
+ url: server.url,
1226
+ headers: server.headers.reduce<Record<string, string>>((acc, { name, value }) => {
1227
+ acc[name] = value
1228
+ return acc
1229
+ }, {}),
1230
+ type: "remote",
1231
+ }
1232
+ } else {
1233
+ mcpServers[server.name] = {
1234
+ type: "local",
1235
+ command: [server.command, ...server.args],
1236
+ environment: server.env.reduce<Record<string, string>>((acc, { name, value }) => {
1237
+ acc[name] = value
1238
+ return acc
1239
+ }, {}),
1240
+ }
1241
+ }
1242
+ }
1243
+
1244
+ await Promise.all(
1245
+ Object.entries(mcpServers).map(async ([key, mcp]) => {
1246
+ await this.sdk.mcp
1247
+ .add(
1248
+ {
1249
+ directory,
1250
+ name: key,
1251
+ config: mcp,
1252
+ },
1253
+ { throwOnError: true },
1254
+ )
1255
+ .catch((error) => {
1256
+ log.error("failed to add mcp server", { name: key, error })
1257
+ })
1258
+ }),
1259
+ )
1260
+
1261
+ setTimeout(() => {
1262
+ this.connection.sessionUpdate({
1263
+ sessionId,
1264
+ update: {
1265
+ sessionUpdate: "available_commands_update",
1266
+ availableCommands,
1267
+ },
1268
+ })
1269
+ }, 0)
1270
+
1271
+ return {
1272
+ sessionId,
1273
+ models: {
1274
+ currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
1275
+ availableModels,
1276
+ },
1277
+ modes,
1278
+ configOptions: buildConfigOptions({
1279
+ currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
1280
+ availableModels,
1281
+ modes,
1282
+ }),
1283
+ _meta: buildVariantMeta({
1284
+ model,
1285
+ variant: this.sessionManager.getVariant(sessionId),
1286
+ availableVariants,
1287
+ }),
1288
+ }
1289
+ }
1290
+
1291
+ async unstable_setSessionModel(params: SetSessionModelRequest) {
1292
+ const session = this.sessionManager.get(params.sessionId)
1293
+ const providers = await this.sdk.config
1294
+ .providers({ directory: session.cwd }, { throwOnError: true })
1295
+ .then((x) => x.data!.providers)
1296
+
1297
+ const selection = parseModelSelection(params.modelId, providers)
1298
+ this.sessionManager.setModel(session.id, selection.model)
1299
+ this.sessionManager.setVariant(session.id, selection.variant)
1300
+
1301
+ const entries = sortProvidersByName(providers)
1302
+ const availableVariants = modelVariantsFromProviders(entries, selection.model)
1303
+
1304
+ return {
1305
+ _meta: buildVariantMeta({
1306
+ model: selection.model,
1307
+ variant: selection.variant,
1308
+ availableVariants,
1309
+ }),
1310
+ }
1311
+ }
1312
+
1313
+ async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
1314
+ const session = this.sessionManager.get(params.sessionId)
1315
+ const availableModes = await this.loadAvailableModes(session.cwd)
1316
+ if (!availableModes.some((mode) => mode.id === params.modeId)) {
1317
+ throw new Error(`Agent not found: ${params.modeId}`)
1318
+ }
1319
+ this.sessionManager.setMode(params.sessionId, params.modeId)
1320
+ }
1321
+
1322
+ async setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse> {
1323
+ const session = this.sessionManager.get(params.sessionId)
1324
+ const providers = await this.sdk.config
1325
+ .providers({ directory: session.cwd }, { throwOnError: true })
1326
+ .then((x) => x.data!.providers)
1327
+ const entries = sortProvidersByName(providers)
1328
+
1329
+ if (params.configId === "model") {
1330
+ if (typeof params.value !== "string") throw RequestError.invalidParams("model value must be a string")
1331
+ const selection = parseModelSelection(params.value, providers)
1332
+ this.sessionManager.setModel(session.id, selection.model)
1333
+ this.sessionManager.setVariant(session.id, selection.variant)
1334
+ } else if (params.configId === "mode") {
1335
+ if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string")
1336
+ const availableModes = await this.loadAvailableModes(session.cwd)
1337
+ if (!availableModes.some((mode) => mode.id === params.value)) {
1338
+ throw RequestError.invalidParams(JSON.stringify({ error: `Mode not found: ${params.value}` }))
1339
+ }
1340
+ this.sessionManager.setMode(session.id, params.value)
1341
+ } else {
1342
+ throw RequestError.invalidParams(JSON.stringify({ error: `Unknown config option: ${params.configId}` }))
1343
+ }
1344
+
1345
+ const updatedSession = this.sessionManager.get(session.id)
1346
+ const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd))
1347
+ const availableVariants = modelVariantsFromProviders(entries, model)
1348
+ const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, true)
1349
+ const availableModels = buildAvailableModels(entries, { includeVariants: true })
1350
+ const modeState = await this.resolveModeState(session.cwd, session.id)
1351
+ const modes = modeState.currentModeId
1352
+ ? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId }
1353
+ : undefined
1354
+
1355
+ return {
1356
+ configOptions: buildConfigOptions({ currentModelId, availableModels, modes }),
1357
+ }
1358
+ }
1359
+
1360
+ async prompt(params: PromptRequest) {
1361
+ const sessionID = params.sessionId
1362
+ const session = this.sessionManager.get(sessionID)
1363
+ const directory = session.cwd
1364
+
1365
+ const current = session.model
1366
+ const model = current ?? (await defaultModel(this.config, directory))
1367
+ if (!current) {
1368
+ this.sessionManager.setModel(session.id, model)
1369
+ }
1370
+ const agent = session.modeId ?? (await AgentModule.defaultAgent())
1371
+
1372
+ const parts: Array<
1373
+ | { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
1374
+ | { type: "file"; url: string; filename: string; mime: string }
1375
+ > = []
1376
+ for (const part of params.prompt) {
1377
+ switch (part.type) {
1378
+ case "text":
1379
+ const audience = part.annotations?.audience
1380
+ const forAssistant = audience?.length === 1 && audience[0] === "assistant"
1381
+ const forUser = audience?.length === 1 && audience[0] === "user"
1382
+ parts.push({
1383
+ type: "text" as const,
1384
+ text: part.text,
1385
+ ...(forAssistant && { synthetic: true }),
1386
+ ...(forUser && { ignored: true }),
1387
+ })
1388
+ break
1389
+ case "image": {
1390
+ const parsed = parseUri(part.uri ?? "")
1391
+ const filename = parsed.type === "file" ? parsed.filename : "image"
1392
+ if (part.data) {
1393
+ parts.push({
1394
+ type: "file",
1395
+ url: `data:${part.mimeType};base64,${part.data}`,
1396
+ filename,
1397
+ mime: part.mimeType,
1398
+ })
1399
+ } else if (part.uri && part.uri.startsWith("http:")) {
1400
+ parts.push({
1401
+ type: "file",
1402
+ url: part.uri,
1403
+ filename,
1404
+ mime: part.mimeType,
1405
+ })
1406
+ }
1407
+ break
1408
+ }
1409
+
1410
+ case "resource_link":
1411
+ const parsed = parseUri(part.uri)
1412
+ // Use the name from resource_link if available
1413
+ if (part.name && parsed.type === "file") {
1414
+ parsed.filename = part.name
1415
+ }
1416
+ parts.push(parsed)
1417
+
1418
+ break
1419
+
1420
+ case "resource": {
1421
+ const resource = part.resource
1422
+ if ("text" in resource && resource.text) {
1423
+ parts.push({
1424
+ type: "text",
1425
+ text: resource.text,
1426
+ })
1427
+ } else if ("blob" in resource && resource.blob && resource.mimeType) {
1428
+ // Binary resource (PDFs, etc.): store as file part with data URL
1429
+ const parsed = parseUri(resource.uri ?? "")
1430
+ const filename = parsed.type === "file" ? parsed.filename : "file"
1431
+ parts.push({
1432
+ type: "file",
1433
+ url: `data:${resource.mimeType};base64,${resource.blob}`,
1434
+ filename,
1435
+ mime: resource.mimeType,
1436
+ })
1437
+ }
1438
+ break
1439
+ }
1440
+
1441
+ default:
1442
+ break
1443
+ }
1444
+ }
1445
+
1446
+ log.info("parts", { parts })
1447
+
1448
+ const cmd = (() => {
1449
+ const text = parts
1450
+ .filter((p): p is { type: "text"; text: string } => p.type === "text")
1451
+ .map((p) => p.text)
1452
+ .join("")
1453
+ .trim()
1454
+
1455
+ if (!text.startsWith("/")) return
1456
+
1457
+ const [name, ...rest] = text.slice(1).split(/\s+/)
1458
+ return { name, args: rest.join(" ").trim() }
1459
+ })()
1460
+
1461
+ const buildUsage = (msg: AssistantMessage): Usage => ({
1462
+ totalTokens:
1463
+ msg.tokens.input +
1464
+ msg.tokens.output +
1465
+ msg.tokens.reasoning +
1466
+ (msg.tokens.cache?.read ?? 0) +
1467
+ (msg.tokens.cache?.write ?? 0),
1468
+ inputTokens: msg.tokens.input,
1469
+ outputTokens: msg.tokens.output,
1470
+ thoughtTokens: msg.tokens.reasoning || undefined,
1471
+ cachedReadTokens: msg.tokens.cache?.read || undefined,
1472
+ cachedWriteTokens: msg.tokens.cache?.write || undefined,
1473
+ })
1474
+
1475
+ if (!cmd) {
1476
+ const response = await this.sdk.session.prompt({
1477
+ sessionID,
1478
+ model: {
1479
+ providerID: model.providerID,
1480
+ modelID: model.modelID,
1481
+ },
1482
+ variant: this.sessionManager.getVariant(sessionID),
1483
+ parts,
1484
+ agent,
1485
+ directory,
1486
+ })
1487
+ const msg = response.data?.info
1488
+
1489
+ await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
1490
+
1491
+ return {
1492
+ stopReason: "end_turn" as const,
1493
+ usage: msg ? buildUsage(msg) : undefined,
1494
+ _meta: {},
1495
+ }
1496
+ }
1497
+
1498
+ const command = await this.config.sdk.command
1499
+ .list({ directory }, { throwOnError: true })
1500
+ .then((x) => x.data!.find((c) => c.name === cmd.name))
1501
+ if (command) {
1502
+ const response = await this.sdk.session.command({
1503
+ sessionID,
1504
+ command: command.name,
1505
+ arguments: cmd.args,
1506
+ model: model.providerID + "/" + model.modelID,
1507
+ agent,
1508
+ directory,
1509
+ })
1510
+ const msg = response.data?.info
1511
+
1512
+ await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
1513
+
1514
+ return {
1515
+ stopReason: "end_turn" as const,
1516
+ usage: msg ? buildUsage(msg) : undefined,
1517
+ _meta: {},
1518
+ }
1519
+ }
1520
+
1521
+ switch (cmd.name) {
1522
+ case "compact":
1523
+ await this.config.sdk.session.summarize(
1524
+ {
1525
+ sessionID,
1526
+ directory,
1527
+ providerID: model.providerID,
1528
+ modelID: model.modelID,
1529
+ },
1530
+ { throwOnError: true },
1531
+ )
1532
+ break
1533
+ }
1534
+
1535
+ await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
1536
+
1537
+ return {
1538
+ stopReason: "end_turn" as const,
1539
+ _meta: {},
1540
+ }
1541
+ }
1542
+
1543
+ async cancel(params: CancelNotification) {
1544
+ const session = this.sessionManager.get(params.sessionId)
1545
+ await this.config.sdk.session.abort(
1546
+ {
1547
+ sessionID: params.sessionId,
1548
+ directory: session.cwd,
1549
+ },
1550
+ { throwOnError: true },
1551
+ )
1552
+ }
1553
+ }
1554
+
1555
+ function toToolKind(toolName: string): ToolKind {
1556
+ const tool = toolName.toLocaleLowerCase()
1557
+ switch (tool) {
1558
+ case "bash":
1559
+ return "execute"
1560
+ case "webfetch":
1561
+ return "fetch"
1562
+
1563
+ case "edit":
1564
+ case "patch":
1565
+ case "write":
1566
+ return "edit"
1567
+
1568
+ case "grep":
1569
+ case "glob":
1570
+ case "context7_resolve_library_id":
1571
+ case "context7_get_library_docs":
1572
+ return "search"
1573
+
1574
+ case "list":
1575
+ case "read":
1576
+ return "read"
1577
+
1578
+ default:
1579
+ return "other"
1580
+ }
1581
+ }
1582
+
1583
+ function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
1584
+ const tool = toolName.toLocaleLowerCase()
1585
+ switch (tool) {
1586
+ case "read":
1587
+ case "edit":
1588
+ case "write":
1589
+ return input["filePath"] ? [{ path: input["filePath"] }] : []
1590
+ case "glob":
1591
+ case "grep":
1592
+ return input["path"] ? [{ path: input["path"] }] : []
1593
+ case "bash":
1594
+ return []
1595
+ case "list":
1596
+ return input["path"] ? [{ path: input["path"] }] : []
1597
+ default:
1598
+ return []
1599
+ }
1600
+ }
1601
+
1602
+ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderID; modelID: ModelID }> {
1603
+ const sdk = config.sdk
1604
+ const configured = config.defaultModel
1605
+ if (configured) return configured
1606
+
1607
+ const directory = cwd ?? process.cwd()
1608
+
1609
+ const specified = await sdk.config
1610
+ .get({ directory }, { throwOnError: true })
1611
+ .then((resp) => {
1612
+ const cfg = resp.data
1613
+ if (!cfg || !cfg.model) return undefined
1614
+ return Provider.parseModel(cfg.model)
1615
+ })
1616
+ .catch((error) => {
1617
+ log.error("failed to load user config for default model", { error })
1618
+ return undefined
1619
+ })
1620
+
1621
+ const providers = await sdk.config
1622
+ .providers({ directory }, { throwOnError: true })
1623
+ .then((x) => x.data?.providers ?? [])
1624
+ .catch((error) => {
1625
+ log.error("failed to list providers for default model", { error })
1626
+ return []
1627
+ })
1628
+
1629
+ if (specified && providers.length) {
1630
+ const provider = providers.find((p) => p.id === specified.providerID)
1631
+ if (provider && provider.models[specified.modelID]) return specified
1632
+ }
1633
+
1634
+ if (specified && !providers.length) return specified
1635
+
1636
+ const iroderProvider = providers.find((p) => p.id === "iroder")
1637
+ if (iroderProvider) {
1638
+ if (iroderProvider.models["big-pickle"]) {
1639
+ return { providerID: ProviderID.iroder, modelID: ModelID.make("big-pickle") }
1640
+ }
1641
+ const [best] = Provider.sort(Object.values(iroderProvider.models))
1642
+ if (best) {
1643
+ return {
1644
+ providerID: ProviderID.make(best.providerID),
1645
+ modelID: ModelID.make(best.id),
1646
+ }
1647
+ }
1648
+ }
1649
+
1650
+ const models = providers.flatMap((p) => Object.values(p.models))
1651
+ const [best] = Provider.sort(models)
1652
+ if (best) {
1653
+ return {
1654
+ providerID: ProviderID.make(best.providerID),
1655
+ modelID: ModelID.make(best.id),
1656
+ }
1657
+ }
1658
+
1659
+ if (specified) return specified
1660
+
1661
+ return { providerID: ProviderID.iroder, modelID: ModelID.make("big-pickle") }
1662
+ }
1663
+
1664
+ function parseUri(
1665
+ uri: string,
1666
+ ): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
1667
+ try {
1668
+ if (uri.startsWith("file://")) {
1669
+ const path = uri.slice(7)
1670
+ const name = path.split("/").pop() || path
1671
+ return {
1672
+ type: "file",
1673
+ url: uri,
1674
+ filename: name,
1675
+ mime: "text/plain",
1676
+ }
1677
+ }
1678
+ if (uri.startsWith("zed://")) {
1679
+ const url = new URL(uri)
1680
+ const path = url.searchParams.get("path")
1681
+ if (path) {
1682
+ const name = path.split("/").pop() || path
1683
+ return {
1684
+ type: "file",
1685
+ url: pathToFileURL(path).href,
1686
+ filename: name,
1687
+ mime: "text/plain",
1688
+ }
1689
+ }
1690
+ }
1691
+ return {
1692
+ type: "text",
1693
+ text: uri,
1694
+ }
1695
+ } catch {
1696
+ return {
1697
+ type: "text",
1698
+ text: uri,
1699
+ }
1700
+ }
1701
+ }
1702
+
1703
+ function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined {
1704
+ const result = applyPatch(fileOriginal, unifiedDiff)
1705
+ if (result === false) {
1706
+ log.error("Failed to apply unified diff (context mismatch)")
1707
+ return undefined
1708
+ }
1709
+ return result
1710
+ }
1711
+
1712
+ function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
1713
+ return [...providers].sort((a, b) => {
1714
+ const nameA = a.name.toLowerCase()
1715
+ const nameB = b.name.toLowerCase()
1716
+ if (nameA < nameB) return -1
1717
+ if (nameA > nameB) return 1
1718
+ return 0
1719
+ })
1720
+ }
1721
+
1722
+ function modelVariantsFromProviders(
1723
+ providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
1724
+ model: { providerID: ProviderID; modelID: ModelID },
1725
+ ): string[] {
1726
+ const provider = providers.find((entry) => entry.id === model.providerID)
1727
+ if (!provider) return []
1728
+ const modelInfo = provider.models[model.modelID]
1729
+ if (!modelInfo?.variants) return []
1730
+ return Object.keys(modelInfo.variants)
1731
+ }
1732
+
1733
+ function buildAvailableModels(
1734
+ providers: Array<{ id: string; name: string; models: Record<string, any> }>,
1735
+ options: { includeVariants?: boolean } = {},
1736
+ ): ModelOption[] {
1737
+ const includeVariants = options.includeVariants ?? false
1738
+ return providers.flatMap((provider) => {
1739
+ const unsorted: Array<{ id: string; name: string; variants?: Record<string, any> }> = Object.values(
1740
+ provider.models,
1741
+ )
1742
+ const models = Provider.sort(unsorted)
1743
+ return models.flatMap((model) => {
1744
+ const base: ModelOption = {
1745
+ modelId: `${provider.id}/${model.id}`,
1746
+ name: `${provider.name}/${model.name}`,
1747
+ }
1748
+ if (!includeVariants || !model.variants) return [base]
1749
+ const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
1750
+ const variantOptions = variants.map((variant) => ({
1751
+ modelId: `${provider.id}/${model.id}/${variant}`,
1752
+ name: `${provider.name}/${model.name} (${variant})`,
1753
+ }))
1754
+ return [base, ...variantOptions]
1755
+ })
1756
+ })
1757
+ }
1758
+
1759
+ function formatModelIdWithVariant(
1760
+ model: { providerID: ProviderID; modelID: ModelID },
1761
+ variant: string | undefined,
1762
+ availableVariants: string[],
1763
+ includeVariant: boolean,
1764
+ ) {
1765
+ const base = `${model.providerID}/${model.modelID}`
1766
+ if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
1767
+ return `${base}/${variant}`
1768
+ }
1769
+
1770
+ function buildVariantMeta(input: {
1771
+ model: { providerID: ProviderID; modelID: ModelID }
1772
+ variant?: string
1773
+ availableVariants: string[]
1774
+ }) {
1775
+ return {
1776
+ iroder: {
1777
+ modelId: `${input.model.providerID}/${input.model.modelID}`,
1778
+ variant: input.variant ?? null,
1779
+ availableVariants: input.availableVariants,
1780
+ },
1781
+ }
1782
+ }
1783
+
1784
+ function parseModelSelection(
1785
+ modelId: string,
1786
+ providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
1787
+ ): { model: { providerID: ProviderID; modelID: ModelID }; variant?: string } {
1788
+ const parsed = Provider.parseModel(modelId)
1789
+ const provider = providers.find((p) => p.id === parsed.providerID)
1790
+ if (!provider) {
1791
+ return { model: parsed, variant: undefined }
1792
+ }
1793
+
1794
+ // Check if modelID exists directly
1795
+ if (provider.models[parsed.modelID]) {
1796
+ return { model: parsed, variant: undefined }
1797
+ }
1798
+
1799
+ // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high")
1800
+ const segments = parsed.modelID.split("/")
1801
+ if (segments.length > 1) {
1802
+ const candidateVariant = segments[segments.length - 1]
1803
+ const baseModelId = segments.slice(0, -1).join("/")
1804
+ const baseModelInfo = provider.models[baseModelId]
1805
+ if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
1806
+ return {
1807
+ model: { providerID: parsed.providerID, modelID: ModelID.make(baseModelId) },
1808
+ variant: candidateVariant,
1809
+ }
1810
+ }
1811
+ }
1812
+
1813
+ return { model: parsed, variant: undefined }
1814
+ }
1815
+
1816
+ function buildConfigOptions(input: {
1817
+ currentModelId: string
1818
+ availableModels: ModelOption[]
1819
+ modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined
1820
+ }): SessionConfigOption[] {
1821
+ const options: SessionConfigOption[] = [
1822
+ {
1823
+ id: "model",
1824
+ name: "Model",
1825
+ category: "model",
1826
+ type: "select",
1827
+ currentValue: input.currentModelId,
1828
+ options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })),
1829
+ },
1830
+ ]
1831
+ if (input.modes) {
1832
+ options.push({
1833
+ id: "mode",
1834
+ name: "Session Mode",
1835
+ category: "mode",
1836
+ type: "select",
1837
+ currentValue: input.modes.currentModeId,
1838
+ options: input.modes.availableModes.map((m) => ({
1839
+ value: m.id,
1840
+ name: m.name,
1841
+ ...(m.description ? { description: m.description } : {}),
1842
+ })),
1843
+ })
1844
+ }
1845
+ return options
1846
+ }
1847
+ }