First Version

This commit is contained in:
2026-07-10 15:02:09 +01:00
commit 38494a85a9
562 changed files with 196313 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
# 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/commands``src/frontend/api/commands.rs` |
| Autocomplete + telnet gate | `web/components/copilot.js`, `web/lib/chat-session.js` |
## Example
`commands/echo/meta.json`:
```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`.