Files
2026-07-10 15:02:09 +01:00

4.9 KiB

Custom slash commands

User-defined /command shortcuts that expand a prompt template into a normal user message on the interactive session. They let you package a long, reusable instruction behind a short command (/deeploop, /review, …) — the template is interpolated with whatever the user typed and handed to the model as if the user had written it out in full.

Because the expansion becomes an ordinary user turn on the main session, a command is fully interactive: after it fires the model can ask follow-up questions, iterate with the user, write files, and dispatch sub-agents — exactly like any other turn. A command is not a one-off request.

File layout (mirrors agents/)

Each command is a directory under commands/:

commands/<name>/meta.json     # manifest
commands/<name>/COMMAND.md     # the prompt template
  • <name> is the directory name and the token typed after / (e.g. commands/echo//echo). Use lowercase [a-z0-9_-].
  • meta.json:
    • description (required) — one line, shown in /help and the composer autocomplete.
    • enabled (optional, default true) — set false to hide a command without deleting it.
  • COMMAND.md — the template body. {{args}} (or {{prompt}}) is replaced with the user's arguments. If neither placeholder appears, the arguments are appended after a --- separator (opencode-like default).

Files are read at request time, so edits to meta.json / COMMAND.md take effect without a restart (same as agents/*/AGENT.md). There is no DB table and no CRUD API — you create a command by adding files, just like an agent. A missing commands/ directory is fine (no custom commands).

Precedence

Hard-coded system commands (/help, /clear, /new, /model, /models, /context, /cost, /compact, /resettools, /sethome, /stop) always win: they are matched first in each surface's command handler (the WS handler for web/mobile, the Telegram plugin handler), so a custom command with a colliding name is unreachable and is filtered out of /help and the autocomplete. An unrecognised /… is rejected ("Unknown command") and never forwarded to the LLM.

Dual view (typed command vs. expanded template)

The history row's content stores the expanded template (what the model replays); the UI shows the typed command (/echo ciao), never the template. This is carried by MessageMetadata.command (CommandRef { name, display }), persisted on the user turn next to attachments:

  • The backend substitutes content = command.display when emitting the user_message echo (emitter.user_message, both call sites in llm_loop.rs / session/handler/mod.rs) and when serialising history (api/sessions.rs).
  • The frontend is telnet-style: custom commands are not echoed optimistically on send — the bubble appears only when the backend re-emits the user_message carrying the typed form. Only system commands (which reply with a Done and never echo back) are rendered optimistically. See SYSTEM_SLASH_COMMANDS in web/lib/chat-session.js.

Composer autocomplete

Typing / in the copilot composer opens a dropdown: built-in system commands first, then custom commands fetched from GET /api/commands. Filter by prefix, navigate with /, accept with Enter/Tab (inserts /name ), dismiss with Esc. (web/components/copilot.js; styles in web/css/copilot-input.css.)

Implementation map

Concern Location
Capability trait + DTOs + expansion crates/core-api/src/command.rs (CommandApi, CommandInfo, ResolvedCommand, expand_template)
Discovery / resolve / expand src/core/command/mod.rs (LlmCommandManager: CommandApi, owned by Skald as Arc<…>)
Metadata dual-view type crates/core-api/src/message_meta.rs (CommandRef, MessageMetadata.command)
Matching + dynamic /help (web + mobile) src/frontend/api/ws.rs (dynamic_help, custom-command interposition)
Matching + dynamic /help (Telegram) crates/plugin-telegram-bot/src/handlers.rs (help_text, fallback-command arm; resolves via ctx.command)
Plugin wiring PluginContext.command (crates/core-api/src/plugin.rs) → TgShared.command
Echo content = display src/core/session/handler/{llm_loop.rs, mod.rs}, src/frontend/api/sessions.rs (applies to every surface: web, mobile, Telegram)
Listing endpoint GET /api/commandssrc/frontend/api/commands.rs
Autocomplete + telnet gate web/components/copilot.js, web/lib/chat-session.js

Example

commands/echo/meta.json:

{ "description": "Test command — echoes your input", "enabled": true }

commands/echo/COMMAND.md:

Repeat the user's input back verbatim.

User input:
{{args}}

Typing /echo hello world runs a turn where the model receives Repeat the user's input back verbatim.\n\nUser input:\nhello world, while the chat bubble shows /echo hello world.