Modal Input Components
此内容尚不支持你的语言。
TomoriBot uses Discord modal dialogs to collect structured inputs: text inputs, single-choice radio groups, multi-choice checkbox groups, binary toggles, entity selects, and file uploads. Because discord.js lacks complete native support for newer modal components, TomoriBot coordinates modal dispatch via raw Discord REST API payloads and intercepts gateway packets before client parsing. For complete wire schema definitions and modal lifecycles, refer to the Discord Modal documentation.
The discord.js gap and raw modal architecture
Section titled “The discord.js gap and raw modal architecture”The pinned discord.js version does not provide complete builder classes or parser structures for Label wrappers (type 18), Radio Groups (21), Checkbox Groups (22), Checkboxes (23), modal File Uploads (19), or modal entity selects (types 5, 6, 8).
TomoriBot handles these components through raw REST transmission and WebSocket interception:
- Raw REST transmission:
showRoutedRawModalinsrc/utils/discord/ui/interactionCore.tssends raw JSON payloads to Discord’s interaction callback REST endpoint (InteractionResponseType.Modal, type 9). It first passes the payload throughclampRawModalText, which truncates titles, labels, descriptions, placeholders, and option text to their caps, because Discord rejects the whole modal when one translated string is too long. It marks the interaction in therawModalAcknowledgedWeakMapso internal flow logic recognizes that the interaction has been answered, even though discord.js properties (replied,deferred) remain false. - WebSocket packet interception:
setupWebSocketInterceptioninsrc/utils/discord/ui/interactionCore.tshooks into the Discord client WebSocket packet handler (client.ws.handlePacket). When anINTERACTION_CREATEgateway packet arrives with type 5 (Modal Submit) containing Type 18 Label components:- Structured value extraction: The interceptor extracts submitted values from nested components into session memory maps keyed by interaction ID:
modalSelectValues: The first value of string, user, and role selects, plus radio group values and checkbox booleans.modalChannelSelectValues: Every submitted channel ID per Channel Select, recorded as[]when submitted empty whether or not Discord sendsvalues, so an optional select left empty stays distinct from an uncaptured or already consumed entry.modalStringSelectValues: Every submitted value per String Select, recorded the same way, for multi-pick fields. The scalar store keeps only the first value, and taking a select from either store consumes it from both.modalCheckboxGroupValues: Checkbox group selection string arrays.modalFileUploadValues: File upload attachment ID arrays.modalResolvedAttachments: Discord API attachment objects frompacket.d.data.resolved.attachments.
- Packet transformation:
transformModalSubmissionPacketrewrites Type 18 Label components into standard Type 1 Action Rows containing the nested inputs. This allows discord.js native modal parsing to process the submission without throwing unhandled component type errors.
- Structured value extraction: The interceptor extracts submitted values from nested components into session memory maps keyed by interaction ID:
- Consume-once value extraction: Route handlers and modal helpers read stored values using
takeRawModalSelectValue,takeRawModalStringSelectValues,takeRawModalChannelSelectValues,takeRawModalCheckboxGroupValues, andtakeRawModalFileUpload. Each call deletes the retrieved entry from memory, reclaiming memory immediately after submission.takeRawModalStringSelectValuesandtakeRawModalChannelSelectValuesreturnundefinedfor a missing entry and[]for an empty submission; single-select readers take the first ID. InpromptWithRawModal, a Channel Select withmaxValuesabove 1 lands inmultiValues.
Modal limits and wire constraints
Section titled “Modal limits and wire constraints”validateRawModalLimits in src/utils/discord/ui/componentsV2Limits.ts validates raw modal payloads against Discord wire limits:
- Dialog title: At most 45 Unicode codepoints (
DISCORD_MODAL_TITLE_MAX). - Top-level components: 1 to 5 components (
DISCORD_MODAL_COMPONENTS_MINandDISCORD_MODAL_COMPONENTS_MAX). - Field labels: At most 45 characters on Type 18 Label wrappers (
DISCORD_MODAL_FIELD_LABEL_MAX). - Field descriptions: At most 100 characters on Type 18 Label wrappers (
DISCORD_MODAL_FIELD_DESCRIPTION_MAX). - Radio and checkbox groups: Discord requires 2-10 radio options and 1-10 checkbox options. The local validator currently permits a one-option radio group; callers must still satisfy the upstream component limits.
- Text inputs: At most 4,000 characters per field (
DISCORD_TEXT_INPUT_MAX).
Discord clients cache checked states and selections by component custom_id. Re-opening a modal or opening paginated slices that share custom IDs causes the client to restore stale user selections instead of displaying truthful defaults. Modal builders generate a unique timestamp nonce (modalNonce = Date.now().toString(36)) appended to both modal and component custom IDs (${customId}_${modalNonce}).
Interaction timing and acknowledgment constraints
Section titled “Interaction timing and acknowledgment constraints”Modal forms must satisfy Discord interaction timing deadlines:
- The 3-second acknowledgment window: Discord requires modal dialogs to be sent as the first response to an interaction via
showModal()or the raw modal REST callback. CallingdeferReply()ordeferUpdate()before sending a modal causes Discord to reject the modal opening with an error. - Unacknowledged confirmations: Standard confirmation dialogs like
promptWithConfirmationcalldeferUpdate()inside their collector filter, consuming the button interaction. When a button click must lead to a modal, handlers usepromptWithUnacknowledgedConfirmationinsrc/utils/discord/ui/interactionCore.ts, which returns the raw, unacknowledgedButtonInteractionsoshowModal()orpromptWithRawModal()can serve as the first response. - Asynchronous option loading: In anchor workflows (
runPersonaPickerWorkflow), modal options that require database or network queries are passed as an asynchronous factory toselection.openModal(async () => ...)insrc/utils/discord/ui/personaWorkflow.ts. The workflow immediately acknowledges the initial button withdeferUpdate(), replaces the anchor message with a loading indicator, loads the options, and renders a freshOpen Formbutton (or range selector). The user clicks the fresh button, allowing its unacknowledged interaction to callshowModal(). - Submission timing: Modal submissions arrive as unacknowledged interactions. To prevent the 3-second interaction timeout during slow validation or database writes, handlers immediately acknowledge submissions with
submitted.deferReply()ormodal.phase.beginInPlaceWork().
Component selection standards
Section titled “Component selection standards”Use these guidelines when selecting modal input components:
- Text Input (type 4): Free-form string values (names, prompts, keys, numbers).
- Radio Group (type 21): Small fixed sets of 2 to 10 mutually exclusive choices.
- Checkbox (type 23): Single optional binary toggles (
trueorfalse). Standalone checkboxes cannot be required by Discord API rules. - Checkbox Group (type 22): Multi-select options (1 to 10 choices). Also provides the required boolean pattern: a Checkbox Group with 1 option and
required: true, requiring explicit user confirmation before submission. - String Select (type 3): Dynamic collections or sets with 11 to 25 choices.
- Entity Selects (types 5, 6, 8): Native server entity selection for users, roles, or channels with optional type filtering.
All structured modal inputs must be wrapped inside a Label component (type 18). Text inputs in raw modals are also wrapped in Labels to support field descriptions.
Prompts exceeding 4,000 characters are partitioned across up to 4 text inputs using splitPromptIntoModalParts from src/utils/text/modalPromptParts.ts. Labels are generated via promptPartLabel and promptPartDescription in src/utils/discord/ui/modalPromptPartLabels.ts (for example System Prompt (Part 2 of 4)). On submission, parts are rejoined with combineModalPromptParts.
High-volume patterns and form coordination
Section titled “High-volume patterns and form coordination”Complex configuration workflows coordinate modals with persistent panels and anchor flows:
- Bulk configuration checklist pattern: Used in
/moderation(user blacklists, channel whitelists, role whitelists, persona mappings) and/configchannel rules. Combines up to 5 Checkbox Groups with 10 options each (up to 50 total entries). A/moderationlist already pages in the panel, so its Remove modal presents only the entries on the visible page. All current items are pre-checked; unchecking an entry indicates removal upon submission without requiring a second confirmation dialog. The submission handler compares the submitted array against a short-lived presented snapshot keyed by modal nonce, preventing accidental deletion of items added concurrently after the modal was displayed. - Range selector bridges: When collections exceed single-modal limits (50 items for Checkbox Groups, 25 options for String Selects), control panels display bounded range buttons (
1-25,26-50or1-50,51-100). Each range button opens a modal sliced to that exact subset. - Provider select to model modal pattern: In
/configand/personal configmodel settings (src/utils/discord/ui/modelRoutingControls.ts), selecting a provider opens a modal containing that provider’s model list. This moves large model catalogs into modal dialogs, preserving the 40-component limit on the parent panel page. - Setup wizard modals:
src/utils/discord/ui/setupPanel.tsconstructs configuration modals directly from draft records. All inputs are enclosed in Type 18 Labels. Starting Settings includes an optional feature checklist that starts fully checked and restores the saved selection, including an empty selection. Its submit route carries a fresh modal nonce for the checklist’s custom ID so Discord cannot restore stale checkbox state. Text inputs preserve configured strings, and string selects carry saved defaults as client hints.
Source pointers
Section titled “Source pointers”src/utils/discord/ui/interactionCore.ts:showRoutedRawModal,promptWithRawModal,setupWebSocketInterception, and value extraction helpers.src/utils/discord/ui/componentsV2Limits.ts:validateRawModalLimitsand modal limit constants.src/utils/text/modalPromptParts.ts:splitPromptIntoModalPartsandcombineModalPromptParts.src/utils/discord/ui/modalPromptPartLabels.ts:promptPartLabelandpromptPartDescription.src/utils/discord/ui/personaWorkflow.ts:openModallifecycle and asynchronous option bridging.src/utils/discord/ui/modelRoutingControls.ts: Provider-to-model modal generation.src/utils/discord/ui/setupPanel.ts: Setup wizard modal construction.