@code0123/opencode-android-arm64 1.1.54 → 1.1.56

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