@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,776 @@
1
+ // /model command - Set the preferred model for this channel or session.
2
+ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
3
+ import crypto from 'node:crypto';
4
+ import { setChannelModel, setSessionModel, setSessionAgent, getChannelModel, getSessionModel, getSessionAgent, getChannelAgent, getThreadSession, getGlobalModel, setGlobalModel, getVariantCascade, } from '../database.js';
5
+ import { initializeOpencodeForDirectory } from '../opencode.js';
6
+ import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
7
+ import { getDefaultModel } from '../session-handler/model-utils.js';
8
+ import { getRuntime } from '../session-handler/thread-session-runtime.js';
9
+ import { getThinkingValuesForModel } from '../thinking-utils.js';
10
+ import { createLogger, LogPrefix } from '../logger.js';
11
+ import * as errore from 'errore';
12
+ import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js';
13
+ const modelLogger = createLogger(LogPrefix.MODEL);
14
+ // Store context by hash to avoid customId length limits (Discord max: 100 chars).
15
+ // Entries are TTL'd to prevent unbounded growth when users open /model and never
16
+ // interact with the select menu.
17
+ const MODEL_CONTEXT_TTL_MS = 10 * 60 * 1000;
18
+ const pendingModelContexts = new Map();
19
+ function setModelContext(contextHash, context) {
20
+ pendingModelContexts.set(contextHash, context);
21
+ setTimeout(() => {
22
+ pendingModelContexts.delete(contextHash);
23
+ }, MODEL_CONTEXT_TTL_MS).unref();
24
+ }
25
+ function parseModelId(modelString) {
26
+ const [providerID, ...modelParts] = modelString.split('/');
27
+ const modelID = modelParts.join('/');
28
+ if (providerID && modelID) {
29
+ return { providerID, modelID };
30
+ }
31
+ return undefined;
32
+ }
33
+ export async function ensureSessionPreferencesSnapshot({ sessionId, channelId, appId, getClient, agentOverride, modelOverride, force, }) {
34
+ const [sessionAgentPreference, sessionModelPreference] = await Promise.all([
35
+ getSessionAgent(sessionId),
36
+ getSessionModel(sessionId),
37
+ ]);
38
+ const shouldBootstrapSessionPreferences = force || (!sessionAgentPreference && !sessionModelPreference);
39
+ if (!shouldBootstrapSessionPreferences) {
40
+ return;
41
+ }
42
+ const bootstrappedAgent = agentOverride ||
43
+ sessionAgentPreference ||
44
+ (channelId ? await getChannelAgent(channelId) : undefined);
45
+ if (!sessionAgentPreference && bootstrappedAgent) {
46
+ await setSessionAgent(sessionId, bootstrappedAgent);
47
+ modelLogger.log(`[MODEL] Snapshotted session agent ${bootstrappedAgent} for session ${sessionId}`);
48
+ }
49
+ if (sessionModelPreference) {
50
+ return;
51
+ }
52
+ if (modelOverride) {
53
+ const parsedModelOverride = parseModelId(modelOverride);
54
+ if (parsedModelOverride) {
55
+ const bootstrappedVariant = await getVariantCascade({
56
+ sessionId,
57
+ channelId,
58
+ appId,
59
+ });
60
+ await setSessionModel({
61
+ sessionId,
62
+ modelId: modelOverride,
63
+ variant: bootstrappedVariant ?? null,
64
+ });
65
+ modelLogger.log(`[MODEL] Snapshotted explicit session model ${modelOverride} for session ${sessionId}`);
66
+ return;
67
+ }
68
+ modelLogger.warn(`[MODEL] Ignoring invalid explicit model override "${modelOverride}" for session ${sessionId}`);
69
+ }
70
+ const bootstrappedModel = await getCurrentModelInfo({
71
+ sessionId,
72
+ channelId,
73
+ appId,
74
+ agentPreference: bootstrappedAgent,
75
+ getClient,
76
+ });
77
+ if (bootstrappedModel.type === 'none') {
78
+ return;
79
+ }
80
+ const bootstrappedVariant = await getVariantCascade({
81
+ sessionId,
82
+ channelId,
83
+ appId,
84
+ });
85
+ await setSessionModel({
86
+ sessionId,
87
+ modelId: bootstrappedModel.model,
88
+ variant: bootstrappedVariant ?? null,
89
+ });
90
+ modelLogger.log(`[MODEL] Snapshotted session model ${bootstrappedModel.model} for session ${sessionId}`);
91
+ }
92
+ /**
93
+ * Get the current model info for a channel/session, including where it comes from.
94
+ * Priority: session > agent > channel > global > opencode default
95
+ */
96
+ export async function getCurrentModelInfo({ sessionId, channelId, appId, agentPreference, getClient, }) {
97
+ if (getClient instanceof Error) {
98
+ return { type: 'none' };
99
+ }
100
+ // 1. Check session model preference
101
+ if (sessionId) {
102
+ const sessionPref = await getSessionModel(sessionId);
103
+ if (sessionPref) {
104
+ const parsed = parseModelId(sessionPref.modelId);
105
+ if (parsed) {
106
+ return { type: 'session', model: sessionPref.modelId, ...parsed };
107
+ }
108
+ }
109
+ }
110
+ // 2. Check agent's configured model
111
+ const effectiveAgent = agentPreference ??
112
+ (sessionId
113
+ ? (await getSessionAgent(sessionId)) ||
114
+ (channelId ? await getChannelAgent(channelId) : undefined)
115
+ : channelId
116
+ ? await getChannelAgent(channelId)
117
+ : undefined);
118
+ if (effectiveAgent) {
119
+ const agentsResponse = await getClient().app.agents({});
120
+ if (agentsResponse.data) {
121
+ const agent = agentsResponse.data.find((a) => a.name === effectiveAgent);
122
+ if (agent?.model) {
123
+ const model = `${agent.model.providerID}/${agent.model.modelID}`;
124
+ return {
125
+ type: 'agent',
126
+ model,
127
+ providerID: agent.model.providerID,
128
+ modelID: agent.model.modelID,
129
+ agentName: effectiveAgent,
130
+ };
131
+ }
132
+ }
133
+ }
134
+ // 3. Check channel model preference
135
+ if (channelId) {
136
+ const channelPref = await getChannelModel(channelId);
137
+ if (channelPref) {
138
+ const parsed = parseModelId(channelPref.modelId);
139
+ if (parsed) {
140
+ return { type: 'channel', model: channelPref.modelId, ...parsed };
141
+ }
142
+ }
143
+ }
144
+ // 4. Check global model preference
145
+ if (appId) {
146
+ const globalPref = await getGlobalModel(appId);
147
+ if (globalPref) {
148
+ const parsed = parseModelId(globalPref.modelId);
149
+ if (parsed) {
150
+ return { type: 'global', model: globalPref.modelId, ...parsed };
151
+ }
152
+ }
153
+ }
154
+ // 5. Get opencode default (config > recent > provider default)
155
+ const defaultModel = await getDefaultModel({ getClient });
156
+ if (defaultModel) {
157
+ const model = `${defaultModel.providerID}/${defaultModel.modelID}`;
158
+ return {
159
+ type: defaultModel.source,
160
+ model,
161
+ providerID: defaultModel.providerID,
162
+ modelID: defaultModel.modelID,
163
+ };
164
+ }
165
+ return { type: 'none' };
166
+ }
167
+ /**
168
+ * Handle the /model slash command.
169
+ * Shows a select menu with available providers.
170
+ */
171
+ export async function handleModelCommand({ interaction, appId, }) {
172
+ modelLogger.log('[MODEL] handleModelCommand called');
173
+ // Defer reply immediately to avoid 3-second timeout
174
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
175
+ modelLogger.log('[MODEL] Deferred reply');
176
+ const channel = interaction.channel;
177
+ if (!channel) {
178
+ await interaction.editReply({
179
+ content: 'This command can only be used in a channel',
180
+ });
181
+ return;
182
+ }
183
+ // Determine if we're in a thread or text channel
184
+ const isThread = [
185
+ ChannelType.PublicThread,
186
+ ChannelType.PrivateThread,
187
+ ChannelType.AnnouncementThread,
188
+ ].includes(channel.type);
189
+ let projectDirectory;
190
+ let targetChannelId;
191
+ let sessionId;
192
+ if (isThread) {
193
+ const thread = channel;
194
+ // Parallelize: resolve metadata and session ID at the same time
195
+ const [textChannel, threadSessionId] = await Promise.all([
196
+ resolveTextChannel(thread),
197
+ getThreadSession(thread.id),
198
+ ]);
199
+ const metadata = await getKimakiMetadata(textChannel);
200
+ projectDirectory = metadata.projectDirectory;
201
+ targetChannelId = textChannel?.id || channel.id;
202
+ sessionId = threadSessionId;
203
+ }
204
+ else if (channel.type === ChannelType.GuildText) {
205
+ const textChannel = channel;
206
+ const metadata = await getKimakiMetadata(textChannel);
207
+ projectDirectory = metadata.projectDirectory;
208
+ targetChannelId = channel.id;
209
+ }
210
+ else {
211
+ await interaction.editReply({
212
+ content: 'This command can only be used in text channels or threads',
213
+ });
214
+ return;
215
+ }
216
+ if (!projectDirectory) {
217
+ await interaction.editReply({
218
+ content: 'This channel is not configured with a project directory',
219
+ });
220
+ return;
221
+ }
222
+ try {
223
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
224
+ if (getClient instanceof Error) {
225
+ await interaction.editReply({ content: getClient.message });
226
+ return;
227
+ }
228
+ const effectiveAppId = appId;
229
+ if (isThread && sessionId) {
230
+ await ensureSessionPreferencesSnapshot({
231
+ sessionId,
232
+ channelId: targetChannelId,
233
+ appId: effectiveAppId,
234
+ getClient,
235
+ });
236
+ }
237
+ // Parallelize: fetch providers, current model info, and variant cascade at the same time.
238
+ // getCurrentModelInfo does DB lookups first (fast) and only hits provider.list as fallback.
239
+ const [providersResponse, currentModelInfo, cascadeVariant] = await Promise.all([
240
+ getClient().provider.list({ directory: projectDirectory }),
241
+ getCurrentModelInfo({
242
+ sessionId,
243
+ channelId: targetChannelId,
244
+ appId: effectiveAppId,
245
+ getClient,
246
+ }),
247
+ getVariantCascade({
248
+ sessionId,
249
+ channelId: targetChannelId,
250
+ appId: effectiveAppId,
251
+ }),
252
+ ]);
253
+ if (!providersResponse.data) {
254
+ await interaction.editReply({
255
+ content: 'Failed to fetch providers',
256
+ });
257
+ return;
258
+ }
259
+ const { all: allProviders, connected } = providersResponse.data;
260
+ // Filter to only connected providers (have credentials)
261
+ const availableProviders = allProviders.filter((p) => {
262
+ return connected.includes(p.id);
263
+ });
264
+ if (availableProviders.length === 0) {
265
+ await interaction.editReply({
266
+ content: 'No providers with credentials found. Use `/login` to connect a provider and add credentials.',
267
+ });
268
+ return;
269
+ }
270
+ const currentModelText = (() => {
271
+ switch (currentModelInfo.type) {
272
+ case 'session':
273
+ return `**Current (this thread):** \`${currentModelInfo.model}\``;
274
+ case 'agent':
275
+ return `**Current (agent "${currentModelInfo.agentName}"):** \`${currentModelInfo.model}\``;
276
+ case 'channel':
277
+ return `**Current (channel override):** \`${currentModelInfo.model}\``;
278
+ case 'global':
279
+ return `**Current (global default):** \`${currentModelInfo.model}\``;
280
+ case 'opencode-config':
281
+ case 'opencode-recent':
282
+ case 'opencode-provider-default':
283
+ return `**Current (opencode default):** \`${currentModelInfo.model}\``;
284
+ case 'none':
285
+ return '**Current:** none';
286
+ }
287
+ })();
288
+ const variantText = (() => {
289
+ if (currentModelInfo.type === 'none' || !cascadeVariant) {
290
+ return '';
291
+ }
292
+ return `\n**Variant:** \`${cascadeVariant}\``;
293
+ })();
294
+ // Store context with a short hash key to avoid customId length limits.
295
+ const providerSelectHeader = `**Set Model Preference**\n${currentModelText}${variantText}\nSelect a provider:`;
296
+ const context = {
297
+ dir: projectDirectory,
298
+ channelId: targetChannelId,
299
+ sessionId: sessionId,
300
+ isThread: isThread,
301
+ thread: isThread ? channel : undefined,
302
+ appId,
303
+ providerSelectHeader,
304
+ };
305
+ const contextHash = crypto.randomBytes(8).toString('hex');
306
+ setModelContext(contextHash, context);
307
+ const allProviderOptions = [...availableProviders]
308
+ .sort((a, b) => a.name.localeCompare(b.name))
309
+ .map((provider) => {
310
+ const modelCount = Object.keys(provider.models || {}).length;
311
+ return {
312
+ label: provider.name.slice(0, 100),
313
+ value: provider.id,
314
+ description: `${modelCount} model${modelCount !== 1 ? 's' : ''} available`.slice(0, 100),
315
+ };
316
+ });
317
+ const { options } = buildPaginatedOptions({
318
+ allOptions: allProviderOptions,
319
+ page: 0,
320
+ });
321
+ const selectMenu = new StringSelectMenuBuilder()
322
+ .setCustomId(`model_provider:${contextHash}`)
323
+ .setPlaceholder('Select a provider')
324
+ .addOptions(options);
325
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
326
+ await interaction.editReply({
327
+ content: providerSelectHeader,
328
+ components: [actionRow],
329
+ });
330
+ }
331
+ catch (error) {
332
+ modelLogger.error('Error loading providers:', error);
333
+ await interaction.editReply({
334
+ content: `Failed to load providers: ${error instanceof Error ? error.message : 'Unknown error'}`,
335
+ });
336
+ }
337
+ }
338
+ /**
339
+ * Handle the provider select menu interaction.
340
+ * Shows a second select menu with models for the chosen provider.
341
+ */
342
+ export async function handleProviderSelectMenu(interaction) {
343
+ const customId = interaction.customId;
344
+ if (!customId.startsWith('model_provider:')) {
345
+ return;
346
+ }
347
+ // Defer update immediately to avoid timeout
348
+ await interaction.deferUpdate();
349
+ const contextHash = customId.replace('model_provider:', '');
350
+ const context = pendingModelContexts.get(contextHash);
351
+ if (!context) {
352
+ await interaction.editReply({
353
+ content: 'Selection expired. Please run /model again.',
354
+ components: [],
355
+ });
356
+ return;
357
+ }
358
+ const selectedProviderId = interaction.values[0];
359
+ if (!selectedProviderId) {
360
+ await interaction.editReply({
361
+ content: 'No provider selected',
362
+ components: [],
363
+ });
364
+ return;
365
+ }
366
+ // Handle pagination nav — re-render the same provider select with new page
367
+ const providerNavPage = parsePaginationValue(selectedProviderId);
368
+ if (providerNavPage !== undefined) {
369
+ context.providerPage = providerNavPage;
370
+ setModelContext(contextHash, context);
371
+ const getClient = await initializeOpencodeForDirectory(context.dir);
372
+ if (getClient instanceof Error) {
373
+ await interaction.editReply({ content: getClient.message, components: [] });
374
+ return;
375
+ }
376
+ const providersResponse = await getClient().provider.list({ directory: context.dir });
377
+ if (!providersResponse.data) {
378
+ await interaction.editReply({ content: 'Failed to fetch providers', components: [] });
379
+ return;
380
+ }
381
+ const { all: allProviders, connected } = providersResponse.data;
382
+ const availableProviders = allProviders.filter((p) => connected.includes(p.id));
383
+ const allProviderOptions = [...availableProviders]
384
+ .sort((a, b) => a.name.localeCompare(b.name))
385
+ .map((p) => {
386
+ const modelCount = Object.keys(p.models || {}).length;
387
+ return {
388
+ label: p.name.slice(0, 100),
389
+ value: p.id,
390
+ description: `${modelCount} model${modelCount !== 1 ? 's' : ''} available`.slice(0, 100),
391
+ };
392
+ });
393
+ const { options } = buildPaginatedOptions({ allOptions: allProviderOptions, page: providerNavPage });
394
+ const selectMenu = new StringSelectMenuBuilder()
395
+ .setCustomId(`model_provider:${contextHash}`)
396
+ .setPlaceholder('Select a provider')
397
+ .addOptions(options);
398
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
399
+ await interaction.editReply({
400
+ content: context.providerSelectHeader || `**Set Model Preference**\nSelect a provider:`,
401
+ components: [actionRow],
402
+ });
403
+ return;
404
+ }
405
+ try {
406
+ const getClient = await initializeOpencodeForDirectory(context.dir);
407
+ if (getClient instanceof Error) {
408
+ await interaction.editReply({
409
+ content: getClient.message,
410
+ components: [],
411
+ });
412
+ return;
413
+ }
414
+ const providersResponse = await getClient().provider.list({
415
+ directory: context.dir,
416
+ });
417
+ if (!providersResponse.data) {
418
+ await interaction.editReply({
419
+ content: 'Failed to fetch providers',
420
+ components: [],
421
+ });
422
+ return;
423
+ }
424
+ const provider = providersResponse.data.all.find((p) => p.id === selectedProviderId);
425
+ if (!provider) {
426
+ await interaction.editReply({
427
+ content: 'Provider not found',
428
+ components: [],
429
+ });
430
+ return;
431
+ }
432
+ const models = Object.entries(provider.models || {})
433
+ .map(([modelId, model]) => ({
434
+ id: modelId,
435
+ name: model.name,
436
+ releaseDate: model.release_date,
437
+ }))
438
+ .sort((a, b) => a.name.localeCompare(b.name));
439
+ if (models.length === 0) {
440
+ await interaction.editReply({
441
+ content: `No models available for ${provider.name}`,
442
+ components: [],
443
+ });
444
+ return;
445
+ }
446
+ // Update context with provider info and reuse the same hash
447
+ context.providerId = selectedProviderId;
448
+ context.providerName = provider.name;
449
+ context.modelPage = 0;
450
+ setModelContext(contextHash, context);
451
+ const allModelOptions = models.map((model) => {
452
+ const dateStr = model.releaseDate
453
+ ? new Date(model.releaseDate).toLocaleDateString()
454
+ : 'Unknown date';
455
+ return {
456
+ label: model.name.slice(0, 100),
457
+ value: model.id,
458
+ description: dateStr.slice(0, 100),
459
+ };
460
+ });
461
+ const { options } = buildPaginatedOptions({
462
+ allOptions: allModelOptions,
463
+ page: 0,
464
+ });
465
+ const selectMenu = new StringSelectMenuBuilder()
466
+ .setCustomId(`model_select:${contextHash}`)
467
+ .setPlaceholder('Select a model')
468
+ .addOptions(options);
469
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
470
+ await interaction.editReply({
471
+ content: `**Set Model Preference**\nProvider: **${provider.name}**\nSelect a model:`,
472
+ components: [actionRow],
473
+ });
474
+ }
475
+ catch (error) {
476
+ modelLogger.error('Error loading models:', error);
477
+ await interaction.editReply({
478
+ content: `Failed to load models: ${error instanceof Error ? error.message : 'Unknown error'}`,
479
+ components: [],
480
+ });
481
+ }
482
+ }
483
+ /**
484
+ * Handle the model select menu interaction.
485
+ * Stores the model preference in the database.
486
+ */
487
+ export async function handleModelSelectMenu(interaction) {
488
+ const customId = interaction.customId;
489
+ if (!customId.startsWith('model_select:')) {
490
+ return;
491
+ }
492
+ // Defer update immediately
493
+ await interaction.deferUpdate();
494
+ const contextHash = customId.replace('model_select:', '');
495
+ const context = pendingModelContexts.get(contextHash);
496
+ if (!context || !context.providerId || !context.providerName) {
497
+ await interaction.editReply({
498
+ content: 'Selection expired. Please run /model again.',
499
+ components: [],
500
+ });
501
+ return;
502
+ }
503
+ const selectedModelId = interaction.values[0];
504
+ if (!selectedModelId) {
505
+ await interaction.editReply({
506
+ content: 'No model selected',
507
+ components: [],
508
+ });
509
+ return;
510
+ }
511
+ // Handle pagination nav — re-render the same model select with new page
512
+ const modelNavPage = parsePaginationValue(selectedModelId);
513
+ if (modelNavPage !== undefined) {
514
+ context.modelPage = modelNavPage;
515
+ setModelContext(contextHash, context);
516
+ const getClient = await initializeOpencodeForDirectory(context.dir);
517
+ if (getClient instanceof Error) {
518
+ await interaction.editReply({ content: getClient.message, components: [] });
519
+ return;
520
+ }
521
+ const providersResponse = await getClient().provider.list({ directory: context.dir });
522
+ const provider = providersResponse.data?.all.find((p) => p.id === context.providerId);
523
+ if (!provider) {
524
+ await interaction.editReply({ content: 'Provider not found', components: [] });
525
+ return;
526
+ }
527
+ const allModelOptions = Object.entries(provider.models || {})
528
+ .map(([modelId, model]) => ({
529
+ label: model.name.slice(0, 100),
530
+ value: modelId,
531
+ description: (model.release_date
532
+ ? new Date(model.release_date).toLocaleDateString()
533
+ : 'Unknown date').slice(0, 100),
534
+ }))
535
+ .sort((a, b) => a.label.localeCompare(b.label));
536
+ const { options } = buildPaginatedOptions({ allOptions: allModelOptions, page: modelNavPage });
537
+ const selectMenu = new StringSelectMenuBuilder()
538
+ .setCustomId(`model_select:${contextHash}`)
539
+ .setPlaceholder('Select a model')
540
+ .addOptions(options);
541
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
542
+ await interaction.editReply({
543
+ content: `**Set Model Preference**\nProvider: **${context.providerName}**\nSelect a model:`,
544
+ components: [actionRow],
545
+ });
546
+ return;
547
+ }
548
+ // Build full model ID: provider_id/model_id
549
+ const fullModelId = `${context.providerId}/${selectedModelId}`;
550
+ try {
551
+ context.selectedModelId = fullModelId;
552
+ setModelContext(contextHash, context);
553
+ // Check if model has variants (thinking levels) - if so, show variant picker first
554
+ const getClient = await initializeOpencodeForDirectory(context.dir);
555
+ if (!(getClient instanceof Error)) {
556
+ const providersResponse = await getClient().provider.list({
557
+ directory: context.dir,
558
+ });
559
+ if (providersResponse.data) {
560
+ const variants = getThinkingValuesForModel({
561
+ providers: providersResponse.data.all,
562
+ providerId: context.providerId,
563
+ modelId: selectedModelId,
564
+ });
565
+ if (variants.length > 0) {
566
+ context.availableVariants = variants;
567
+ setModelContext(contextHash, context);
568
+ const variantOptions = [
569
+ {
570
+ label: 'None (default)',
571
+ value: '__none__',
572
+ description: 'Use the model without a specific thinking level',
573
+ },
574
+ ...variants.slice(0, 24).map((v) => ({
575
+ label: v.slice(0, 100),
576
+ value: v,
577
+ description: `Use ${v} thinking`.slice(0, 100),
578
+ })),
579
+ ];
580
+ const selectMenu = new StringSelectMenuBuilder()
581
+ .setCustomId(`model_variant:${contextHash}`)
582
+ .setPlaceholder('Select a thinking level')
583
+ .addOptions(variantOptions);
584
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
585
+ await interaction.editReply({
586
+ content: `**Set Model Preference**\nModel: **${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\nSelect a thinking level:`,
587
+ components: [actionRow],
588
+ });
589
+ return;
590
+ }
591
+ }
592
+ }
593
+ // No variants available - skip to scope
594
+ context.selectedVariant = null;
595
+ setModelContext(contextHash, context);
596
+ await showScopeMenu({ interaction, contextHash, context });
597
+ }
598
+ catch (error) {
599
+ modelLogger.error('Error saving model preference:', error);
600
+ await interaction.editReply({
601
+ content: `Failed to save model preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
602
+ components: [],
603
+ });
604
+ }
605
+ }
606
+ /**
607
+ * Handle the variant select menu interaction.
608
+ * Stores the selected variant and shows the scope menu.
609
+ */
610
+ export async function handleModelVariantSelectMenu(interaction) {
611
+ const customId = interaction.customId;
612
+ if (!customId.startsWith('model_variant:')) {
613
+ return;
614
+ }
615
+ await interaction.deferUpdate();
616
+ const contextHash = customId.replace('model_variant:', '');
617
+ const context = pendingModelContexts.get(contextHash);
618
+ if (!context || !context.selectedModelId) {
619
+ await interaction.editReply({
620
+ content: 'Selection expired. Please run /model again.',
621
+ components: [],
622
+ });
623
+ return;
624
+ }
625
+ const selectedValue = interaction.values[0];
626
+ if (!selectedValue) {
627
+ await interaction.editReply({
628
+ content: 'No variant selected',
629
+ components: [],
630
+ });
631
+ return;
632
+ }
633
+ context.selectedVariant = selectedValue === '__none__' ? null : selectedValue;
634
+ setModelContext(contextHash, context);
635
+ await showScopeMenu({ interaction, contextHash, context });
636
+ }
637
+ async function showScopeMenu({ interaction, contextHash, context, }) {
638
+ const modelId = context.selectedModelId;
639
+ const modelDisplay = modelId.split('/')[1] || modelId;
640
+ const variantSuffix = context.selectedVariant
641
+ ? ` (${context.selectedVariant})`
642
+ : '';
643
+ const scopeOptions = [
644
+ ...(context.isThread && context.sessionId
645
+ ? [
646
+ {
647
+ label: 'This session only',
648
+ value: 'session',
649
+ description: 'Override for this session only',
650
+ },
651
+ ]
652
+ : []),
653
+ {
654
+ label: 'This channel only',
655
+ value: 'channel',
656
+ description: 'Override for this channel only',
657
+ },
658
+ {
659
+ label: 'Global default',
660
+ value: 'global',
661
+ description: 'Set for this channel and as default for all others',
662
+ },
663
+ ];
664
+ const selectMenu = new StringSelectMenuBuilder()
665
+ .setCustomId(`model_scope:${contextHash}`)
666
+ .setPlaceholder('Apply to...')
667
+ .addOptions(scopeOptions);
668
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
669
+ await interaction.editReply({
670
+ content: `**Set Model Preference**\nModel: **${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nApply to:`,
671
+ components: [actionRow],
672
+ });
673
+ }
674
+ /**
675
+ * Handle the scope select menu interaction.
676
+ * Applies the model to either the channel or globally.
677
+ */
678
+ export async function handleModelScopeSelectMenu(interaction) {
679
+ const customId = interaction.customId;
680
+ if (!customId.startsWith('model_scope:')) {
681
+ return;
682
+ }
683
+ // Defer update immediately
684
+ await interaction.deferUpdate();
685
+ const contextHash = customId.replace('model_scope:', '');
686
+ const context = pendingModelContexts.get(contextHash);
687
+ if (!context ||
688
+ !context.providerId ||
689
+ !context.providerName ||
690
+ !context.selectedModelId) {
691
+ await interaction.editReply({
692
+ content: 'Selection expired. Please run /model again.',
693
+ components: [],
694
+ });
695
+ return;
696
+ }
697
+ const selectedScope = interaction.values[0];
698
+ if (!selectedScope) {
699
+ await interaction.editReply({
700
+ content: 'No scope selected',
701
+ components: [],
702
+ });
703
+ return;
704
+ }
705
+ const modelId = context.selectedModelId;
706
+ const modelDisplay = modelId.split('/')[1] || modelId;
707
+ const variant = context.selectedVariant ?? null;
708
+ const variantSuffix = variant ? ` (${variant})` : '';
709
+ const agentTip = '\n_Tip: create [agent .md files](https://github.com/remorses/kimaki/blob/main/docs/model-switching.md) in .opencode/agent/ for one-command model switching_';
710
+ try {
711
+ if (selectedScope === 'session') {
712
+ if (!context.sessionId) {
713
+ pendingModelContexts.delete(contextHash);
714
+ await interaction.editReply({
715
+ content: 'No active session in this thread. Please run /model in a thread with a session.',
716
+ components: [],
717
+ });
718
+ return;
719
+ }
720
+ await setSessionModel({ sessionId: context.sessionId, modelId, variant });
721
+ modelLogger.log(`Set model ${modelId}${variantSuffix} for session ${context.sessionId}`);
722
+ let retried = false;
723
+ if (context.thread) {
724
+ const runtime = getRuntime(context.thread.id);
725
+ if (runtime) {
726
+ retried = await runtime.retryLastUserPrompt();
727
+ }
728
+ }
729
+ const retryNote = retried
730
+ ? '\n_Restarting current request with new model..._'
731
+ : '';
732
+ await interaction.editReply({
733
+ content: `Model set for this session:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`${retryNote}${agentTip}`,
734
+ flags: MessageFlags.SuppressEmbeds,
735
+ components: [],
736
+ });
737
+ }
738
+ else if (selectedScope === 'global') {
739
+ if (!context.appId) {
740
+ pendingModelContexts.delete(contextHash);
741
+ await interaction.editReply({
742
+ content: 'Cannot set global model: channel is not linked to a bot',
743
+ components: [],
744
+ });
745
+ return;
746
+ }
747
+ await setGlobalModel({ appId: context.appId, modelId, variant });
748
+ await setChannelModel({ channelId: context.channelId, modelId, variant });
749
+ modelLogger.log(`Set global model ${modelId}${variantSuffix} for app ${context.appId} and channel ${context.channelId}`);
750
+ await interaction.editReply({
751
+ content: `Model set for this channel and as global default:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nAll channels will use this model (unless they have their own override).${agentTip}`,
752
+ flags: MessageFlags.SuppressEmbeds,
753
+ components: [],
754
+ });
755
+ }
756
+ else {
757
+ // channel scope
758
+ await setChannelModel({ channelId: context.channelId, modelId, variant });
759
+ modelLogger.log(`Set model ${modelId}${variantSuffix} for channel ${context.channelId}`);
760
+ await interaction.editReply({
761
+ content: `Model preference set for this channel:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nAll new sessions in this channel will use this model.${agentTip}`,
762
+ flags: MessageFlags.SuppressEmbeds,
763
+ components: [],
764
+ });
765
+ }
766
+ // Clean up the context from memory
767
+ pendingModelContexts.delete(contextHash);
768
+ }
769
+ catch (error) {
770
+ modelLogger.error('Error saving model preference:', error);
771
+ await interaction.editReply({
772
+ content: `Failed to save model preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
773
+ components: [],
774
+ });
775
+ }
776
+ }