Hire Team

INDUSAGI DOCUMENTATION

Slash Commands

Preserved documentation snapshot. APIs and capabilities differ by edition and version. Check the selected package before implementation. View pinned source.

View Python Coding Agent source on GitHub
On this page

The slash-command subsystem of the pindus / induscode interactive console: a typed line like /model, /resume, or /export out.html is parsed, resolved against an ordered registry, and run as a thin async handler that acts only through an injected context. The pure framework lives at induscode.console_slash; the ~26-command catalog lives at induscode.console.slash_commands.

Table of Contents

Overview

When you type a line into the composer the console routes it in a fixed order: a leading !/!! is a shell escape; otherwise the line is resolved against the slash registry; otherwise it is submitted as a plain prompt. The slash layer owns that middle step — turning the raw string into a discriminated outcome (NotSlash / Match / Miss) and then await-ing the matched command's handler.

The subsystem is deliberately split so the parsing/resolution machinery never depends on the live console. Handlers are uniform and thin: every one is

async def run(ctx: SlashContext) -> SlashOutcome: ...

and reaches the terminal only through the SlashContext it is handed (open a modal, dispatch a reducer event, set a status toast, append a display block, request exit, or return a Prompt for the dispatcher to submit). That keeps handlers pure with respect to the TUI and trivially fakeable in tests.

Two packages

Package Role Depends on console?
induscode.console_slash The pure framework: contracts, registry assembler + derived lookups, the line resolver, and the shared family/status toolkit. No — a dependency-free leaf.
induscode.console.slash_commands The command catalog: the transcript / workbench / integration groups, the dynamic /skill: + /<template> rows, and the build_catalog assembler. Yes — it is built on top of the framework.

The catalog imports SlashCommand, SlashContext, HANDLED, info, warn, family_runner, and SubCommand from induscode.console_slash and folds its rows through build_registry. The framework never imports the catalog (clean leaf direction).

Resolution pipeline

Resolution is two pure functions over a string and a registry. None of it touches I/O, the conductor, or the TUI.

  1. parse_slash(line) decides whether the line is command-shaped and, if so, splits it into a SlashLine(name, args). A line qualifies only when — after trimming leading blanks — it starts with / and the token matches ^[a-zA-Z][a-zA-Z0-9:-]*. A bare /, a //comment, and a /usr/local/bin path are all rejected (the embedded slash breaks the token shape), so those reach the prompt untouched. The token is lower-cased; the argument tail keeps its interior spacing.
  2. resolve_slash(line, registry) parses the line, then looks the token up in the registry index and returns a SlashResolution:
Outcome Meaning Dispatcher action
NotSlash not command-shaped hand on as a normal prompt or !/!! escape
Match(command, args) a row owns the token await command.run(ctx)
Miss(name) command-shaped but no row owns it surface "Unknown command"
from induscode.console_slash import resolve_slash
from induscode.console.slash_commands import DEFAULT_SLASH_REGISTRY

res = resolve_slash("/branch since the refactor", DEFAULT_SLASH_REGISTRY)
assert res.kind == "match"
print(res.command.name, repr(res.args))  # 'branch' 'since the refactor'

miss = resolve_slash("/nope", DEFAULT_SLASH_REGISTRY)
assert miss.kind == "miss" and miss.name == "nope"

not_cmd = resolve_slash("/usr/local/bin", DEFAULT_SLASH_REGISTRY)
assert not_cmd.kind == "not-slash"  # embedded slash -> reaches the prompt

Resolution is case-insensitive on the command token, and an alias resolves to the same row as its canonical name (/fork resolves to branch).

The command catalog

build_catalog(cwd, home) concatenates three static groups in listing order, then splices in the discovered dynamic rows:

transcript_commands (11)  ->  workbench_commands (7)  ->  build_integration_commands (8)
                          ->  dynamic /skill:<name> + /<template-name> rows

There are roughly 26 static commands (11 + 7 + 8), plus N discovered dynamic rows. Counting aliases as invokable tokens (reset, condense, compact, sessions, fork, tree, models, scoped-models, ?, hotkeys, changelog) it is about 36 resolvable tokens.

cwd and home are explicit arguments — there are no import-time filesystem scans. The process-default registry is built once by build_default_registry() from the real os.getcwd() / ~ and cached; SLASH_COMMANDS and DEFAULT_SLASH_REGISTRY are lazy module attributes (PEP 562 __getattr__) minted on first access. Building the default registry also installs it as the /help provider (see Family commands).

Transcript and session control

The first group resets, renames, branches, inspects, or leaves the live session. Pickers open an overlay through ctx.open_modal(...); the rest drive the conductor.

Command Aliases Args What it does
/clear reset — Wipe the view and start a new session (conductor new_session()).
/new — — Start a fresh session (identical to /clear).
/summarize-context condense, compact yes Condense the transcript to reclaim context window; trailing guidance is recorded first, then a parameterless condense() runs.
/resume sessions — Open the persisted-session list (sessions overlay).
/session — — Append a session-statistics display block (ids, counts, tokens, cost).
/branch fork — Branch the transcript from a prior turn (userTurns overlay).
/timeline tree — Navigate the transcript tree (tree overlay).
/name — yes Name the session, or with no argument report the current name.
/reload — — Re-read session resources (no-op tree navigation onto the current leaf).
/quit — — Leave the interactive console (ctx.request_exit()).
/exit — — Leave the interactive console.

/clear and /new both wipe the rendered view and call conductor.new_session() — the conductor reset is what actually empties the transcript, since the rendered conversation comes from conductor.messages().

Workbench pickers, help, diagnostics

The overlays a user reaches to inspect or retune the session, plus the live help and keymap surfaces. See Theming for the scheme picker (reached via /settings, not a dedicated command).

Command Aliases Args What it does
/model models — Open the model picker overlay (models).
/models-for scoped-models yes Edit / show / reset per-scope model routing (a family; scopedModels overlay).
/settings — — Open the settings overlay (settings).
/help ? — Append a display block listing every command and its summary.
/keys hotkeys — Append the keyboard-shortcut table (HOTKEYS).
/whats-new changelog — Render a CHANGELOG.md (or build highlights) as a display block.
/debug — — Write a JSONL session-diagnostics log to a temp file and toggle verbose view.

/help reads the live catalog through a late-bound provider, so it lists the dynamic /skill: and /<template> rows too. /keys renders the grounded chord/effect table — Enter (submit), Shift+Enter (soft newline), Esc Esc (tree navigator), Ctrl+C (interrupt), Ctrl+L (model picker), Ctrl+R (resume list), ! <cmd> / !! <cmd> (shell escapes), and the rest.

Integrations: auth, MCP, memory, composio, IO

The bridge group is minted by build_integration_commands(runtime) over one per-console IntegrationsRuntime holder (its mcp_pool, composio_gateway, memory_enabled flag, and tracked tasks). It builds on the framework MCP facade and connectors.

Command Args Family What it does
/login yes — Sign in to a model provider (auth overlay).
/logout — — Sign out of a model provider.
/mcp yes — Manage MCP servers and their tools (connect, reconnect, disconnect, tools, status).
/memory yes memory Inspect and toggle the working-memory capability (status, on, off, tools).
/composio yes composio Connect and inspect Composio bridges (status, accounts, tools, connect, enable).
/copy — — Copy the last reply to the clipboard.
/export yes — Export the transcript to an HTML file (via transcript_export).
/share — — Share the session as a secret GitHub gist.
# inside `pindus` / `induscode` interactive session
/mcp connect             # build + connect the workspace MCP pool, show status
/composio enable github  # hydrate a Composio toolkit's tools
/export out.html         # render the live transcript to HTML
/share                   # publish the transcript as a secret gist
/copy                    # last reply -> clipboard

/composio needs COMPOSIO_API_KEY in the environment; without it the verbs surface a "Composio is not configured" status. See MCP configuration and Transcript Export.

Dynamic rows: skills and templates

Two row shapes are discovered, not hand-written, by scanning .indusagi/skills and .indusagi/commands under the project (cwd) then user (home) roots (via the briefing layer). Both return a Prompt outcome the dispatcher submits as a normal turn:

  • /skill:<name> — submits an Agent-Skills invocation block <skill name="..." location="...">body</skill>, where the trailing argument becomes the body. So /skill:commit tidy the diff runs the commit card against "tidy the diff".
  • /<template-name> — expands the macro body against the trailing arguments (single-pass $arg model) and submits the expansion.

Discovery never raises (a missing root yields nothing), templates are deduped first-wins, and any dynamic token that collides with a reserved static token is dropped — so splicing dynamic rows can never make build_registry raise.

Family commands

A command with sub-verbs (/models-for, /memory, /composio) is built from a SubCommand table via family_runner, never a hand-written if-ladder. The runner splits the leading verb, dispatches by dict lookup, and falls back to a usage toast assembled from the table's descriptions when the verb is missing or unknown.

from induscode.console_slash import SlashCommand, SubCommand, family_runner, HANDLED, info

async def _on(ctx, rest):
    ctx.set_status(info("enabled"))
    return HANDLED

run = family_runner("demo", (SubCommand(verb="on", describe="turn on", run=_on),))
cmd = SlashCommand(name="demo", summary="toggle demo", run=run, family="demo", takes_args=True)
# bare '/demo' -> usage warn "on (turn on)"; '/demo on' -> _on

FAMILY (a FamilyLabels record) names the labels: composio, memory, models-for (scoped models), and theme. theme is declared for parity but no /theme command ships — the scheme picker is reached via /settings.

/help sourcing uses the same pattern in reverse: instead of importing the catalog at call time (a cycle, since the catalog imports the help module), set_help_registry_provider late-binds the assembled registry at registry-build time. Until one is installed, /help falls back to the static transcript + workbench groups.

The registry as a value

SlashRegistry is an immutable ordered command tuple plus a derived token-to-command index (MappingProxyType). Adding a command is appending a row to a group; resolution, completion, and grouping are all pure derivations:

  • build_registry(commands) folds the ordered list into the registry, lower-casing every token (name + aliases). It raises ValueError on any duplicate token — loud at assembly time, never silent shadowing.
  • find_command(registry, token) — exact lookup (canonical or alias, leading slash optional).
  • match_prefix(registry, partial) — the completion candidate set, in registry order; an empty partial returns the whole list.
  • list_families(registry) / commands_in_family(registry, family) — family labels in first-seen order and the rows in a family, for /help grouping.
from induscode.console_slash import build_registry, match_prefix
from induscode.console.slash_commands import build_catalog

catalog = build_catalog(cwd="/work/proj", home="/home/me")
registry = build_registry(catalog)
candidates = match_prefix(registry, "mo")  # /model, /models-for, ...
print([c.name for c in candidates])

How dispatch works

The effectful dispatcher is not part of this subsystem — it lives in the ConsoleApp (console/app.py). On a submitted line the app follows the verbatim routing order:

  1. A leading ! / !! escapes to conductor.execute_bash.
  2. Otherwise resolve_slash(line, self._slash) runs.
  3. On Match, the app constructs a SlashContext from its own callables (dispatch, _open_modal, close_modal, _request_exit, set_status, _set_buffer, append_block, plus the SessionConductor) and awaits resolution.command.run(ctx). A Prompt outcome is fed to the turn runner; Handled ends the line.
  4. On Miss, an "Unknown command" warning is surfaced.
  5. On NotSlash, the line becomes a normal prompt.

open_modal kinds map to the console's ModalKind / MODAL_KINDS vocabulary (settings, models, scopedModels, sessions, tree, userTurns, signIn, signOut, plugin, ...). See Console Overview for the surface and Dialogs for the overlays.

Public API

induscode.console_slash (framework)

Name Kind Source Purpose
SlashCommand dataclass contract.py One registry row: name, summary, run, aliases, family, takes_args.
SlashContext dataclass contract.py The callables a handler acts through: args, conductor, dispatch, open_modal, close_modal, request_exit, set_status, set_buffer, append_block.
Handled / Prompt / Unknown dataclass contract.py The SlashOutcome union a handler returns.
SlashRun type contract.py Callable[[SlashContext], Awaitable[SlashOutcome]].
SlashRegistry dataclass contract.py Resolved table: ordered commands + index (MappingProxyType).
OpenModal protocol contract.py open_modal(kind: str, payload=None) -> None.
build_registry function registry.py Fold a command list into a registry; raise on duplicate tokens.
find_command / match_prefix / list_families / commands_in_family / tokens_of function registry.py Pure derivations over a registry.
resolve_slash function resolve.py Parse a line then resolve to NotSlash / Match / Miss.
parse_slash / looks_like_slash function resolve.py Lexical front half; split into SlashLine(name, args).
Match / Miss / NotSlash / SlashLine / SlashResolution / SLASH_PREFIX type resolve.py The resolution union and parsed shapes.
family_runner function shared.py Build a family run from a SubCommand table.
SubCommand / VerbSplit / split_verb — shared.py A family sub-verb and the verb/rest splitter.
FAMILY / FamilyLabels const shared.py Named family labels (composio, memory, models-for, theme).
HANDLED / info / warn const shared.py The shared Handled() singleton and status-toast minters.

induscode.console.slash_commands (catalog)

Name Kind Source Purpose
build_catalog function builtins.py Assemble the ordered catalog (transcript → workbench → integrations → dynamic).
build_default_registry / reset_default_registry function builtins.py Cached process registry from real cwd/~; installs the /help provider.
discover_dynamic_sources / dynamic_roots / PROJECT_DIR function/const builtins.py Scan .indusagi/skills and .indusagi/commands; never raises.
SLASH_COMMANDS / DEFAULT_SLASH_REGISTRY const builtins.py Lazy module attributes minted on first access.
transcript_commands const transcript.py The 11 session-control rows.
workbench_commands const workbench.py The 7 picker/help/diagnostics rows.
set_help_registry_provider / HOTKEYS function/const workbench.py Late-bound /help source; the /keys chord table.
build_integration_commands / IntegrationsRuntime function/class integrations.py The 8 integration rows over one mutable runtime holder.
build_dynamic_commands / DynamicCommandSources / skill_invocation_block function dynamic.py Project skills/macros into /skill: and /<template> rows.

Notes and parity

  • Roughly 26 commands, not "30+": 11 transcript + 7 workbench + 8 integration static rows, plus the discovered dynamic rows.
  • No /theme command ships. FamilyLabels.theme is declared for parity but the scheme picker is reached via /settings only. /export is HTML only.
  • Every handler is async and awaited. Fire-and-forget bodies (the /clear session reset, the Composio settle paths) ride tracked asyncio tasks that swallow their own failures into a status warn — never bare tasks.
  • /summarize-context with guidance records the guidance via conductor.execute_bash(printf ...) then runs the parameterless condense() — the closest real action on this build.
  • /memory on|off only flips the IntegrationsRuntime.memory_enabled reporting flag; it does not detach the memory tool from the live capability deck.
  • build_registry raises on any duplicate token at assembly time; build_catalog pre-reserves static tokens so a colliding discovered skill/template is dropped rather than crashing the build.
  • The /mcp status text currently hints .indusvx/mcp.json while the dynamic roots resolve under .indusagi/ — a known stale-string inconsistency.

This area is a clean-room rebuild of the TypeScript console-slash lineage; see Parity for the documented port deltas.