MusePi

Slash command internals

This document describes how slash commands are discovered, deduplicated, surfaced in interactive mode, and expanded at prompt time in coding-agent.

Implementation files

1) Discovery model

Slash commands are a capability (id: "slash-commands") keyed by command name (key: cmd => cmd.name).

The capability registry loads all registered providers, sorted by provider priority descending, and deduplicates by key with first wins semantics.

Provider precedence

Current slash-command providers and priorities:

  1. native (OMP) — priority 100
  2. omp-plugins (extension packages) — priority 90
  3. claude — priority 80
  4. claude-plugins — priority 70
  5. agents (.agent/.agents standard dirs) — priority 70
  6. codex — priority 70
  7. opencode — priority 55

Tie behavior: equal-priority providers keep registration order. Current import order registers claude-plugins before agents before codex, so plugin commands win over both on name collisions.

Name-collision behavior

For slash-commands, collisions are resolved strictly by capability dedup:

This applies across providers and also within a provider if it returns duplicate names.

File scanning behavior

Providers mostly use loadFilesFromDir(...), which currently:

So hidden files/directories are not loaded, ignored paths are skipped, and file order follows native glob result order unless a provider adds its own ordering.

2) Provider-specific source paths and local precedence

native provider (builtin.ts)

Search roots come from .musepi directories:

getConfigDirs() returns project first, then user, so project native commands beat user native commands when names collide.

claude provider (claude.ts)

Loads, subject to commands.enableClaudeUser and commands.enableClaudeProject settings:

Commands in subdirectories additionally get a namespaced alias: foo/bar.md is registered under both bar and foo:bar (addClaudeCommandNamespaceAliases).

The provider pushes user items before project items, so user Claude commands beat project Claude commands on same-name collisions inside this provider.

codex provider (codex.ts)

Loads:

Both sides are loaded then flattened in user-first order, so user Codex commands beat project Codex commands on collisions.

Codex command content is parsed with frontmatter stripping (parseFrontmatter), and command name can be overridden by frontmatter name; otherwise filename is used.

opencode provider (opencode.ts)

Loads, subject to commands.enableOpencodeUser and commands.enableOpencodeProject settings:

Both sides are loaded then flattened in user-first order, so user OpenCode commands beat project OpenCode commands on collisions. OpenCode command content is parsed with frontmatter stripping, and command name can be overridden by frontmatter name; otherwise filename is used.

claude-plugins provider (claude-plugins.ts)

Loads plugin command roots via listClaudePluginRoots(...), which reads ~/.claude/plugins/installed_plugins.json, ~/.musepi/plugins/installed_plugins.json, and the nearest project-scoped registry resolved from cwd. For each root it scans <pluginRoot>/commands/*.md (the directory can be remapped by plugin config keys commands/slash-commands), and command names are prefixed with the plugin name: <plugin>:<command>.

Across the three registries, roots are merged by precedence rather than sorted: --plugin-dir injected roots come first, then project-scoped entries (which shadow user entries for the same plugin id), then user entries, with the OMP registry authoritative over Claude’s for the same plugin id. Within each registry, per-plugin entry order from the JSON data is preserved; there is no additional sort step.

3) Materialization to runtime FileSlashCommand

loadSlashCommands() in src/extensibility/slash-commands.ts converts capability items into FileSlashCommand objects used at prompt time.

For each command:

  1. parse frontmatter/body (parseFrontmatter)
  2. description source:
    • frontmatter.description if present
    • else first non-empty body line (max 60 chars with ...)
  3. keep parsed body as executable template content
  4. compute a display source string like via Claude Code Project

Frontmatter parse severity is source-dependent:

Bundled fallback commands

After filesystem/provider commands, embedded command templates are appended (EMBEDDED_COMMAND_TEMPLATES) if their names are not already present.

Current embedded set comes from src/task/commands.ts and is used as a fallback (source: "bundled").

4) Interactive mode: where command lists come from

Interactive mode combines multiple command sources for autocomplete and command routing.

At construction time it builds a pending command list from:

Then init() calls refreshSlashCommandState(...) to load file-based commands and install one autocomplete provider (createPromptActionAutocompleteProvider, a PromptActionAutocompleteProvider wrapping a CombinedAutocompleteProvider) containing:

refreshSlashCommandState(...) also updates session.setSlashCommands(...) so prompt expansion uses the same discovered file command set.

Refresh lifecycle

Slash command state is refreshed:

There is no continuous file watcher for command directories.

Other surfacing

The Extensions dashboard also loads slash-commands capability and displays active/shadowed command entries, including _shadowed duplicates.

5) Prompt pipeline placement

AgentSession.prompt(...) slash handling order (when expandPromptTemplates !== false):

  1. Extension commands (#tryExecuteExtensionCommand)
    If /name matches extension-registered command, handler executes immediately and prompt returns.
  2. TypeScript custom commands and MCP prompt commands (#tryExecuteCustomCommand) Boundary only: if matched, it executes and may return:
    • string -> replace prompt text with that string
    • void/undefined -> treated as handled; no LLM prompt
  3. File-based slash commands (expandSlashCommand)
    If text still starts with /, attempt markdown command expansion.
  4. Prompt templates (expandPromptTemplate)
    Applied after slash/custom processing.
  5. Delivery
    • idle: prompt is sent immediately to agent
    • streaming: prompt is queued as steer/follow-up depending on streamingBehavior

This is why slash command expansion sits before prompt-template expansion, and why custom commands can transform away the leading slash before file-command matching.

6) Expansion semantics for file-based slash commands

expandSlashCommand(text, fileCommands) behavior:

parseCommandArgs caveats

The parser is simple quote-aware splitting:

7) Unknown /... behavior

Unknown slash input is not rejected by core slash logic.

If command is not handled by extension/custom/file layers, expandSlashCommand returns original text, and the literal /... prompt proceeds through normal prompt-template expansion and LLM delivery.

Interactive mode separately hard-handles many built-ins in InputController (for example /settings, /model, /mcp, /move, /exit). Those are consumed before session.prompt(...) and therefore never reach file-command expansion in that path.

8) Streaming-time differences vs idle

Idle path

Streaming path (session.isStreaming === true)

Important command-specific streaming behavior

9) Error handling and failure surfaces