Esposter

Slash Commands

Triggered by / in the message input. The TipTap suggestion API powers the picker; definitions live in SlashCommandDefinitionMap.

Command reference

CommandKindParametersResult
/flipImmediate clientPosts 🌝 **Heads** or 🌚 **Tails**
/meImmediate clientmessage (required)Posts *message* (italic emphasis)
/rollImmediate clientPosts 🎲 Rolled a **N** (1–100)
/shrugImmediate clienttext (optional)Posts text¯\_(ツ)_/¯
/tableflipImmediate clientPosts (╯°□°)╯︵ ┻━┻
/unflipImmediate clientPosts ┬─┬ノ( º _ ºノ)
/topicImmediate tRPCtext (optional)Calls room.updateRoom to set/clear the topic
/pollDialog → tRPCOpens the poll dialog; on submit posts a MessageType.Poll message
/remindDialog → tRPCOpens the scheduled-job dialog → /docs/esbabbler/scheduled-messages
/scheduleDialog → tRPCOpens the scheduled-job dialog → /docs/esbabbler/scheduled-messages

How it works

Adding a command touches three places (per the registry pattern): the SlashCommandType enum, its SlashCommandDefinitionMap entry (icon, title, description, parameters[], type), and an arm in useExecuteSlashCommand.

All execution happens in useExecuteSlashCommand via a switch on SlashCommandType — the definition is static metadata only, with no execute() method. Three execution paths:

  • Client-only (Flip, Me, Roll, Shrug, TableFlip, Unflip) — build createMessageInput inline and fall through to the normal storeSendMessage.
  • Server call (Topic) — call a tRPC mutation directly (room.updateRoom), no message posted by the command itself.
  • Dialog (Poll, Remind, Schedule) — open a dialog that owns its own submit, so richer inputs use normal form controls instead of inline parameter chips.

Typing a command

flowchart TD
    A([User types /]) --> B[TipTap suggestion activates]
    B --> C{User picks command}
    C -->|no parameters| D[useExecuteSlashCommand immediately]
    C -->|has parameters| E[SlashCommandSuggestion.command fires]
    E --> F[Read remaining editor text after range]
    F --> G[Delete from range.from to doc end]
    G --> H{remainingText?}
    H -->|yes| I[parseTextAndParameters → restore parameterValues + trailingMessage]
    H -->|no| J[empty parameterValues]
    I --> K[setPendingSlashCommand + parsed values]
    J --> K
    K --> L[SlashCommandParameters UI shows with chips pre-filled]
    L --> M([User fills in + submits])
    M --> D

Parameter mode ↔ text serialisation

Collapsing parameter mode back to text (Escape, or Backspace in an empty command/trailing input) rebuilds the input as:

/CommandType parameterName1|value1 parameterName2|value2 trailingMessage

ID_SEPARATOR (|) separates a parameter name from its value; parameters are space-separated; the last parameter's value is greedy; re-parsing uses prefix matching per parameter in definition order. The RichTextEditor remounts with the formatted text and focuses at the end.

Key files

FileRole
packages/app/app/models/message/slashCommands/SlashCommandType.tscommand enum
packages/app/app/services/message/slashCommands/SlashCommandDefinitionMap.tsRecord<SlashCommandType, SlashCommand> — static definitions
packages/app/app/composables/message/slashCommand/useExecuteSlashCommand.tsexecution switch
packages/app/app/composables/message/slashCommand/useSlashCommandExtension.tsTipTap extension (mirrors useMentionExtension)
packages/app/app/services/message/slashCommands/SlashCommandSuggestion.tspicker suggestion config
packages/app/app/services/message/slashCommands/parseTextAndParameters.tstext → parameterValues re-parsing
packages/app/app/store/message/input/slashCommand.tspendingCommand + parameterValues + trailingMessage store

Notes

The send pipeline is untouched by commands: sendMessage, RichTextEditor, and the mention suggestion know nothing about slash commands. Client-only commands go through the same storeSendMessage as a hand-typed message.