@openclaw/discord 2026.3.13 → 2026.5.2-beta.1

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 (502) hide show
  1. package/account-inspect-api.ts +6 -0
  2. package/action-runtime-api.ts +1 -0
  3. package/api.ts +132 -0
  4. package/channel-config-api.ts +1 -0
  5. package/channel-plugin-api.ts +3 -0
  6. package/config-api.ts +4 -0
  7. package/configured-state.ts +6 -0
  8. package/contract-api.ts +21 -0
  9. package/directory-contract-api.ts +4 -0
  10. package/doctor-contract-api.ts +1 -0
  11. package/index.test.ts +13 -0
  12. package/index.ts +18 -13
  13. package/openclaw.plugin.json +3326 -1
  14. package/package.json +68 -2
  15. package/runtime-api.actions.ts +15 -0
  16. package/runtime-api.lookup.ts +22 -0
  17. package/runtime-api.monitor.ts +50 -0
  18. package/runtime-api.send.ts +79 -0
  19. package/runtime-api.threads.ts +30 -0
  20. package/runtime-api.ts +180 -0
  21. package/runtime-setter-api.ts +3 -0
  22. package/secret-contract-api.ts +4 -0
  23. package/security-audit-contract-api.ts +1 -0
  24. package/security-contract-api.ts +4 -0
  25. package/session-key-api.ts +1 -0
  26. package/setup-entry.ts +9 -0
  27. package/setup-plugin-api.ts +3 -0
  28. package/src/account-inspect.test.ts +126 -0
  29. package/src/account-inspect.ts +132 -0
  30. package/src/accounts.test.ts +247 -0
  31. package/src/accounts.ts +196 -0
  32. package/src/actions/handle-action.guild-admin.ts +411 -0
  33. package/src/actions/handle-action.test.ts +306 -0
  34. package/src/actions/handle-action.ts +372 -0
  35. package/src/actions/runtime.guild.ts +446 -0
  36. package/src/actions/runtime.messaging.messages.ts +205 -0
  37. package/src/actions/runtime.messaging.reactions.ts +67 -0
  38. package/src/actions/runtime.messaging.runtime.ts +69 -0
  39. package/src/actions/runtime.messaging.send.ts +248 -0
  40. package/src/actions/runtime.messaging.shared.ts +97 -0
  41. package/src/actions/runtime.messaging.ts +37 -0
  42. package/src/actions/runtime.moderation-shared.ts +48 -0
  43. package/src/actions/runtime.moderation.authz.test.ts +151 -0
  44. package/src/actions/runtime.moderation.ts +116 -0
  45. package/src/actions/runtime.presence.test.ts +160 -0
  46. package/src/actions/runtime.presence.ts +117 -0
  47. package/src/actions/runtime.shared.ts +83 -0
  48. package/src/actions/runtime.test.ts +1087 -0
  49. package/src/actions/runtime.ts +87 -0
  50. package/src/api-barrel.test.ts +80 -0
  51. package/src/api.test.ts +130 -0
  52. package/src/api.ts +169 -0
  53. package/src/approval-handler.runtime.test.ts +41 -0
  54. package/src/approval-handler.runtime.ts +632 -0
  55. package/src/approval-native.test.ts +330 -0
  56. package/src/approval-native.ts +219 -0
  57. package/src/approval-runtime.ts +14 -0
  58. package/src/approval-shared.ts +53 -0
  59. package/src/audit-core.ts +141 -0
  60. package/src/audit.test.ts +145 -0
  61. package/src/audit.ts +32 -0
  62. package/src/channel-actions.contract.test.ts +45 -0
  63. package/src/channel-actions.runtime.ts +1 -0
  64. package/src/channel-actions.test.ts +275 -0
  65. package/src/channel-actions.ts +203 -0
  66. package/src/channel-api.ts +29 -0
  67. package/src/channel.conversation.ts +159 -0
  68. package/src/channel.loaders.ts +47 -0
  69. package/src/channel.runtime.ts +1 -0
  70. package/src/channel.setup.ts +12 -0
  71. package/src/channel.test.ts +547 -12
  72. package/src/channel.ts +597 -430
  73. package/src/chunk.test.ts +157 -0
  74. package/src/chunk.ts +321 -0
  75. package/src/client.proxy.test.ts +176 -0
  76. package/src/client.test.ts +76 -0
  77. package/src/client.ts +132 -0
  78. package/src/component-custom-id.ts +72 -0
  79. package/src/components-registry.ts +356 -0
  80. package/src/components.builders.ts +409 -0
  81. package/src/components.modal.ts +124 -0
  82. package/src/components.parse.ts +407 -0
  83. package/src/components.test.ts +312 -0
  84. package/src/components.ts +54 -0
  85. package/src/components.types.ts +187 -0
  86. package/src/config-schema.test.ts +325 -0
  87. package/src/config-schema.ts +6 -0
  88. package/src/config-ui-hints.ts +249 -0
  89. package/src/conversation-identity.ts +58 -0
  90. package/src/delivery-retry.ts +56 -0
  91. package/src/directory-cache.ts +116 -0
  92. package/src/directory-config.ts +58 -0
  93. package/src/directory-contract.test.ts +129 -0
  94. package/src/directory-live.test.ts +126 -0
  95. package/src/directory-live.ts +135 -0
  96. package/src/doctor-contract.ts +477 -0
  97. package/src/doctor-shared.ts +5 -0
  98. package/src/doctor.test.ts +405 -0
  99. package/src/doctor.ts +340 -0
  100. package/src/draft-chunking.test.ts +64 -0
  101. package/src/draft-chunking.ts +43 -0
  102. package/src/draft-stream.test.ts +159 -0
  103. package/src/draft-stream.ts +154 -0
  104. package/src/error-body.ts +38 -0
  105. package/src/exec-approvals.test.ts +88 -0
  106. package/src/exec-approvals.ts +110 -0
  107. package/src/gateway-logging.test.ts +98 -0
  108. package/src/gateway-logging.ts +67 -0
  109. package/src/group-policy.ts +113 -0
  110. package/src/guilds.ts +29 -0
  111. package/src/inbound-context.contract.test.ts +11 -0
  112. package/src/interactive-dispatch.ts +104 -0
  113. package/src/internal/api.commands.ts +51 -0
  114. package/src/internal/api.guild.ts +164 -0
  115. package/src/internal/api.interactions.ts +53 -0
  116. package/src/internal/api.messages.ts +113 -0
  117. package/src/internal/api.reactions.ts +38 -0
  118. package/src/internal/api.test.ts +262 -0
  119. package/src/internal/api.ts +61 -0
  120. package/src/internal/api.users.ts +19 -0
  121. package/src/internal/api.webhooks.ts +13 -0
  122. package/src/internal/client.test.ts +408 -0
  123. package/src/internal/client.ts +308 -0
  124. package/src/internal/command-deploy.ts +237 -0
  125. package/src/internal/commands.ts +188 -0
  126. package/src/internal/components.base.ts +65 -0
  127. package/src/internal/components.message.ts +279 -0
  128. package/src/internal/components.modal.ts +95 -0
  129. package/src/internal/components.ts +31 -0
  130. package/src/internal/discord.ts +11 -0
  131. package/src/internal/embeds.ts +35 -0
  132. package/src/internal/entity-cache.ts +98 -0
  133. package/src/internal/event-queue.ts +162 -0
  134. package/src/internal/gateway-close-codes.ts +25 -0
  135. package/src/internal/gateway-dispatch.ts +96 -0
  136. package/src/internal/gateway-identify-limiter.ts +26 -0
  137. package/src/internal/gateway-lifecycle.ts +61 -0
  138. package/src/internal/gateway-rate-limit.ts +104 -0
  139. package/src/internal/gateway.test.ts +603 -0
  140. package/src/internal/gateway.ts +476 -0
  141. package/src/internal/interaction-dispatch.test.ts +148 -0
  142. package/src/internal/interaction-dispatch.ts +162 -0
  143. package/src/internal/interaction-options.ts +98 -0
  144. package/src/internal/interaction-response.ts +53 -0
  145. package/src/internal/interactions.test.ts +325 -0
  146. package/src/internal/interactions.ts +378 -0
  147. package/src/internal/listeners.ts +85 -0
  148. package/src/internal/live-smoke.live.test.ts +26 -0
  149. package/src/internal/modal-fields.ts +95 -0
  150. package/src/internal/payload.ts +69 -0
  151. package/src/internal/rest-body.ts +115 -0
  152. package/src/internal/rest-errors.ts +88 -0
  153. package/src/internal/rest-routes.ts +50 -0
  154. package/src/internal/rest-scheduler.ts +557 -0
  155. package/src/internal/rest.test.ts +673 -0
  156. package/src/internal/rest.ts +322 -0
  157. package/src/internal/schemas.ts +36 -0
  158. package/src/internal/structures.test.ts +43 -0
  159. package/src/internal/structures.ts +280 -0
  160. package/src/internal/test-builders.test-support.ts +163 -0
  161. package/src/internal/voice.ts +49 -0
  162. package/src/media-detection.ts +28 -0
  163. package/src/mentions.test.ts +111 -0
  164. package/src/mentions.ts +147 -0
  165. package/src/monitor/access-groups.ts +55 -0
  166. package/src/monitor/ack-reactions.ts +70 -0
  167. package/src/monitor/acp-bind-here.integration.test.ts +211 -0
  168. package/src/monitor/agent-components-auth.ts +7 -0
  169. package/src/monitor/agent-components-context.ts +154 -0
  170. package/src/monitor/agent-components-data.ts +224 -0
  171. package/src/monitor/agent-components-dm-auth.ts +221 -0
  172. package/src/monitor/agent-components-guild-auth.ts +322 -0
  173. package/src/monitor/agent-components-helpers.runtime.ts +5 -0
  174. package/src/monitor/agent-components-helpers.ts +34 -0
  175. package/src/monitor/agent-components-reply.ts +10 -0
  176. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  177. package/src/monitor/agent-components.dispatch.ts +366 -0
  178. package/src/monitor/agent-components.handlers.ts +303 -0
  179. package/src/monitor/agent-components.modal.ts +160 -0
  180. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  181. package/src/monitor/agent-components.runtime.ts +14 -0
  182. package/src/monitor/agent-components.system-controls.ts +211 -0
  183. package/src/monitor/agent-components.ts +70 -0
  184. package/src/monitor/agent-components.types.ts +58 -0
  185. package/src/monitor/agent-components.wildcard-controls.ts +168 -0
  186. package/src/monitor/agent-components.wildcard.test.ts +71 -0
  187. package/src/monitor/allow-list.test.ts +14 -0
  188. package/src/monitor/allow-list.ts +633 -0
  189. package/src/monitor/auto-presence.test.ts +156 -0
  190. package/src/monitor/auto-presence.ts +356 -0
  191. package/src/monitor/channel-access.test.ts +99 -0
  192. package/src/monitor/channel-access.ts +102 -0
  193. package/src/monitor/commands.test.ts +24 -0
  194. package/src/monitor/commands.ts +9 -0
  195. package/src/monitor/dm-command-auth.test.ts +197 -0
  196. package/src/monitor/dm-command-auth.ts +158 -0
  197. package/src/monitor/dm-command-decision.test.ts +113 -0
  198. package/src/monitor/dm-command-decision.ts +49 -0
  199. package/src/monitor/exec-approvals.test.ts +226 -0
  200. package/src/monitor/exec-approvals.ts +158 -0
  201. package/src/monitor/format.ts +45 -0
  202. package/src/monitor/gateway-handle.ts +34 -0
  203. package/src/monitor/gateway-metadata.test.ts +29 -0
  204. package/src/monitor/gateway-metadata.ts +298 -0
  205. package/src/monitor/gateway-plugin.test.ts +297 -0
  206. package/src/monitor/gateway-plugin.ts +294 -0
  207. package/src/monitor/gateway-registry.ts +37 -0
  208. package/src/monitor/gateway-supervisor.test.ts +150 -0
  209. package/src/monitor/gateway-supervisor.ts +206 -0
  210. package/src/monitor/inbound-context.test-helpers.ts +37 -0
  211. package/src/monitor/inbound-context.test.ts +106 -0
  212. package/src/monitor/inbound-context.ts +103 -0
  213. package/src/monitor/inbound-dedupe.ts +79 -0
  214. package/src/monitor/inbound-job.test.ts +203 -0
  215. package/src/monitor/inbound-job.ts +118 -0
  216. package/src/monitor/listeners.queue.ts +91 -0
  217. package/src/monitor/listeners.reactions.ts +610 -0
  218. package/src/monitor/listeners.test.ts +200 -0
  219. package/src/monitor/listeners.ts +150 -0
  220. package/src/monitor/message-channel-info.ts +96 -0
  221. package/src/monitor/message-forwarded.ts +107 -0
  222. package/src/monitor/message-handler.batch-gate.test.ts +22 -0
  223. package/src/monitor/message-handler.batch-gate.ts +19 -0
  224. package/src/monitor/message-handler.bot-self-filter.test.ts +68 -0
  225. package/src/monitor/message-handler.context.ts +406 -0
  226. package/src/monitor/message-handler.dm-preflight.ts +123 -0
  227. package/src/monitor/message-handler.draft-preview.ts +246 -0
  228. package/src/monitor/message-handler.hydration.test.ts +80 -0
  229. package/src/monitor/message-handler.hydration.ts +198 -0
  230. package/src/monitor/message-handler.inbound-context.test.ts +59 -0
  231. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  232. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  233. package/src/monitor/message-handler.preflight-channel-context.test.ts +18 -0
  234. package/src/monitor/message-handler.preflight-channel-context.ts +58 -0
  235. package/src/monitor/message-handler.preflight-context.ts +54 -0
  236. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  237. package/src/monitor/message-handler.preflight-history.ts +23 -0
  238. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  239. package/src/monitor/message-handler.preflight-pluralkit.ts +26 -0
  240. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  241. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  242. package/src/monitor/message-handler.preflight.acp-bindings.test.ts +369 -0
  243. package/src/monitor/message-handler.preflight.test-helpers.ts +111 -0
  244. package/src/monitor/message-handler.preflight.test.ts +1623 -0
  245. package/src/monitor/message-handler.preflight.ts +679 -0
  246. package/src/monitor/message-handler.preflight.types.ts +110 -0
  247. package/src/monitor/message-handler.process.test.ts +1369 -0
  248. package/src/monitor/message-handler.process.ts +686 -0
  249. package/src/monitor/message-handler.queue.test.ts +496 -0
  250. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  251. package/src/monitor/message-handler.test-harness.ts +99 -0
  252. package/src/monitor/message-handler.test-helpers.ts +75 -0
  253. package/src/monitor/message-handler.ts +274 -0
  254. package/src/monitor/message-media.ts +509 -0
  255. package/src/monitor/message-run-queue.ts +101 -0
  256. package/src/monitor/message-text.ts +171 -0
  257. package/src/monitor/message-utils.test.ts +1157 -0
  258. package/src/monitor/message-utils.ts +32 -0
  259. package/src/monitor/model-picker-preferences.test.ts +67 -0
  260. package/src/monitor/model-picker-preferences.ts +184 -0
  261. package/src/monitor/model-picker.state.ts +364 -0
  262. package/src/monitor/model-picker.test-utils.ts +26 -0
  263. package/src/monitor/model-picker.test.ts +794 -0
  264. package/src/monitor/model-picker.ts +38 -0
  265. package/src/monitor/model-picker.view.ts +695 -0
  266. package/src/monitor/monitor.agent-components.test.ts +375 -0
  267. package/src/monitor/monitor.test.ts +849 -0
  268. package/src/monitor/monitor.threading-utils.test.ts +598 -0
  269. package/src/monitor/native-command-agent-reply.ts +125 -0
  270. package/src/monitor/native-command-arg-ui.ts +233 -0
  271. package/src/monitor/native-command-auth.ts +308 -0
  272. package/src/monitor/native-command-bypass.ts +13 -0
  273. package/src/monitor/native-command-context.test.ts +98 -0
  274. package/src/monitor/native-command-context.ts +103 -0
  275. package/src/monitor/native-command-dispatch.ts +35 -0
  276. package/src/monitor/native-command-model-picker-apply.ts +177 -0
  277. package/src/monitor/native-command-model-picker-interaction.ts +461 -0
  278. package/src/monitor/native-command-model-picker-ui.ts +368 -0
  279. package/src/monitor/native-command-reply.test.ts +68 -0
  280. package/src/monitor/native-command-reply.ts +185 -0
  281. package/src/monitor/native-command-route.ts +91 -0
  282. package/src/monitor/native-command-status.ts +76 -0
  283. package/src/monitor/native-command-ui.ts +26 -0
  284. package/src/monitor/native-command-ui.types.ts +20 -0
  285. package/src/monitor/native-command.args.ts +45 -0
  286. package/src/monitor/native-command.command-arg.test.ts +99 -0
  287. package/src/monitor/native-command.commands-allowfrom.test.ts +490 -0
  288. package/src/monitor/native-command.model-picker.test.ts +767 -0
  289. package/src/monitor/native-command.options.test.ts +369 -0
  290. package/src/monitor/native-command.options.ts +153 -0
  291. package/src/monitor/native-command.plugin-dispatch.test.ts +961 -0
  292. package/src/monitor/native-command.runtime.ts +50 -0
  293. package/src/monitor/native-command.status-direct.test.ts +272 -0
  294. package/src/monitor/native-command.test-helpers.ts +64 -0
  295. package/src/monitor/native-command.think-autocomplete.test.ts +416 -0
  296. package/src/monitor/native-command.ts +700 -0
  297. package/src/monitor/native-command.types.ts +9 -0
  298. package/src/monitor/native-interaction-channel-context.ts +50 -0
  299. package/src/monitor/preflight-audio.runtime.ts +9 -0
  300. package/src/monitor/preflight-audio.test.ts +157 -0
  301. package/src/monitor/preflight-audio.ts +130 -0
  302. package/src/monitor/presence-cache.ts +61 -0
  303. package/src/monitor/presence.test.ts +44 -0
  304. package/src/monitor/presence.ts +50 -0
  305. package/src/monitor/provider-session.runtime.ts +12 -0
  306. package/src/monitor/provider.acp.ts +89 -0
  307. package/src/monitor/provider.allowlist.test.ts +149 -0
  308. package/src/monitor/provider.allowlist.ts +394 -0
  309. package/src/monitor/provider.cleanup.ts +41 -0
  310. package/src/monitor/provider.commands.ts +129 -0
  311. package/src/monitor/provider.config-log.ts +45 -0
  312. package/src/monitor/provider.deploy-errors.ts +362 -0
  313. package/src/monitor/provider.deploy.ts +221 -0
  314. package/src/monitor/provider.interactions.ts +160 -0
  315. package/src/monitor/provider.lifecycle.test.ts +713 -0
  316. package/src/monitor/provider.lifecycle.ts +552 -0
  317. package/src/monitor/provider.proxy.test.ts +745 -0
  318. package/src/monitor/provider.rest-proxy.test.ts +121 -0
  319. package/src/monitor/provider.runtime.ts +1 -0
  320. package/src/monitor/provider.skill-dedupe.test.ts +42 -0
  321. package/src/monitor/provider.startup-log.ts +32 -0
  322. package/src/monitor/provider.startup.test.ts +426 -0
  323. package/src/monitor/provider.startup.ts +323 -0
  324. package/src/monitor/provider.test.ts +1111 -0
  325. package/src/monitor/provider.ts +713 -0
  326. package/src/monitor/reply-context.ts +64 -0
  327. package/src/monitor/reply-delivery.test.ts +244 -0
  328. package/src/monitor/reply-delivery.ts +203 -0
  329. package/src/monitor/rest-fetch.ts +43 -0
  330. package/src/monitor/route-resolution.test.ts +204 -0
  331. package/src/monitor/route-resolution.ts +140 -0
  332. package/src/monitor/sender-identity.ts +81 -0
  333. package/src/monitor/startup-status.test.ts +30 -0
  334. package/src/monitor/startup-status.ts +10 -0
  335. package/src/monitor/status.ts +22 -0
  336. package/src/monitor/system-events.ts +55 -0
  337. package/src/monitor/thread-bindings.config.ts +35 -0
  338. package/src/monitor/thread-bindings.discord-api.test.ts +229 -0
  339. package/src/monitor/thread-bindings.discord-api.ts +310 -0
  340. package/src/monitor/thread-bindings.lifecycle.test.ts +1871 -0
  341. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  342. package/src/monitor/thread-bindings.manager.ts +553 -0
  343. package/src/monitor/thread-bindings.messages.ts +6 -0
  344. package/src/monitor/thread-bindings.persona.test.ts +34 -0
  345. package/src/monitor/thread-bindings.persona.ts +25 -0
  346. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  347. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  348. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  349. package/src/monitor/thread-bindings.shared-state.test.ts +36 -0
  350. package/src/monitor/thread-bindings.state.ts +540 -0
  351. package/src/monitor/thread-bindings.ts +48 -0
  352. package/src/monitor/thread-bindings.types.ts +83 -0
  353. package/src/monitor/thread-channel-context.ts +112 -0
  354. package/src/monitor/thread-session-close.test.ts +180 -0
  355. package/src/monitor/thread-session-close.ts +63 -0
  356. package/src/monitor/thread-title.generate.test.ts +197 -0
  357. package/src/monitor/thread-title.test.ts +31 -0
  358. package/src/monitor/thread-title.ts +181 -0
  359. package/src/monitor/threading.auto-thread.test.ts +327 -0
  360. package/src/monitor/threading.auto-thread.ts +287 -0
  361. package/src/monitor/threading.cache.ts +45 -0
  362. package/src/monitor/threading.parent-info.test.ts +156 -0
  363. package/src/monitor/threading.starter.test.ts +260 -0
  364. package/src/monitor/threading.starter.ts +287 -0
  365. package/src/monitor/threading.ts +20 -0
  366. package/src/monitor/threading.types.ts +102 -0
  367. package/src/monitor/timeouts.ts +84 -0
  368. package/src/monitor/typing.test.ts +42 -0
  369. package/src/monitor/typing.ts +17 -0
  370. package/src/monitor.gateway.test.ts +187 -0
  371. package/src/monitor.gateway.ts +75 -0
  372. package/src/monitor.test.ts +1397 -0
  373. package/src/monitor.ts +28 -0
  374. package/src/normalize.test.ts +56 -0
  375. package/src/normalize.ts +86 -0
  376. package/src/outbound-adapter.interactive-order.test.ts +64 -0
  377. package/src/outbound-adapter.test-harness.ts +207 -0
  378. package/src/outbound-adapter.test.ts +696 -0
  379. package/src/outbound-adapter.ts +291 -0
  380. package/src/outbound-approval.ts +29 -0
  381. package/src/outbound-components.ts +81 -0
  382. package/src/outbound-payload.contract.test.ts +38 -0
  383. package/src/outbound-payload.ts +134 -0
  384. package/src/outbound-send-context.ts +92 -0
  385. package/src/outbound-session-route.test.ts +34 -0
  386. package/src/outbound-session-route.ts +72 -0
  387. package/src/pluralkit.test.ts +67 -0
  388. package/src/pluralkit.ts +58 -0
  389. package/src/preview-streaming.ts +32 -0
  390. package/src/probe.intents.test.ts +94 -0
  391. package/src/probe.parse-token.test.ts +43 -0
  392. package/src/probe.runtime.ts +1 -0
  393. package/src/probe.ts +237 -0
  394. package/src/proxy-fetch.ts +92 -0
  395. package/src/proxy-request-client.test.ts +78 -0
  396. package/src/proxy-request-client.ts +21 -0
  397. package/src/recipient-resolution.ts +39 -0
  398. package/src/resolve-allowlist-common.test.ts +36 -0
  399. package/src/resolve-allowlist-common.ts +39 -0
  400. package/src/resolve-channels.test.ts +340 -0
  401. package/src/resolve-channels.ts +369 -0
  402. package/src/resolve-users.test.ts +222 -0
  403. package/src/resolve-users.ts +184 -0
  404. package/src/retry.test.ts +83 -0
  405. package/src/retry.ts +98 -0
  406. package/src/runtime-api.ts +64 -0
  407. package/src/runtime.ts +22 -5
  408. package/src/secret-config-contract.ts +140 -0
  409. package/src/security-audit.runtime.ts +1 -0
  410. package/src/security-audit.test.ts +246 -0
  411. package/src/security-audit.ts +208 -0
  412. package/src/security-contract.ts +47 -0
  413. package/src/security-doctor.test.ts +25 -0
  414. package/src/security-doctor.ts +20 -0
  415. package/src/security.ts +60 -0
  416. package/src/send-target-parsing.ts +14 -0
  417. package/src/send.channels.ts +139 -0
  418. package/src/send.components.test.ts +275 -0
  419. package/src/send.components.ts +381 -0
  420. package/src/send.creates-thread.test.ts +643 -0
  421. package/src/send.emojis-stickers.ts +57 -0
  422. package/src/send.guild.ts +170 -0
  423. package/src/send.message-request.ts +97 -0
  424. package/src/send.messages.test.ts +53 -0
  425. package/src/send.messages.ts +225 -0
  426. package/src/send.outbound.ts +413 -0
  427. package/src/send.permissions.authz.test.ts +188 -0
  428. package/src/send.permissions.ts +283 -0
  429. package/src/send.reactions.ts +155 -0
  430. package/src/send.sends-basic-channel-messages.test.ts +941 -0
  431. package/src/send.shared.ts +447 -0
  432. package/src/send.test-harness.ts +56 -0
  433. package/src/send.ts +82 -0
  434. package/src/send.types.ts +188 -0
  435. package/src/send.typing.test.ts +41 -0
  436. package/src/send.typing.ts +9 -0
  437. package/src/send.voice.ts +134 -0
  438. package/src/send.webhook-activity.test.ts +105 -0
  439. package/src/send.webhook.proxy.test.ts +191 -0
  440. package/src/send.webhook.ts +133 -0
  441. package/src/session-contract.ts +3 -0
  442. package/src/session-key-normalization.test.ts +44 -0
  443. package/src/session-key-normalization.ts +47 -0
  444. package/src/setup-account-state.test.ts +91 -0
  445. package/src/setup-account-state.ts +144 -0
  446. package/src/setup-adapter.ts +12 -0
  447. package/src/setup-core.ts +212 -0
  448. package/src/setup-runtime-helpers.ts +10 -0
  449. package/src/setup-surface.test.ts +137 -0
  450. package/src/setup-surface.ts +129 -0
  451. package/src/shared-interactive.test.ts +153 -0
  452. package/src/shared-interactive.ts +124 -0
  453. package/src/shared.test.ts +165 -0
  454. package/src/shared.ts +190 -0
  455. package/src/status-issues.test.ts +70 -0
  456. package/src/status-issues.ts +169 -0
  457. package/src/subagent-hooks.test.ts +130 -81
  458. package/src/subagent-hooks.ts +184 -122
  459. package/src/target-parsing.ts +53 -0
  460. package/src/target-resolver.ts +129 -0
  461. package/src/targets.test.ts +367 -0
  462. package/src/targets.ts +12 -0
  463. package/src/test-http-helpers.ts +10 -0
  464. package/src/test-support/component-runtime.ts +190 -0
  465. package/src/test-support/config.ts +7 -0
  466. package/src/test-support/configured-binding-runtime.ts +29 -0
  467. package/src/test-support/partial-channel.ts +26 -0
  468. package/src/test-support/provider.test-support.ts +545 -0
  469. package/src/token.test.ts +107 -0
  470. package/src/token.ts +60 -0
  471. package/src/ui-colors.ts +27 -0
  472. package/src/ui.ts +20 -0
  473. package/src/voice/access.test.ts +217 -0
  474. package/src/voice/access.ts +124 -0
  475. package/src/voice/audio.ts +173 -0
  476. package/src/voice/capture-state.test.ts +48 -0
  477. package/src/voice/capture-state.ts +120 -0
  478. package/src/voice/command.test.ts +164 -0
  479. package/src/voice/command.ts +283 -0
  480. package/src/voice/config.ts +8 -0
  481. package/src/voice/manager.e2e.test.ts +928 -0
  482. package/src/voice/manager.ready-listener.test.ts +37 -0
  483. package/src/voice/manager.runtime.ts +11 -0
  484. package/src/voice/manager.ts +691 -0
  485. package/src/voice/prompt.test.ts +16 -0
  486. package/src/voice/prompt.ts +17 -0
  487. package/src/voice/receive-recovery.test.ts +79 -0
  488. package/src/voice/receive-recovery.ts +159 -0
  489. package/src/voice/sanitize.test.ts +34 -0
  490. package/src/voice/sanitize.ts +32 -0
  491. package/src/voice/sdk-runtime.ts +14 -0
  492. package/src/voice/segment.ts +156 -0
  493. package/src/voice/session.ts +50 -0
  494. package/src/voice/speaker-context.ts +127 -0
  495. package/src/voice/tts.ts +125 -0
  496. package/src/voice-message.test.ts +234 -0
  497. package/src/voice-message.ts +444 -0
  498. package/subagent-hooks-api.ts +27 -0
  499. package/test-api.ts +4 -0
  500. package/thread-binding-api.ts +1 -0
  501. package/timeouts.ts +6 -0
  502. package/tsconfig.json +16 -0
@@ -0,0 +1,794 @@
1
+ import { ComponentType } from "discord-api-types/v10";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { serializePayload } from "../internal/discord.js";
4
+ import { EMPTY_DISCORD_TEST_CONFIG } from "../test-support/config.js";
5
+ import {
6
+ DISCORD_CUSTOM_ID_MAX_CHARS,
7
+ DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE,
8
+ DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE,
9
+ DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX,
10
+ buildDiscordModelPickerCustomId,
11
+ getDiscordModelPickerModelPage,
12
+ getDiscordModelPickerProviderPage,
13
+ loadDiscordModelPickerData,
14
+ parseDiscordModelPickerCustomId,
15
+ parseDiscordModelPickerData,
16
+ renderDiscordModelPickerModelsView,
17
+ renderDiscordModelPickerProvidersView,
18
+ renderDiscordModelPickerRecentsView,
19
+ toDiscordModelPickerMessagePayload,
20
+ } from "./model-picker.js";
21
+ import { createModelsProviderData } from "./model-picker.test-utils.js";
22
+
23
+ const buildModelsProviderDataMock = vi.hoisted(() => vi.fn());
24
+
25
+ vi.mock("openclaw/plugin-sdk/models-provider-runtime", () => ({
26
+ buildModelsProviderData: buildModelsProviderDataMock,
27
+ }));
28
+
29
+ type SerializedComponent = {
30
+ type: number;
31
+ custom_id?: string;
32
+ options?: Array<{ label?: string; value: string; default?: boolean }>;
33
+ components?: SerializedComponent[];
34
+ };
35
+
36
+ const DISCORD_CONTAINER_COMPONENT_TYPE: SerializedComponent["type"] = ComponentType.Container;
37
+ const DISCORD_ACTION_ROW_COMPONENT_TYPE: SerializedComponent["type"] = ComponentType.ActionRow;
38
+ const DISCORD_STRING_SELECT_COMPONENT_TYPE: SerializedComponent["type"] =
39
+ ComponentType.StringSelect;
40
+
41
+ function extractContainerRows(components?: SerializedComponent[]): SerializedComponent[] {
42
+ const container = components?.find(
43
+ (component) => component.type === DISCORD_CONTAINER_COMPONENT_TYPE,
44
+ );
45
+ if (!container) {
46
+ return [];
47
+ }
48
+ return (container.components ?? []).filter(
49
+ (component) => component.type === DISCORD_ACTION_ROW_COMPONENT_TYPE,
50
+ );
51
+ }
52
+
53
+ function renderModelsViewRows(
54
+ params: Parameters<typeof renderDiscordModelPickerModelsView>[0],
55
+ ): SerializedComponent[] {
56
+ const rendered = renderDiscordModelPickerModelsView(params);
57
+ const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
58
+ components?: SerializedComponent[];
59
+ };
60
+ return extractContainerRows(payload.components);
61
+ }
62
+
63
+ function renderRecentsViewRows(
64
+ params: Parameters<typeof renderDiscordModelPickerRecentsView>[0],
65
+ ): SerializedComponent[] {
66
+ const rendered = renderDiscordModelPickerRecentsView(params);
67
+ const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
68
+ components?: SerializedComponent[];
69
+ };
70
+ return extractContainerRows(payload.components);
71
+ }
72
+
73
+ function requireValue<T>(value: T | null | undefined, message: string): T {
74
+ if (value == null) {
75
+ throw new Error(message);
76
+ }
77
+ return value;
78
+ }
79
+
80
+ describe("loadDiscordModelPickerData", () => {
81
+ it("reuses buildModelsProviderData as source of truth with agent scope", async () => {
82
+ const expected = createModelsProviderData({ openai: ["gpt-4o"] });
83
+ const cfg = EMPTY_DISCORD_TEST_CONFIG;
84
+ buildModelsProviderDataMock.mockResolvedValue(expected);
85
+
86
+ const result = await loadDiscordModelPickerData(cfg, "support");
87
+
88
+ expect(buildModelsProviderDataMock).toHaveBeenCalledTimes(1);
89
+ expect(buildModelsProviderDataMock).toHaveBeenCalledWith(cfg, "support");
90
+ expect(result).toBe(expected);
91
+ });
92
+ });
93
+
94
+ describe("Discord model picker custom_id", () => {
95
+ it("encodes and decodes command/provider/page/user context", () => {
96
+ const customId = buildDiscordModelPickerCustomId({
97
+ command: "models",
98
+ action: "provider",
99
+ view: "models",
100
+ provider: "OpenAI",
101
+ page: 3,
102
+ userId: "1234567890",
103
+ });
104
+
105
+ const parsed = parseDiscordModelPickerCustomId(customId);
106
+
107
+ expect(parsed).toEqual({
108
+ command: "models",
109
+ action: "provider",
110
+ view: "models",
111
+ provider: "openai",
112
+ page: 3,
113
+ userId: "1234567890",
114
+ });
115
+ });
116
+
117
+ it("parses component data payloads", () => {
118
+ const parsed = parseDiscordModelPickerData({
119
+ cmd: "model",
120
+ act: "back",
121
+ view: "providers",
122
+ u: "42",
123
+ p: "anthropic",
124
+ pg: "2",
125
+ });
126
+
127
+ expect(parsed).toEqual({
128
+ command: "model",
129
+ action: "back",
130
+ view: "providers",
131
+ userId: "42",
132
+ provider: "anthropic",
133
+ page: 2,
134
+ });
135
+ });
136
+
137
+ it("parses compact custom_id aliases", () => {
138
+ const parsed = parseDiscordModelPickerData({
139
+ c: "models",
140
+ a: "submit",
141
+ v: "models",
142
+ u: "42",
143
+ p: "openai",
144
+ g: "3",
145
+ mi: "2",
146
+ });
147
+
148
+ expect(parsed).toEqual({
149
+ command: "models",
150
+ action: "submit",
151
+ view: "models",
152
+ userId: "42",
153
+ provider: "openai",
154
+ page: 3,
155
+ modelIndex: 2,
156
+ });
157
+ });
158
+
159
+ it("parses optional submit model index", () => {
160
+ const parsed = parseDiscordModelPickerData({
161
+ cmd: "models",
162
+ act: "submit",
163
+ view: "models",
164
+ u: "42",
165
+ p: "openai",
166
+ r: "codex",
167
+ pg: "1",
168
+ mi: "7",
169
+ });
170
+
171
+ expect(parsed).toEqual({
172
+ command: "models",
173
+ action: "submit",
174
+ view: "models",
175
+ userId: "42",
176
+ provider: "openai",
177
+ runtime: "codex",
178
+ page: 1,
179
+ modelIndex: 7,
180
+ });
181
+ });
182
+
183
+ it("rejects invalid command/action/view values", () => {
184
+ expect(
185
+ parseDiscordModelPickerData({
186
+ cmd: "status",
187
+ act: "nav",
188
+ view: "providers",
189
+ u: "42",
190
+ }),
191
+ ).toBeNull();
192
+ expect(
193
+ parseDiscordModelPickerData({
194
+ cmd: "model",
195
+ act: "unknown",
196
+ view: "providers",
197
+ u: "42",
198
+ }),
199
+ ).toBeNull();
200
+ expect(
201
+ parseDiscordModelPickerData({
202
+ cmd: "model",
203
+ act: "nav",
204
+ view: "unknown",
205
+ u: "42",
206
+ }),
207
+ ).toBeNull();
208
+ });
209
+
210
+ it("enforces Discord custom_id max length", () => {
211
+ const longProvider = `provider-${"x".repeat(DISCORD_CUSTOM_ID_MAX_CHARS)}`;
212
+ expect(() =>
213
+ buildDiscordModelPickerCustomId({
214
+ command: "model",
215
+ action: "provider",
216
+ view: "models",
217
+ provider: longProvider,
218
+ page: 1,
219
+ userId: "42",
220
+ }),
221
+ ).toThrow(/custom_id exceeds/i);
222
+ });
223
+
224
+ it("keeps typical submit ids under Discord max length", () => {
225
+ const customId = buildDiscordModelPickerCustomId({
226
+ command: "models",
227
+ action: "submit",
228
+ view: "models",
229
+ provider: "azure-openai-responses",
230
+ page: 1,
231
+ providerPage: 1,
232
+ modelIndex: 10,
233
+ userId: "12345678901234567890",
234
+ });
235
+
236
+ expect(customId.length).toBeLessThanOrEqual(DISCORD_CUSTOM_ID_MAX_CHARS);
237
+ });
238
+ });
239
+
240
+ describe("provider paging", () => {
241
+ it("keeps providers on a single page when count fits Discord button rows", () => {
242
+ const entries: Record<string, string[]> = {};
243
+ for (let i = 1; i <= DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX - 2; i += 1) {
244
+ entries[`provider-${String(i).padStart(2, "0")}`] = [`model-${i}`];
245
+ }
246
+ const data = createModelsProviderData(entries);
247
+
248
+ const page = getDiscordModelPickerProviderPage({ data, page: 1 });
249
+
250
+ expect(page.items).toHaveLength(DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX - 2);
251
+ expect(page.totalPages).toBe(1);
252
+ expect(page.pageSize).toBe(DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX);
253
+ expect(page.hasPrev).toBe(false);
254
+ expect(page.hasNext).toBe(false);
255
+ });
256
+
257
+ it("paginates providers when count exceeds one-page Discord button limits", () => {
258
+ const entries: Record<string, string[]> = {};
259
+ for (let i = 1; i <= DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX + 3; i += 1) {
260
+ entries[`provider-${String(i).padStart(2, "0")}`] = [`model-${i}`];
261
+ }
262
+ const data = createModelsProviderData(entries);
263
+
264
+ const page1 = getDiscordModelPickerProviderPage({ data, page: 1 });
265
+ const lastPage = getDiscordModelPickerProviderPage({ data, page: 99 });
266
+
267
+ expect(page1.items).toHaveLength(DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE);
268
+ expect(page1.totalPages).toBe(2);
269
+ expect(page1.hasNext).toBe(true);
270
+
271
+ expect(lastPage.page).toBe(2);
272
+ expect(lastPage.items).toHaveLength(8);
273
+ expect(lastPage.hasPrev).toBe(true);
274
+ expect(lastPage.hasNext).toBe(false);
275
+ });
276
+
277
+ it("caps custom provider page size at Discord-safe max", () => {
278
+ const compactData = createModelsProviderData({
279
+ anthropic: ["claude-sonnet-4-5"],
280
+ openai: ["gpt-4o"],
281
+ google: ["gemini-3-pro"],
282
+ });
283
+ const compactPage = getDiscordModelPickerProviderPage({
284
+ data: compactData,
285
+ page: 1,
286
+ pageSize: 999,
287
+ });
288
+ expect(compactPage.pageSize).toBe(DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX);
289
+
290
+ const pagedEntries: Record<string, string[]> = {};
291
+ for (let i = 1; i <= DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX + 1; i += 1) {
292
+ pagedEntries[`provider-${String(i).padStart(2, "0")}`] = [`model-${i}`];
293
+ }
294
+ const pagedData = createModelsProviderData(pagedEntries);
295
+ const pagedPage = getDiscordModelPickerProviderPage({
296
+ data: pagedData,
297
+ page: 1,
298
+ pageSize: 999,
299
+ });
300
+ expect(pagedPage.pageSize).toBe(DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE);
301
+ });
302
+ });
303
+
304
+ describe("model paging", () => {
305
+ it("sorts models and paginates with Discord select-option constraints", () => {
306
+ const models = Array.from(
307
+ { length: DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE + 4 },
308
+ (_, idx) =>
309
+ `model-${String(DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE + 4 - idx).padStart(2, "0")}`,
310
+ );
311
+ const data = createModelsProviderData({ openai: models });
312
+
313
+ const page1 = requireValue(
314
+ getDiscordModelPickerModelPage({ data, provider: "openai", page: 1 }),
315
+ "expected first model page for openai",
316
+ );
317
+ const page2 = requireValue(
318
+ getDiscordModelPickerModelPage({ data, provider: "openai", page: 2 }),
319
+ "expected second model page for openai",
320
+ );
321
+
322
+ expect(page1.items).toHaveLength(DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE);
323
+ expect(page1.items[0]).toBe("model-01");
324
+ expect(page1.hasNext).toBe(true);
325
+
326
+ expect(page2.items).toHaveLength(4);
327
+ expect(page2.page).toBe(2);
328
+ expect(page2.hasPrev).toBe(true);
329
+ expect(page2.hasNext).toBe(false);
330
+ });
331
+
332
+ it("returns null for unknown provider", () => {
333
+ const data = createModelsProviderData({ anthropic: ["claude-sonnet-4-5"] });
334
+ const page = getDiscordModelPickerModelPage({ data, provider: "openai", page: 1 });
335
+ expect(page).toBeNull();
336
+ });
337
+
338
+ it("caps custom model page size at Discord select-option max", () => {
339
+ const data = createModelsProviderData({ openai: ["gpt-4o", "gpt-4.1"] });
340
+ const page = requireValue(
341
+ getDiscordModelPickerModelPage({ data, provider: "openai", pageSize: 999 }),
342
+ "expected model page when provider exists",
343
+ );
344
+ expect(page.pageSize).toBe(DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE);
345
+ });
346
+ });
347
+
348
+ describe("Discord model picker rendering", () => {
349
+ it("renders provider view on one page when provider count is <= 25", () => {
350
+ const entries: Record<string, string[]> = {};
351
+ for (let i = 1; i <= 22; i += 1) {
352
+ entries[`provider-${String(i).padStart(2, "0")}`] = [`model-${i}`];
353
+ }
354
+ entries["azure-openai-responses"] = ["gpt-4.1"];
355
+ entries["vercel-ai-gateway"] = ["gpt-4o-mini"];
356
+ const data = createModelsProviderData(entries);
357
+
358
+ const rendered = renderDiscordModelPickerProvidersView({
359
+ command: "models",
360
+ userId: "42",
361
+ data,
362
+ currentModel: "provider-01/model-1",
363
+ });
364
+
365
+ const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
366
+ content?: string;
367
+ components?: SerializedComponent[];
368
+ };
369
+
370
+ expect(payload.content).toBeUndefined();
371
+ const firstComponent = requireValue(
372
+ payload.components?.[0],
373
+ "provider view should render a container component",
374
+ );
375
+ expect(firstComponent.type).toBe(ComponentType.Container);
376
+
377
+ const rows = extractContainerRows(payload.components);
378
+ expect(rows.length).toBeGreaterThan(0);
379
+
380
+ const rowProviderCounts = rows.map(
381
+ (row) =>
382
+ (row.components ?? []).filter((component) => {
383
+ const parsed = parseDiscordModelPickerCustomId(component.custom_id ?? "");
384
+ return parsed?.action === "provider";
385
+ }).length,
386
+ );
387
+ expect(rowProviderCounts).toEqual([4, 5, 5, 5, 5]);
388
+
389
+ const allButtons = rows.flatMap((row) => row.components ?? []);
390
+ const providerButtons = allButtons.filter((component) => {
391
+ const parsed = parseDiscordModelPickerCustomId(component.custom_id ?? "");
392
+ return parsed?.action === "provider";
393
+ });
394
+ expect(providerButtons).toHaveLength(Object.keys(entries).length);
395
+ expect(allButtons.some((component) => (component.custom_id ?? "").includes(";a=nav;"))).toBe(
396
+ false,
397
+ );
398
+ });
399
+
400
+ it("does not render navigation buttons even when provider count exceeds one page", () => {
401
+ const entries: Record<string, string[]> = {};
402
+ for (let i = 1; i <= DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX + 4; i += 1) {
403
+ entries[`provider-${String(i).padStart(2, "0")}`] = [`model-${i}`];
404
+ }
405
+ const data = createModelsProviderData(entries);
406
+
407
+ const rendered = renderDiscordModelPickerProvidersView({
408
+ command: "models",
409
+ userId: "42",
410
+ data,
411
+ currentModel: "provider-01/model-1",
412
+ });
413
+
414
+ const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
415
+ components?: SerializedComponent[];
416
+ };
417
+
418
+ const rows = extractContainerRows(payload.components);
419
+ expect(rows.length).toBeGreaterThan(0);
420
+
421
+ const allButtons = rows.flatMap((row) => row.components ?? []);
422
+ expect(allButtons.some((component) => (component.custom_id ?? "").includes(";a=nav;"))).toBe(
423
+ false,
424
+ );
425
+ });
426
+
427
+ it("supports classic fallback rendering with content + action rows", () => {
428
+ const data = createModelsProviderData({ openai: ["gpt-4o"], anthropic: ["claude-sonnet-4-5"] });
429
+
430
+ const rendered = renderDiscordModelPickerProvidersView({
431
+ command: "model",
432
+ userId: "99",
433
+ data,
434
+ layout: "classic",
435
+ });
436
+
437
+ const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
438
+ content?: string;
439
+ components?: SerializedComponent[];
440
+ };
441
+
442
+ expect(payload.content).toContain("Model Picker");
443
+ const firstComponent = requireValue(
444
+ payload.components?.[0],
445
+ "classic provider view should render an action row",
446
+ );
447
+ expect(firstComponent.type).toBe(ComponentType.ActionRow);
448
+ });
449
+
450
+ it("preserves the stored model suffix spacing in Discord current-model text", () => {
451
+ const data = createModelsProviderData({ openai: [" gpt-5", "gpt-4o"] });
452
+
453
+ const rendered = renderDiscordModelPickerProvidersView({
454
+ command: "model",
455
+ userId: "99",
456
+ data,
457
+ currentModel: " OpenAI/ gpt-5 ",
458
+ layout: "classic",
459
+ });
460
+
461
+ const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
462
+ content?: string;
463
+ };
464
+
465
+ expect(payload.content).toContain("Current model: openai/ gpt-5");
466
+ });
467
+
468
+ it("renders model view with select menu and explicit submit button", () => {
469
+ const data = createModelsProviderData({
470
+ openai: ["gpt-4.1", "gpt-4o", "o3"],
471
+ anthropic: ["claude-sonnet-4-5"],
472
+ });
473
+
474
+ const rendered = renderDiscordModelPickerModelsView({
475
+ command: "models",
476
+ userId: "42",
477
+ data,
478
+ provider: "openai",
479
+ page: 1,
480
+ providerPage: 2,
481
+ currentModel: "openai/gpt-4o",
482
+ pendingModel: "openai/o3",
483
+ pendingModelIndex: 3,
484
+ });
485
+
486
+ const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
487
+ components?: SerializedComponent[];
488
+ };
489
+
490
+ const rows = extractContainerRows(payload.components);
491
+ expect(rows).toHaveLength(3);
492
+
493
+ const providerSelect = rows[0]?.components?.find(
494
+ (component) => component.type === DISCORD_STRING_SELECT_COMPONENT_TYPE,
495
+ );
496
+ if (!providerSelect) {
497
+ throw new Error("models view did not render a provider select");
498
+ }
499
+ expect(providerSelect.options?.length).toBe(2);
500
+ expect(providerSelect.options?.find((option) => option.value === "openai")?.default).toBe(true);
501
+ const parsedProviderState = parseDiscordModelPickerCustomId(providerSelect.custom_id ?? "");
502
+ expect(parsedProviderState?.action).toBe("provider");
503
+
504
+ const modelSelect = rows[1]?.components?.find(
505
+ (component) => component.type === DISCORD_STRING_SELECT_COMPONENT_TYPE,
506
+ );
507
+ if (!modelSelect) {
508
+ throw new Error("models view did not render a model select");
509
+ }
510
+ expect(modelSelect.options?.length).toBe(3);
511
+ expect(modelSelect.options?.find((option) => option.value === "o3")?.default).toBe(true);
512
+
513
+ const parsedModelSelectState = parseDiscordModelPickerCustomId(modelSelect.custom_id ?? "");
514
+ expect(parsedModelSelectState?.action).toBe("model");
515
+ expect(parsedModelSelectState?.provider).toBe("openai");
516
+
517
+ const navButtons = rows[2]?.components ?? [];
518
+ expect(navButtons).toHaveLength(3);
519
+
520
+ const cancelState = parseDiscordModelPickerCustomId(navButtons[0]?.custom_id ?? "");
521
+ expect(cancelState?.action).toBe("cancel");
522
+
523
+ const resetState = parseDiscordModelPickerCustomId(navButtons[1]?.custom_id ?? "");
524
+ expect(resetState?.action).toBe("reset");
525
+ expect(resetState?.provider).toBe("openai");
526
+
527
+ const submitState = parseDiscordModelPickerCustomId(navButtons[2]?.custom_id ?? "");
528
+ expect(submitState?.action).toBe("submit");
529
+ expect(submitState?.provider).toBe("openai");
530
+ expect(submitState?.modelIndex).toBe(3);
531
+ });
532
+
533
+ it("renders provider-compatible runtime choices in the model view", () => {
534
+ const data = createModelsProviderData({
535
+ openai: ["gpt-4.1", "gpt-4o", "o3"],
536
+ anthropic: ["claude-sonnet-4-5"],
537
+ });
538
+ data.runtimeChoicesByProvider = new Map([
539
+ [
540
+ "openai",
541
+ [
542
+ {
543
+ id: "pi",
544
+ label: "OpenClaw Pi Default",
545
+ description: "Use the built-in OpenClaw Pi runtime.",
546
+ },
547
+ {
548
+ id: "codex",
549
+ label: "codex",
550
+ description: "Run openai models through the codex harness.",
551
+ },
552
+ ],
553
+ ],
554
+ ]);
555
+
556
+ const rows = renderModelsViewRows({
557
+ command: "model",
558
+ userId: "42",
559
+ data,
560
+ provider: "openai",
561
+ page: 1,
562
+ providerPage: 1,
563
+ currentModel: "openai/gpt-4o",
564
+ currentRuntime: "pi",
565
+ pendingModel: "openai/o3",
566
+ pendingModelIndex: 3,
567
+ pendingRuntime: "codex",
568
+ });
569
+
570
+ expect(rows).toHaveLength(4);
571
+
572
+ const runtimeSelect = rows[1]?.components?.find(
573
+ (component) => component.type === DISCORD_STRING_SELECT_COMPONENT_TYPE,
574
+ );
575
+ if (!runtimeSelect) {
576
+ throw new Error("models view did not render a runtime select");
577
+ }
578
+ expect(runtimeSelect.options?.map((option) => option.value)).toEqual(["pi", "codex"]);
579
+ expect(runtimeSelect.options?.find((option) => option.value === "pi")?.label).toBe(
580
+ "OpenClaw Pi Default",
581
+ );
582
+ expect(runtimeSelect.options?.find((option) => option.value === "codex")?.default).toBe(true);
583
+
584
+ const submitButton = rows[3]?.components?.at(-1);
585
+ const submitState = requireValue(
586
+ parseDiscordModelPickerCustomId(submitButton?.custom_id ?? ""),
587
+ "submit custom id should parse",
588
+ );
589
+ expect(submitState.runtime).toBe("codex");
590
+ expect(submitState.modelIndex).toBe(3);
591
+ });
592
+
593
+ it("renders not-found model view with a back button", () => {
594
+ const data = createModelsProviderData({ openai: ["gpt-4o"] });
595
+
596
+ const rendered = renderDiscordModelPickerModelsView({
597
+ command: "model",
598
+ userId: "42",
599
+ data,
600
+ provider: "does-not-exist",
601
+ providerPage: 3,
602
+ });
603
+
604
+ const payload = serializePayload(toDiscordModelPickerMessagePayload(rendered)) as {
605
+ components?: SerializedComponent[];
606
+ };
607
+
608
+ const rows = extractContainerRows(payload.components);
609
+ expect(rows).toHaveLength(1);
610
+
611
+ const backButton = requireValue(
612
+ rows[0]?.components?.[0],
613
+ "models view should render a back button row",
614
+ );
615
+ expect(backButton.type).toBe(ComponentType.Button);
616
+
617
+ const state = requireValue(
618
+ parseDiscordModelPickerCustomId(backButton.custom_id ?? ""),
619
+ "back button custom id should parse",
620
+ );
621
+ expect(state.action).toBe("back");
622
+ expect(state.view).toBe("providers");
623
+ expect(state.page).toBe(3);
624
+ });
625
+
626
+ it("shows Recents button when quickModels are provided", () => {
627
+ const data = createModelsProviderData({
628
+ openai: ["gpt-4.1", "gpt-4o"],
629
+ anthropic: ["claude-sonnet-4-5"],
630
+ });
631
+
632
+ const rows = renderModelsViewRows({
633
+ command: "model",
634
+ userId: "42",
635
+ data,
636
+ provider: "openai",
637
+ page: 1,
638
+ providerPage: 1,
639
+ currentModel: "openai/gpt-4o",
640
+ quickModels: ["openai/gpt-4o", "anthropic/claude-sonnet-4-5"],
641
+ });
642
+ const buttonRow = rows[2];
643
+ const buttons = buttonRow?.components ?? [];
644
+ expect(buttons).toHaveLength(4);
645
+
646
+ const favoritesState = requireValue(
647
+ parseDiscordModelPickerCustomId(buttons[2]?.custom_id ?? ""),
648
+ "recents button custom id should parse",
649
+ );
650
+ expect(favoritesState.action).toBe("recents");
651
+ expect(favoritesState.view).toBe("recents");
652
+ });
653
+
654
+ it("omits Recents button when no quickModels", () => {
655
+ const data = createModelsProviderData({
656
+ openai: ["gpt-4.1", "gpt-4o"],
657
+ });
658
+
659
+ const rows = renderModelsViewRows({
660
+ command: "model",
661
+ userId: "42",
662
+ data,
663
+ provider: "openai",
664
+ page: 1,
665
+ providerPage: 1,
666
+ currentModel: "openai/gpt-4o",
667
+ });
668
+ const buttonRow = rows[2];
669
+ const buttons = buttonRow?.components ?? [];
670
+ expect(buttons).toHaveLength(3);
671
+
672
+ const allActions = buttons.map(
673
+ (b) => parseDiscordModelPickerCustomId(b?.custom_id ?? "")?.action,
674
+ );
675
+ expect(allActions).not.toContain("recents");
676
+ });
677
+ });
678
+
679
+ describe("Discord model picker recents view", () => {
680
+ it("renders one button per model with back button after divider", () => {
681
+ const data = createModelsProviderData({
682
+ openai: ["gpt-4.1", "gpt-4o"],
683
+ anthropic: ["claude-sonnet-4-5"],
684
+ });
685
+
686
+ // Default is openai/gpt-4.1 (first key in entries).
687
+ // Neither quickModel matches, so no deduping — 1 default + 2 recents + 1 back = 4 rows.
688
+ const rows = renderRecentsViewRows({
689
+ command: "model",
690
+ userId: "42",
691
+ data,
692
+ quickModels: ["openai/gpt-4o", "anthropic/claude-sonnet-4-5"],
693
+ currentModel: "openai/gpt-4o",
694
+ });
695
+ expect(rows).toHaveLength(4);
696
+
697
+ // First row: default model button (slot 1).
698
+ const defaultBtn = requireValue(
699
+ rows[0]?.components?.[0],
700
+ "recents view should render a default model button",
701
+ );
702
+ expect(defaultBtn.type).toBe(ComponentType.Button);
703
+ const defaultState = requireValue(
704
+ parseDiscordModelPickerCustomId(defaultBtn.custom_id ?? ""),
705
+ "default recents button custom id should parse",
706
+ );
707
+ expect(defaultState.action).toBe("submit");
708
+ expect(defaultState.view).toBe("recents");
709
+ expect(defaultState.recentSlot).toBe(1);
710
+
711
+ // Second row: first recent (slot 2).
712
+ const recentBtn1 = requireValue(
713
+ rows[1]?.components?.[0],
714
+ "recents view should render first recent button",
715
+ );
716
+ const recentState1 = requireValue(
717
+ parseDiscordModelPickerCustomId(recentBtn1.custom_id ?? ""),
718
+ "first recent custom id should parse",
719
+ );
720
+ expect(recentState1.recentSlot).toBe(2);
721
+
722
+ // Third row: second recent (slot 3).
723
+ const recentBtn2 = requireValue(
724
+ rows[2]?.components?.[0],
725
+ "recents view should render second recent button",
726
+ );
727
+ const recentState2 = requireValue(
728
+ parseDiscordModelPickerCustomId(recentBtn2.custom_id ?? ""),
729
+ "second recent custom id should parse",
730
+ );
731
+ expect(recentState2.recentSlot).toBe(3);
732
+
733
+ // Fourth row (after divider): Back button.
734
+ const backBtn = requireValue(
735
+ rows[3]?.components?.[0],
736
+ "recents view should render a back button",
737
+ );
738
+ const backState = requireValue(
739
+ parseDiscordModelPickerCustomId(backBtn.custom_id ?? ""),
740
+ "recents back button custom id should parse",
741
+ );
742
+ expect(backState.action).toBe("back");
743
+ expect(backState.view).toBe("models");
744
+ });
745
+
746
+ it("includes (default) suffix on default model button label", () => {
747
+ const data = createModelsProviderData({
748
+ openai: ["gpt-4o"],
749
+ });
750
+
751
+ const rows = renderRecentsViewRows({
752
+ command: "model",
753
+ userId: "42",
754
+ data,
755
+ quickModels: ["openai/gpt-4o"],
756
+ currentModel: "openai/gpt-4o",
757
+ });
758
+ const defaultBtn = requireValue(
759
+ rows[0]?.components?.[0] as { label?: string } | undefined,
760
+ "recents default row should include a button",
761
+ );
762
+ expect(defaultBtn.label).toContain("(default)");
763
+ });
764
+
765
+ it("deduplicates recents that match the default model", () => {
766
+ const data = createModelsProviderData({
767
+ openai: ["gpt-4o"],
768
+ anthropic: ["claude-sonnet-4-5"],
769
+ });
770
+ // Default is openai/gpt-4o (first key). quickModels contains the default.
771
+ const rows = renderRecentsViewRows({
772
+ command: "model",
773
+ userId: "42",
774
+ data,
775
+ quickModels: ["openai/gpt-4o", "anthropic/claude-sonnet-4-5"],
776
+ currentModel: "openai/gpt-4o",
777
+ });
778
+ // 1 default + 1 deduped recent + 1 back = 3 rows (openai/gpt-4o not shown twice)
779
+ expect(rows).toHaveLength(3);
780
+
781
+ const defaultBtn = requireValue(
782
+ rows[0]?.components?.[0] as { label?: string } | undefined,
783
+ "deduped recents should keep the default button",
784
+ );
785
+ expect(defaultBtn.label).toContain("openai/gpt-4o");
786
+ expect(defaultBtn.label).toContain("(default)");
787
+
788
+ const recentBtn = requireValue(
789
+ rows[1]?.components?.[0] as { label?: string } | undefined,
790
+ "deduped recents should keep the non-default recent button",
791
+ );
792
+ expect(recentBtn.label).toContain("anthropic/claude-sonnet-4-5");
793
+ });
794
+ });