@gakr-gakr/discord 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/account-inspect-api.ts +6 -0
  2. package/action-runtime-api.ts +1 -0
  3. package/api.ts +130 -0
  4. package/autobot.plugin.json +15 -0
  5. package/channel-config-api.ts +1 -0
  6. package/channel-plugin-api.ts +3 -0
  7. package/config-api.ts +4 -0
  8. package/configured-state.ts +6 -0
  9. package/contract-api.ts +21 -0
  10. package/directory-contract-api.ts +4 -0
  11. package/doctor-contract-api.ts +1 -0
  12. package/index.ts +24 -0
  13. package/package.json +79 -0
  14. package/runtime-api.actions.ts +15 -0
  15. package/runtime-api.lookup.ts +22 -0
  16. package/runtime-api.monitor.ts +50 -0
  17. package/runtime-api.send.ts +79 -0
  18. package/runtime-api.threads.ts +31 -0
  19. package/runtime-api.ts +181 -0
  20. package/runtime-setter-api.ts +3 -0
  21. package/secret-contract-api.ts +4 -0
  22. package/security-audit-contract-api.ts +1 -0
  23. package/security-contract-api.ts +4 -0
  24. package/session-key-api.ts +1 -0
  25. package/setup-entry.ts +9 -0
  26. package/setup-plugin-api.ts +3 -0
  27. package/src/account-inspect.ts +131 -0
  28. package/src/accounts.ts +205 -0
  29. package/src/actions/handle-action.guild-admin.ts +421 -0
  30. package/src/actions/handle-action.ts +402 -0
  31. package/src/actions/runtime.guild.ts +446 -0
  32. package/src/actions/runtime.messaging.messages.ts +226 -0
  33. package/src/actions/runtime.messaging.reactions.ts +67 -0
  34. package/src/actions/runtime.messaging.runtime.ts +73 -0
  35. package/src/actions/runtime.messaging.send.ts +336 -0
  36. package/src/actions/runtime.messaging.shared.ts +97 -0
  37. package/src/actions/runtime.messaging.ts +37 -0
  38. package/src/actions/runtime.moderation-shared.ts +48 -0
  39. package/src/actions/runtime.moderation.ts +116 -0
  40. package/src/actions/runtime.presence.ts +117 -0
  41. package/src/actions/runtime.shared.ts +86 -0
  42. package/src/actions/runtime.ts +87 -0
  43. package/src/api.ts +219 -0
  44. package/src/approval-handler.runtime.ts +636 -0
  45. package/src/approval-native.ts +219 -0
  46. package/src/approval-runtime.ts +14 -0
  47. package/src/approval-shared.ts +56 -0
  48. package/src/audit-core.ts +178 -0
  49. package/src/audit.ts +32 -0
  50. package/src/channel-actions.runtime.ts +1 -0
  51. package/src/channel-actions.ts +254 -0
  52. package/src/channel-api.ts +29 -0
  53. package/src/channel.conversation.ts +159 -0
  54. package/src/channel.loaders.ts +50 -0
  55. package/src/channel.runtime.ts +1 -0
  56. package/src/channel.setup.ts +12 -0
  57. package/src/channel.ts +728 -0
  58. package/src/chunk.ts +321 -0
  59. package/src/client.ts +143 -0
  60. package/src/component-custom-id.ts +72 -0
  61. package/src/components-registry.ts +356 -0
  62. package/src/components.builders.ts +410 -0
  63. package/src/components.modal.ts +124 -0
  64. package/src/components.parse.ts +407 -0
  65. package/src/components.ts +54 -0
  66. package/src/components.types.ts +187 -0
  67. package/src/config-schema.ts +6 -0
  68. package/src/config-ui-hints.ts +354 -0
  69. package/src/conversation-identity.ts +58 -0
  70. package/src/delivery-retry.ts +56 -0
  71. package/src/directory-cache.ts +116 -0
  72. package/src/directory-config.ts +58 -0
  73. package/src/directory-live.ts +135 -0
  74. package/src/doctor-contract.ts +477 -0
  75. package/src/doctor-shared.ts +5 -0
  76. package/src/doctor.ts +340 -0
  77. package/src/draft-chunking.ts +43 -0
  78. package/src/draft-stream.ts +162 -0
  79. package/src/error-body.ts +38 -0
  80. package/src/exec-approvals.ts +110 -0
  81. package/src/gateway-logging.ts +67 -0
  82. package/src/group-policy.ts +113 -0
  83. package/src/guilds.ts +29 -0
  84. package/src/inbound-event-delivery.ts +135 -0
  85. package/src/interactive-dispatch.ts +104 -0
  86. package/src/internal/api.commands.ts +51 -0
  87. package/src/internal/api.guild.ts +164 -0
  88. package/src/internal/api.interactions.ts +53 -0
  89. package/src/internal/api.messages.ts +113 -0
  90. package/src/internal/api.reactions.ts +38 -0
  91. package/src/internal/api.ts +61 -0
  92. package/src/internal/api.users.ts +19 -0
  93. package/src/internal/api.webhooks.ts +13 -0
  94. package/src/internal/client.ts +310 -0
  95. package/src/internal/command-deploy.ts +352 -0
  96. package/src/internal/commands.ts +188 -0
  97. package/src/internal/components.base.ts +65 -0
  98. package/src/internal/components.message.ts +279 -0
  99. package/src/internal/components.modal.ts +95 -0
  100. package/src/internal/components.ts +31 -0
  101. package/src/internal/discord.ts +11 -0
  102. package/src/internal/embeds.ts +35 -0
  103. package/src/internal/entity-cache.ts +98 -0
  104. package/src/internal/event-queue.ts +185 -0
  105. package/src/internal/gateway-close-codes.ts +25 -0
  106. package/src/internal/gateway-dispatch.ts +96 -0
  107. package/src/internal/gateway-identify-limiter.ts +26 -0
  108. package/src/internal/gateway-lifecycle.ts +75 -0
  109. package/src/internal/gateway-rate-limit.ts +104 -0
  110. package/src/internal/gateway.ts +479 -0
  111. package/src/internal/interaction-dispatch.ts +162 -0
  112. package/src/internal/interaction-options.ts +98 -0
  113. package/src/internal/interaction-response.ts +53 -0
  114. package/src/internal/interactions.ts +378 -0
  115. package/src/internal/listeners.ts +91 -0
  116. package/src/internal/modal-fields.ts +95 -0
  117. package/src/internal/payload.ts +69 -0
  118. package/src/internal/rest-body.ts +115 -0
  119. package/src/internal/rest-errors.ts +88 -0
  120. package/src/internal/rest-routes.ts +50 -0
  121. package/src/internal/rest-scheduler.ts +557 -0
  122. package/src/internal/rest.ts +322 -0
  123. package/src/internal/schemas.ts +36 -0
  124. package/src/internal/structures.ts +280 -0
  125. package/src/internal/test-builders.test-support.ts +167 -0
  126. package/src/internal/voice.ts +49 -0
  127. package/src/media-detection.ts +28 -0
  128. package/src/mentions.ts +147 -0
  129. package/src/monitor/ack-reactions.ts +70 -0
  130. package/src/monitor/agent-components-auth.ts +7 -0
  131. package/src/monitor/agent-components-context.ts +154 -0
  132. package/src/monitor/agent-components-data.ts +224 -0
  133. package/src/monitor/agent-components-dm-auth.ts +177 -0
  134. package/src/monitor/agent-components-guild-auth.ts +322 -0
  135. package/src/monitor/agent-components-helpers.runtime.ts +3 -0
  136. package/src/monitor/agent-components-helpers.ts +34 -0
  137. package/src/monitor/agent-components-reply.ts +10 -0
  138. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  139. package/src/monitor/agent-components.dispatch.ts +359 -0
  140. package/src/monitor/agent-components.handlers.ts +303 -0
  141. package/src/monitor/agent-components.modal.ts +160 -0
  142. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  143. package/src/monitor/agent-components.runtime.ts +14 -0
  144. package/src/monitor/agent-components.system-controls.ts +215 -0
  145. package/src/monitor/agent-components.ts +70 -0
  146. package/src/monitor/agent-components.types.ts +58 -0
  147. package/src/monitor/agent-components.wildcard-controls.ts +171 -0
  148. package/src/monitor/allow-list.ts +631 -0
  149. package/src/monitor/auto-presence.ts +356 -0
  150. package/src/monitor/channel-access.ts +102 -0
  151. package/src/monitor/commands.ts +9 -0
  152. package/src/monitor/dm-command-auth.ts +259 -0
  153. package/src/monitor/dm-command-decision.ts +49 -0
  154. package/src/monitor/exec-approvals.ts +161 -0
  155. package/src/monitor/format.ts +45 -0
  156. package/src/monitor/gateway-handle.ts +34 -0
  157. package/src/monitor/gateway-metadata.ts +298 -0
  158. package/src/monitor/gateway-plugin.ts +302 -0
  159. package/src/monitor/gateway-registry.ts +37 -0
  160. package/src/monitor/gateway-supervisor.ts +206 -0
  161. package/src/monitor/inbound-context.ts +95 -0
  162. package/src/monitor/inbound-dedupe.ts +79 -0
  163. package/src/monitor/inbound-job.ts +118 -0
  164. package/src/monitor/listeners.queue.ts +91 -0
  165. package/src/monitor/listeners.reactions.ts +594 -0
  166. package/src/monitor/listeners.ts +150 -0
  167. package/src/monitor/message-channel-info.ts +96 -0
  168. package/src/monitor/message-forwarded.ts +114 -0
  169. package/src/monitor/message-handler.batch-gate.ts +19 -0
  170. package/src/monitor/message-handler.context.ts +492 -0
  171. package/src/monitor/message-handler.dm-preflight.ts +119 -0
  172. package/src/monitor/message-handler.draft-preview.ts +436 -0
  173. package/src/monitor/message-handler.hydration.ts +198 -0
  174. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  175. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  176. package/src/monitor/message-handler.preflight-channel-context.ts +58 -0
  177. package/src/monitor/message-handler.preflight-context.ts +54 -0
  178. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  179. package/src/monitor/message-handler.preflight-history.ts +23 -0
  180. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  181. package/src/monitor/message-handler.preflight-pluralkit.ts +28 -0
  182. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  183. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  184. package/src/monitor/message-handler.preflight.ts +822 -0
  185. package/src/monitor/message-handler.preflight.types.ts +115 -0
  186. package/src/monitor/message-handler.process.ts +1033 -0
  187. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  188. package/src/monitor/message-handler.ts +309 -0
  189. package/src/monitor/message-media.ts +536 -0
  190. package/src/monitor/message-run-queue.ts +101 -0
  191. package/src/monitor/message-text.ts +171 -0
  192. package/src/monitor/message-utils.ts +34 -0
  193. package/src/monitor/model-picker-preferences.ts +184 -0
  194. package/src/monitor/model-picker.state.ts +364 -0
  195. package/src/monitor/model-picker.test-utils.ts +26 -0
  196. package/src/monitor/model-picker.ts +38 -0
  197. package/src/monitor/model-picker.view.ts +722 -0
  198. package/src/monitor/native-command-agent-reply.ts +125 -0
  199. package/src/monitor/native-command-arg-ui.ts +233 -0
  200. package/src/monitor/native-command-auth.ts +309 -0
  201. package/src/monitor/native-command-bypass.ts +13 -0
  202. package/src/monitor/native-command-context.ts +109 -0
  203. package/src/monitor/native-command-dispatch.ts +35 -0
  204. package/src/monitor/native-command-model-picker-apply.ts +209 -0
  205. package/src/monitor/native-command-model-picker-interaction.ts +516 -0
  206. package/src/monitor/native-command-model-picker-ui.ts +357 -0
  207. package/src/monitor/native-command-reply.ts +185 -0
  208. package/src/monitor/native-command-route.ts +91 -0
  209. package/src/monitor/native-command-status.ts +76 -0
  210. package/src/monitor/native-command-ui.ts +26 -0
  211. package/src/monitor/native-command-ui.types.ts +20 -0
  212. package/src/monitor/native-command.args.ts +45 -0
  213. package/src/monitor/native-command.options.ts +153 -0
  214. package/src/monitor/native-command.runtime.ts +51 -0
  215. package/src/monitor/native-command.ts +747 -0
  216. package/src/monitor/native-command.types.ts +9 -0
  217. package/src/monitor/native-interaction-channel-context.ts +50 -0
  218. package/src/monitor/preflight-audio.runtime.ts +9 -0
  219. package/src/monitor/preflight-audio.ts +130 -0
  220. package/src/monitor/presence-cache.ts +61 -0
  221. package/src/monitor/presence.ts +50 -0
  222. package/src/monitor/provider-session.runtime.ts +12 -0
  223. package/src/monitor/provider.acp.ts +89 -0
  224. package/src/monitor/provider.allowlist.ts +398 -0
  225. package/src/monitor/provider.cleanup.ts +41 -0
  226. package/src/monitor/provider.commands.ts +129 -0
  227. package/src/monitor/provider.config-log.ts +45 -0
  228. package/src/monitor/provider.deploy-errors.ts +362 -0
  229. package/src/monitor/provider.deploy.ts +221 -0
  230. package/src/monitor/provider.interactions.ts +160 -0
  231. package/src/monitor/provider.lifecycle.ts +562 -0
  232. package/src/monitor/provider.runtime.ts +1 -0
  233. package/src/monitor/provider.startup-log.ts +32 -0
  234. package/src/monitor/provider.startup.ts +323 -0
  235. package/src/monitor/provider.ts +688 -0
  236. package/src/monitor/reply-context.ts +64 -0
  237. package/src/monitor/reply-delivery.ts +216 -0
  238. package/src/monitor/reply-safety.ts +96 -0
  239. package/src/monitor/rest-fetch.ts +97 -0
  240. package/src/monitor/route-resolution.ts +140 -0
  241. package/src/monitor/sender-identity.ts +81 -0
  242. package/src/monitor/startup-status.ts +10 -0
  243. package/src/monitor/status.ts +22 -0
  244. package/src/monitor/system-events.ts +55 -0
  245. package/src/monitor/thread-bindings.config.ts +35 -0
  246. package/src/monitor/thread-bindings.discord-api.ts +310 -0
  247. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  248. package/src/monitor/thread-bindings.manager.ts +554 -0
  249. package/src/monitor/thread-bindings.messages.ts +6 -0
  250. package/src/monitor/thread-bindings.persona.ts +25 -0
  251. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  252. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  253. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  254. package/src/monitor/thread-bindings.state.ts +540 -0
  255. package/src/monitor/thread-bindings.ts +48 -0
  256. package/src/monitor/thread-bindings.types.ts +83 -0
  257. package/src/monitor/thread-channel-context.ts +112 -0
  258. package/src/monitor/thread-session-close.ts +63 -0
  259. package/src/monitor/thread-title.ts +181 -0
  260. package/src/monitor/threading.auto-thread.ts +287 -0
  261. package/src/monitor/threading.cache.ts +45 -0
  262. package/src/monitor/threading.starter.ts +288 -0
  263. package/src/monitor/threading.ts +20 -0
  264. package/src/monitor/threading.types.ts +102 -0
  265. package/src/monitor/timeouts.ts +84 -0
  266. package/src/monitor/typing.ts +17 -0
  267. package/src/monitor.gateway.ts +75 -0
  268. package/src/monitor.ts +28 -0
  269. package/src/network-config.ts +79 -0
  270. package/src/normalize.ts +86 -0
  271. package/src/outbound-adapter.ts +327 -0
  272. package/src/outbound-approval.ts +29 -0
  273. package/src/outbound-components.ts +86 -0
  274. package/src/outbound-payload.ts +208 -0
  275. package/src/outbound-send-context.ts +92 -0
  276. package/src/outbound-session-route.ts +72 -0
  277. package/src/pluralkit.ts +58 -0
  278. package/src/preview-streaming.ts +18 -0
  279. package/src/probe.runtime.ts +1 -0
  280. package/src/probe.ts +237 -0
  281. package/src/proxy-fetch.ts +92 -0
  282. package/src/proxy-request-client.ts +21 -0
  283. package/src/recipient-resolution.ts +39 -0
  284. package/src/resolve-allowlist-common.ts +39 -0
  285. package/src/resolve-channels.ts +369 -0
  286. package/src/resolve-users.ts +184 -0
  287. package/src/retry.ts +98 -0
  288. package/src/runtime-api.ts +64 -0
  289. package/src/runtime-config.ts +16 -0
  290. package/src/runtime.ts +23 -0
  291. package/src/secret-config-contract.ts +140 -0
  292. package/src/security-audit.runtime.ts +1 -0
  293. package/src/security-audit.ts +208 -0
  294. package/src/security-contract.ts +47 -0
  295. package/src/security-doctor.ts +20 -0
  296. package/src/security.ts +60 -0
  297. package/src/send-target-parsing.ts +14 -0
  298. package/src/send.channels.ts +139 -0
  299. package/src/send.components.ts +391 -0
  300. package/src/send.emojis-stickers.ts +57 -0
  301. package/src/send.guild.ts +170 -0
  302. package/src/send.message-request.ts +112 -0
  303. package/src/send.messages.ts +229 -0
  304. package/src/send.outbound.ts +459 -0
  305. package/src/send.permissions.ts +283 -0
  306. package/src/send.reactions.ts +155 -0
  307. package/src/send.receipt.ts +69 -0
  308. package/src/send.shared.ts +469 -0
  309. package/src/send.ts +82 -0
  310. package/src/send.types.ts +191 -0
  311. package/src/send.typing.ts +9 -0
  312. package/src/send.voice.ts +140 -0
  313. package/src/send.webhook.ts +137 -0
  314. package/src/session-contract.ts +3 -0
  315. package/src/session-key-normalization.ts +47 -0
  316. package/src/setup-account-state.ts +144 -0
  317. package/src/setup-adapter.ts +14 -0
  318. package/src/setup-core.ts +215 -0
  319. package/src/setup-runtime-helpers.ts +10 -0
  320. package/src/setup-surface.ts +132 -0
  321. package/src/shared-interactive.ts +167 -0
  322. package/src/shared.ts +197 -0
  323. package/src/status-issues.ts +201 -0
  324. package/src/subagent-hooks.ts +232 -0
  325. package/src/target-parsing.ts +70 -0
  326. package/src/target-resolver.ts +129 -0
  327. package/src/targets.ts +12 -0
  328. package/src/token.ts +107 -0
  329. package/src/ui-colors.ts +27 -0
  330. package/src/ui.ts +20 -0
  331. package/src/voice/access.ts +126 -0
  332. package/src/voice/audio.ts +249 -0
  333. package/src/voice/capture-state.ts +120 -0
  334. package/src/voice/command.ts +284 -0
  335. package/src/voice/config.ts +8 -0
  336. package/src/voice/ingress.ts +164 -0
  337. package/src/voice/manager.runtime.ts +14 -0
  338. package/src/voice/manager.ts +1155 -0
  339. package/src/voice/prompt.ts +22 -0
  340. package/src/voice/realtime.ts +1370 -0
  341. package/src/voice/receive-recovery.ts +159 -0
  342. package/src/voice/sanitize.ts +29 -0
  343. package/src/voice/sdk-runtime.ts +14 -0
  344. package/src/voice/segment.ts +160 -0
  345. package/src/voice/session.ts +81 -0
  346. package/src/voice/speaker-context.ts +127 -0
  347. package/src/voice/tts.ts +151 -0
  348. package/src/voice-message.ts +474 -0
  349. package/subagent-hooks-api.ts +27 -0
  350. package/test-api.ts +4 -0
  351. package/thread-binding-api.ts +1 -0
  352. package/timeouts.ts +6 -0
  353. package/tsconfig.json +16 -0
@@ -0,0 +1,722 @@
1
+ import type { APISelectMenuOption } from "discord-api-types/v10";
2
+ import { ButtonStyle } from "discord-api-types/v10";
3
+ import type {
4
+ ModelsProviderData,
5
+ ModelsRuntimeChoice,
6
+ } from "autobot/plugin-sdk/models-provider-runtime";
7
+ import { normalizeProviderId } from "autobot/plugin-sdk/provider-model-shared";
8
+ import {
9
+ Button,
10
+ Container,
11
+ Row,
12
+ Separator,
13
+ StringSelectMenu,
14
+ TextDisplay,
15
+ type MessagePayloadObject,
16
+ type TopLevelComponents,
17
+ } from "../internal/discord.js";
18
+ import {
19
+ buildDiscordModelPickerCustomId,
20
+ DISCORD_COMPONENT_MAX_BUTTONS_PER_ROW,
21
+ getDiscordModelPickerModelPage,
22
+ getDiscordModelPickerProviderPage,
23
+ normalizeModelPickerPage,
24
+ type DiscordModelPickerCommandContext,
25
+ type DiscordModelPickerLayout,
26
+ type DiscordModelPickerModelPage,
27
+ type DiscordModelPickerPage,
28
+ type DiscordModelPickerProviderItem,
29
+ } from "./model-picker.state.js";
30
+
31
+ const DISCORD_PROVIDER_BUTTON_LABEL_MAX_CHARS = 18;
32
+
33
+ type DiscordModelPickerButtonOptions = {
34
+ label: string;
35
+ customId: string;
36
+ style?: ButtonStyle;
37
+ disabled?: boolean;
38
+ };
39
+
40
+ type DiscordModelPickerCurrentModelRef = {
41
+ provider: string;
42
+ model: string;
43
+ };
44
+
45
+ type DiscordModelPickerRow = Row<Button> | Row<StringSelectMenu>;
46
+
47
+ type DiscordModelPickerRenderShellParams = {
48
+ layout: DiscordModelPickerLayout;
49
+ title: string;
50
+ detailLines: string[];
51
+ rows: DiscordModelPickerRow[];
52
+ footer?: string;
53
+ /** Text shown after the divider but before the interactive rows. */
54
+ preRowText?: string;
55
+ /** Extra rows appended after the main rows, preceded by a divider. */
56
+ trailingRows?: DiscordModelPickerRow[];
57
+ };
58
+
59
+ export type DiscordModelPickerRenderedView = {
60
+ layout: DiscordModelPickerLayout;
61
+ content?: string;
62
+ components: TopLevelComponents[];
63
+ };
64
+
65
+ export type DiscordModelPickerProviderViewParams = {
66
+ command: DiscordModelPickerCommandContext;
67
+ userId: string;
68
+ data: ModelsProviderData;
69
+ page?: number;
70
+ currentModel?: string;
71
+ layout?: DiscordModelPickerLayout;
72
+ };
73
+
74
+ export type DiscordModelPickerModelViewParams = {
75
+ command: DiscordModelPickerCommandContext;
76
+ userId: string;
77
+ data: ModelsProviderData;
78
+ provider: string;
79
+ page?: number;
80
+ providerPage?: number;
81
+ currentModel?: string;
82
+ currentRuntime?: string;
83
+ pendingModel?: string;
84
+ pendingModelIndex?: number;
85
+ pendingRuntime?: string;
86
+ quickModels?: string[];
87
+ layout?: DiscordModelPickerLayout;
88
+ };
89
+
90
+ function parseCurrentModelRef(raw?: string): DiscordModelPickerCurrentModelRef | null {
91
+ const trimmed = raw?.trim();
92
+ const match = trimmed?.match(/^([^/]+)\/(.+)$/u);
93
+ if (!match) {
94
+ return null;
95
+ }
96
+ const provider = normalizeProviderId(match[1]);
97
+ // Preserve the model suffix exactly as entered after "/" so select defaults
98
+ // continue to mirror the stored ref for Discord interactions.
99
+ const model = match[2];
100
+ if (!provider || !model) {
101
+ return null;
102
+ }
103
+ return { provider, model };
104
+ }
105
+
106
+ function formatCurrentModelLine(currentModel?: string): string {
107
+ const parsed = parseCurrentModelRef(currentModel);
108
+ if (!parsed) {
109
+ return "Current model: default";
110
+ }
111
+ return `Current model: ${parsed.provider}/${parsed.model}`;
112
+ }
113
+
114
+ function formatProviderButtonLabel(provider: string): string {
115
+ if (provider.length <= DISCORD_PROVIDER_BUTTON_LABEL_MAX_CHARS) {
116
+ return provider;
117
+ }
118
+ return `${provider.slice(0, DISCORD_PROVIDER_BUTTON_LABEL_MAX_CHARS - 1)}…`;
119
+ }
120
+
121
+ function chunkProvidersForRows(
122
+ items: DiscordModelPickerProviderItem[],
123
+ ): DiscordModelPickerProviderItem[][] {
124
+ if (items.length === 0) {
125
+ return [];
126
+ }
127
+
128
+ const rowCount = Math.max(1, Math.ceil(items.length / DISCORD_COMPONENT_MAX_BUTTONS_PER_ROW));
129
+ const minPerRow = Math.floor(items.length / rowCount);
130
+ const rowsWithExtraItem = items.length % rowCount;
131
+
132
+ const counts = Array.from({ length: rowCount }, (_, index) =>
133
+ index < rowCount - rowsWithExtraItem ? minPerRow : minPerRow + 1,
134
+ );
135
+
136
+ const rows: DiscordModelPickerProviderItem[][] = [];
137
+ let cursor = 0;
138
+ for (const count of counts) {
139
+ rows.push(items.slice(cursor, cursor + count));
140
+ cursor += count;
141
+ }
142
+ return rows;
143
+ }
144
+
145
+ function createModelPickerButton(params: DiscordModelPickerButtonOptions): Button {
146
+ class DiscordModelPickerButton extends Button {
147
+ label = params.label;
148
+ customId = params.customId;
149
+ override style = params.style ?? ButtonStyle.Secondary;
150
+ override disabled = params.disabled ?? false;
151
+ }
152
+ return new DiscordModelPickerButton();
153
+ }
154
+
155
+ function createModelSelect(params: {
156
+ customId: string;
157
+ options: APISelectMenuOption[];
158
+ placeholder?: string;
159
+ disabled?: boolean;
160
+ }): StringSelectMenu {
161
+ class DiscordModelPickerSelect extends StringSelectMenu {
162
+ customId = params.customId;
163
+ override options = params.options;
164
+ override minValues = 1;
165
+ override maxValues = 1;
166
+ override placeholder = params.placeholder;
167
+ override disabled = params.disabled ?? false;
168
+ }
169
+ return new DiscordModelPickerSelect();
170
+ }
171
+
172
+ function getRuntimeChoices(params: {
173
+ data: ModelsProviderData;
174
+ provider: string;
175
+ }): ModelsRuntimeChoice[] {
176
+ const choices = params.data.runtimeChoicesByProvider?.get(normalizeProviderId(params.provider));
177
+ if (choices?.length) {
178
+ return choices;
179
+ }
180
+ return [
181
+ {
182
+ id: "pi",
183
+ label: "AutoBot Pi Default",
184
+ description: "Use the built-in AutoBot Pi runtime.",
185
+ },
186
+ ];
187
+ }
188
+
189
+ function resolveSelectedRuntime(params: {
190
+ data: ModelsProviderData;
191
+ provider: string;
192
+ currentRuntime?: string;
193
+ pendingRuntime?: string;
194
+ }): string {
195
+ const choices = getRuntimeChoices({ data: params.data, provider: params.provider });
196
+ const allowed = new Set(choices.map((choice) => choice.id));
197
+ const pending = params.pendingRuntime?.trim();
198
+ if (pending && allowed.has(pending)) {
199
+ return pending;
200
+ }
201
+ const current = params.currentRuntime?.trim();
202
+ if (current && allowed.has(current)) {
203
+ return current;
204
+ }
205
+ return choices[0]?.id ?? "pi";
206
+ }
207
+
208
+ function resolveExplicitRuntimeState(params: {
209
+ choices: ModelsRuntimeChoice[];
210
+ currentRuntime?: string;
211
+ pendingRuntime?: string;
212
+ }): string | undefined {
213
+ const allowed = new Set(params.choices.map((choice) => choice.id));
214
+ const pending = params.pendingRuntime?.trim();
215
+ if (pending && allowed.has(pending)) {
216
+ return pending;
217
+ }
218
+ const current = params.currentRuntime?.trim();
219
+ if (current && current !== "auto" && current !== "default" && allowed.has(current)) {
220
+ return current;
221
+ }
222
+ return undefined;
223
+ }
224
+
225
+ function buildRenderedShell(
226
+ params: DiscordModelPickerRenderShellParams,
227
+ ): DiscordModelPickerRenderedView {
228
+ if (params.layout === "classic") {
229
+ const lines = [params.title, ...params.detailLines, "", params.footer].filter(Boolean);
230
+ return {
231
+ layout: "classic",
232
+ content: lines.join("\n"),
233
+ components: params.rows,
234
+ };
235
+ }
236
+
237
+ const containerComponents: Array<TextDisplay | Separator | DiscordModelPickerRow> = [
238
+ new TextDisplay(`## ${params.title}`),
239
+ ];
240
+ if (params.detailLines.length > 0) {
241
+ containerComponents.push(new TextDisplay(params.detailLines.join("\n")));
242
+ }
243
+ containerComponents.push(new Separator({ divider: true, spacing: "small" }));
244
+ if (params.preRowText) {
245
+ containerComponents.push(new TextDisplay(params.preRowText));
246
+ }
247
+ containerComponents.push(...params.rows);
248
+ if (params.trailingRows && params.trailingRows.length > 0) {
249
+ containerComponents.push(new Separator({ divider: true, spacing: "small" }));
250
+ containerComponents.push(...params.trailingRows);
251
+ }
252
+ if (params.footer) {
253
+ containerComponents.push(new Separator({ divider: false, spacing: "small" }));
254
+ containerComponents.push(new TextDisplay(`-# ${params.footer}`));
255
+ }
256
+
257
+ const container = new Container(containerComponents);
258
+ return {
259
+ layout: "v2",
260
+ components: [container],
261
+ };
262
+ }
263
+
264
+ function buildProviderRows(params: {
265
+ command: DiscordModelPickerCommandContext;
266
+ userId: string;
267
+ page: DiscordModelPickerPage<DiscordModelPickerProviderItem>;
268
+ currentProvider?: string;
269
+ }): Row<Button>[] {
270
+ const rows = chunkProvidersForRows(params.page.items).map(
271
+ (providers) =>
272
+ new Row(
273
+ providers.map((provider) => {
274
+ const style =
275
+ provider.id === params.currentProvider ? ButtonStyle.Primary : ButtonStyle.Secondary;
276
+ return createModelPickerButton({
277
+ label: formatProviderButtonLabel(provider.id),
278
+ style,
279
+ customId: buildDiscordModelPickerCustomId({
280
+ command: params.command,
281
+ action: "provider",
282
+ view: "models",
283
+ provider: provider.id,
284
+ page: params.page.page,
285
+ userId: params.userId,
286
+ }),
287
+ });
288
+ }),
289
+ ),
290
+ );
291
+
292
+ return rows;
293
+ }
294
+
295
+ function buildModelRows(params: {
296
+ command: DiscordModelPickerCommandContext;
297
+ userId: string;
298
+ data: ModelsProviderData;
299
+ providerPage: number;
300
+ modelPage: DiscordModelPickerModelPage;
301
+ currentModel?: string;
302
+ currentRuntime?: string;
303
+ pendingModel?: string;
304
+ pendingModelIndex?: number;
305
+ pendingRuntime?: string;
306
+ quickModels?: string[];
307
+ }): { rows: DiscordModelPickerRow[]; buttonRow: Row<Button> } {
308
+ const parsedCurrentModel = parseCurrentModelRef(params.currentModel);
309
+ const parsedPendingModel = parseCurrentModelRef(params.pendingModel);
310
+ const rows: DiscordModelPickerRow[] = [];
311
+
312
+ const hasQuickModels = (params.quickModels ?? []).length > 0;
313
+
314
+ const providerPage = getDiscordModelPickerProviderPage({
315
+ data: params.data,
316
+ page: params.providerPage,
317
+ });
318
+ const providerOptions: APISelectMenuOption[] = providerPage.items.map((provider) => ({
319
+ label: provider.id,
320
+ value: provider.id,
321
+ default: provider.id === params.modelPage.provider,
322
+ }));
323
+
324
+ rows.push(
325
+ new Row([
326
+ createModelSelect({
327
+ customId: buildDiscordModelPickerCustomId({
328
+ command: params.command,
329
+ action: "provider",
330
+ view: "models",
331
+ provider: params.modelPage.provider,
332
+ page: providerPage.page,
333
+ providerPage: providerPage.page,
334
+ userId: params.userId,
335
+ }),
336
+ options: providerOptions,
337
+ placeholder: "Select provider",
338
+ }),
339
+ ]),
340
+ );
341
+
342
+ const runtimeChoices = getRuntimeChoices({
343
+ data: params.data,
344
+ provider: params.modelPage.provider,
345
+ });
346
+ const selectedRuntime = resolveSelectedRuntime({
347
+ data: params.data,
348
+ provider: params.modelPage.provider,
349
+ currentRuntime: params.currentRuntime,
350
+ pendingRuntime: params.pendingRuntime,
351
+ });
352
+ const stateRuntime = resolveExplicitRuntimeState({
353
+ choices: runtimeChoices,
354
+ currentRuntime: params.currentRuntime,
355
+ pendingRuntime: params.pendingRuntime,
356
+ });
357
+
358
+ if (runtimeChoices.length > 1) {
359
+ rows.push(
360
+ new Row([
361
+ createModelSelect({
362
+ customId: buildDiscordModelPickerCustomId({
363
+ command: params.command,
364
+ action: "runtime",
365
+ view: "models",
366
+ provider: params.modelPage.provider,
367
+ runtime: selectedRuntime,
368
+ page: params.modelPage.page,
369
+ providerPage: providerPage.page,
370
+ modelIndex: params.pendingModelIndex,
371
+ userId: params.userId,
372
+ }),
373
+ options: runtimeChoices.map((choice) => {
374
+ const option: APISelectMenuOption = {
375
+ label: choice.label,
376
+ value: choice.id,
377
+ default: choice.id === selectedRuntime,
378
+ };
379
+ if (choice.description) {
380
+ option.description = choice.description;
381
+ }
382
+ return option;
383
+ }),
384
+ placeholder: "Select runtime",
385
+ }),
386
+ ]),
387
+ );
388
+ }
389
+
390
+ const selectedModelRef = parsedPendingModel ?? parsedCurrentModel;
391
+ const modelOptions: APISelectMenuOption[] = params.modelPage.items.map((model) => ({
392
+ label: model,
393
+ value: model,
394
+ default: selectedModelRef
395
+ ? selectedModelRef.provider === params.modelPage.provider && selectedModelRef.model === model
396
+ : false,
397
+ }));
398
+
399
+ rows.push(
400
+ new Row([
401
+ createModelSelect({
402
+ customId: buildDiscordModelPickerCustomId({
403
+ command: params.command,
404
+ action: "model",
405
+ view: "models",
406
+ provider: params.modelPage.provider,
407
+ runtime: stateRuntime,
408
+ page: params.modelPage.page,
409
+ providerPage: providerPage.page,
410
+ userId: params.userId,
411
+ }),
412
+ options: modelOptions,
413
+ placeholder: `Select ${params.modelPage.provider} model`,
414
+ }),
415
+ ]),
416
+ );
417
+
418
+ const resolvedDefault = params.data.resolvedDefault;
419
+ const shouldDisableReset =
420
+ Boolean(parsedCurrentModel) &&
421
+ parsedCurrentModel?.provider === resolvedDefault.provider &&
422
+ parsedCurrentModel?.model === resolvedDefault.model;
423
+
424
+ const hasPendingSelection =
425
+ Boolean(parsedPendingModel) &&
426
+ parsedPendingModel?.provider === params.modelPage.provider &&
427
+ typeof params.pendingModelIndex === "number" &&
428
+ params.pendingModelIndex > 0;
429
+
430
+ const buttonRowItems: Button[] = [
431
+ createModelPickerButton({
432
+ label: "Cancel",
433
+ style: ButtonStyle.Secondary,
434
+ customId: buildDiscordModelPickerCustomId({
435
+ command: params.command,
436
+ action: "cancel",
437
+ view: "models",
438
+ provider: params.modelPage.provider,
439
+ runtime: stateRuntime,
440
+ page: params.modelPage.page,
441
+ providerPage: providerPage.page,
442
+ userId: params.userId,
443
+ }),
444
+ }),
445
+ createModelPickerButton({
446
+ label: "Reset to default",
447
+ style: ButtonStyle.Secondary,
448
+ disabled: shouldDisableReset,
449
+ customId: buildDiscordModelPickerCustomId({
450
+ command: params.command,
451
+ action: "reset",
452
+ view: "models",
453
+ provider: params.modelPage.provider,
454
+ runtime: stateRuntime,
455
+ page: params.modelPage.page,
456
+ providerPage: providerPage.page,
457
+ userId: params.userId,
458
+ }),
459
+ }),
460
+ ];
461
+
462
+ if (hasQuickModels) {
463
+ buttonRowItems.push(
464
+ createModelPickerButton({
465
+ label: "Recents",
466
+ style: ButtonStyle.Secondary,
467
+ customId: buildDiscordModelPickerCustomId({
468
+ command: params.command,
469
+ action: "recents",
470
+ view: "recents",
471
+ provider: params.modelPage.provider,
472
+ runtime: stateRuntime,
473
+ page: params.modelPage.page,
474
+ providerPage: providerPage.page,
475
+ userId: params.userId,
476
+ }),
477
+ }),
478
+ );
479
+ }
480
+
481
+ buttonRowItems.push(
482
+ createModelPickerButton({
483
+ label: "Submit",
484
+ style: ButtonStyle.Primary,
485
+ disabled: !hasPendingSelection,
486
+ customId: buildDiscordModelPickerCustomId({
487
+ command: params.command,
488
+ action: "submit",
489
+ view: "models",
490
+ provider: params.modelPage.provider,
491
+ runtime: stateRuntime,
492
+ page: params.modelPage.page,
493
+ providerPage: providerPage.page,
494
+ modelIndex: params.pendingModelIndex,
495
+ userId: params.userId,
496
+ }),
497
+ }),
498
+ );
499
+
500
+ return { rows, buttonRow: new Row(buttonRowItems) };
501
+ }
502
+
503
+ export function renderDiscordModelPickerProvidersView(
504
+ params: DiscordModelPickerProviderViewParams,
505
+ ): DiscordModelPickerRenderedView {
506
+ const page = getDiscordModelPickerProviderPage({ data: params.data, page: params.page });
507
+ const parsedCurrent = parseCurrentModelRef(params.currentModel);
508
+ const rows = buildProviderRows({
509
+ command: params.command,
510
+ userId: params.userId,
511
+ page,
512
+ currentProvider: parsedCurrent?.provider,
513
+ });
514
+
515
+ const detailLines = [
516
+ formatCurrentModelLine(params.currentModel),
517
+ `Select a provider (${page.totalItems} available).`,
518
+ ];
519
+ return buildRenderedShell({
520
+ layout: params.layout ?? "v2",
521
+ title: "Model Picker",
522
+ detailLines,
523
+ rows,
524
+ footer: `All ${page.totalItems} providers shown`,
525
+ });
526
+ }
527
+
528
+ export function renderDiscordModelPickerModelsView(
529
+ params: DiscordModelPickerModelViewParams,
530
+ ): DiscordModelPickerRenderedView {
531
+ const providerPage = normalizeModelPickerPage(params.providerPage);
532
+ const modelPage = getDiscordModelPickerModelPage({
533
+ data: params.data,
534
+ provider: params.provider,
535
+ page: params.page,
536
+ });
537
+
538
+ if (!modelPage) {
539
+ const rows: Row<Button>[] = [
540
+ new Row([
541
+ createModelPickerButton({
542
+ label: "Back",
543
+ customId: buildDiscordModelPickerCustomId({
544
+ command: params.command,
545
+ action: "back",
546
+ view: "providers",
547
+ page: providerPage,
548
+ userId: params.userId,
549
+ }),
550
+ }),
551
+ ]),
552
+ ];
553
+
554
+ return buildRenderedShell({
555
+ layout: params.layout ?? "v2",
556
+ title: "Model Picker",
557
+ detailLines: [
558
+ formatCurrentModelLine(params.currentModel),
559
+ `Provider not found: ${normalizeProviderId(params.provider)}`,
560
+ ],
561
+ rows,
562
+ footer: "Choose a different provider.",
563
+ });
564
+ }
565
+
566
+ const { rows, buttonRow } = buildModelRows({
567
+ command: params.command,
568
+ userId: params.userId,
569
+ data: params.data,
570
+ providerPage,
571
+ modelPage,
572
+ currentModel: params.currentModel,
573
+ currentRuntime: params.currentRuntime,
574
+ pendingModel: params.pendingModel,
575
+ pendingModelIndex: params.pendingModelIndex,
576
+ pendingRuntime: params.pendingRuntime,
577
+ quickModels: params.quickModels,
578
+ });
579
+
580
+ const defaultModel = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
581
+ const pendingLine = params.pendingModel
582
+ ? `Selected: ${params.pendingModel} · runtime ${resolveSelectedRuntime({
583
+ data: params.data,
584
+ provider: modelPage.provider,
585
+ currentRuntime: params.currentRuntime,
586
+ pendingRuntime: params.pendingRuntime,
587
+ })} (press Submit)`
588
+ : "Select a model, then press Submit.";
589
+
590
+ return buildRenderedShell({
591
+ layout: params.layout ?? "v2",
592
+ title: "Model Picker",
593
+ detailLines: [formatCurrentModelLine(params.currentModel), `Default: ${defaultModel}`],
594
+ preRowText: pendingLine,
595
+ rows,
596
+ trailingRows: [buttonRow],
597
+ });
598
+ }
599
+
600
+ export type DiscordModelPickerRecentsViewParams = {
601
+ command: DiscordModelPickerCommandContext;
602
+ userId: string;
603
+ data: ModelsProviderData;
604
+ quickModels: string[];
605
+ currentModel?: string;
606
+ runtime?: string;
607
+ provider?: string;
608
+ page?: number;
609
+ providerPage?: number;
610
+ layout?: DiscordModelPickerLayout;
611
+ };
612
+
613
+ function formatRecentsButtonLabel(modelRef: string, suffix?: string): string {
614
+ const maxLen = 80;
615
+ const label = suffix ? `${modelRef} ${suffix}` : modelRef;
616
+ if (label.length <= maxLen) {
617
+ return label;
618
+ }
619
+ const trimmed = suffix
620
+ ? `${modelRef.slice(0, maxLen - suffix.length - 2)}… ${suffix}`
621
+ : `${modelRef.slice(0, maxLen - 1)}…`;
622
+ return trimmed;
623
+ }
624
+
625
+ export function renderDiscordModelPickerRecentsView(
626
+ params: DiscordModelPickerRecentsViewParams,
627
+ ): DiscordModelPickerRenderedView {
628
+ const defaultModelRef = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
629
+ const rows: DiscordModelPickerRow[] = [];
630
+
631
+ // Dedupe: filter recents that match the default model.
632
+ const dedupedQuickModels = params.quickModels.filter((modelRef) => modelRef !== defaultModelRef);
633
+
634
+ // Default model button — slot 1.
635
+ rows.push(
636
+ new Row([
637
+ createModelPickerButton({
638
+ label: formatRecentsButtonLabel(defaultModelRef, "(default)"),
639
+ style: ButtonStyle.Secondary,
640
+ customId: buildDiscordModelPickerCustomId({
641
+ command: params.command,
642
+ action: "submit",
643
+ view: "recents",
644
+ recentSlot: 1,
645
+ provider: params.provider,
646
+ runtime: params.runtime,
647
+ page: params.page,
648
+ providerPage: params.providerPage,
649
+ userId: params.userId,
650
+ }),
651
+ }),
652
+ ]),
653
+ );
654
+
655
+ // Recent model buttons — slot 2+.
656
+ for (let i = 0; i < dedupedQuickModels.length; i++) {
657
+ const modelRef = dedupedQuickModels[i];
658
+ rows.push(
659
+ new Row([
660
+ createModelPickerButton({
661
+ label: formatRecentsButtonLabel(modelRef),
662
+ style: ButtonStyle.Secondary,
663
+ customId: buildDiscordModelPickerCustomId({
664
+ command: params.command,
665
+ action: "submit",
666
+ view: "recents",
667
+ recentSlot: i + 2,
668
+ provider: params.provider,
669
+ runtime: params.runtime,
670
+ page: params.page,
671
+ providerPage: params.providerPage,
672
+ userId: params.userId,
673
+ }),
674
+ }),
675
+ ]),
676
+ );
677
+ }
678
+
679
+ // Back button after a divider (via trailingRows).
680
+ const backRow: Row<Button> = new Row([
681
+ createModelPickerButton({
682
+ label: "Back",
683
+ style: ButtonStyle.Secondary,
684
+ customId: buildDiscordModelPickerCustomId({
685
+ command: params.command,
686
+ action: "back",
687
+ view: "models",
688
+ provider: params.provider,
689
+ runtime: params.runtime,
690
+ page: params.page,
691
+ providerPage: params.providerPage,
692
+ userId: params.userId,
693
+ }),
694
+ }),
695
+ ]);
696
+
697
+ return buildRenderedShell({
698
+ layout: params.layout ?? "v2",
699
+ title: "Recents",
700
+ detailLines: [
701
+ "Models you've previously selected appear here.",
702
+ formatCurrentModelLine(params.currentModel),
703
+ ],
704
+ preRowText: "Tap a model to switch.",
705
+ rows,
706
+ trailingRows: [backRow],
707
+ });
708
+ }
709
+
710
+ export function toDiscordModelPickerMessagePayload(
711
+ view: DiscordModelPickerRenderedView,
712
+ ): MessagePayloadObject {
713
+ if (view.layout === "classic") {
714
+ return {
715
+ content: view.content,
716
+ components: view.components,
717
+ };
718
+ }
719
+ return {
720
+ components: view.components,
721
+ };
722
+ }