Skip to content

Defining your own slash commands

Beyond the built-in slash commands, you can define your own in Lua. A user command can expand into a canned prompt (optionally with arguments), fill the input box for you to edit, or kick off a detached sub-agent that runs alongside your interactive session.

A discover_commands handler runs when your config loads and again on every /reload, with a CommandRegistry to add commands to, so editing your config and reloading adds or removes a command without restarting the process:

agent.on('discover_commands', function(registry)
  registry:add {
    name = 'pr',
    about = 'Review a GitHub PR',
    aliases = { 'pull' },
    args = { { name = 'number', required = true, help = 'the PR number' } },
    run = function(ctx, args)
      return ctx:submit_prompt('Review PR #' .. args.number)
    end,
  }
end)

That registers /pr <number> (and the alias /pull). The command appears in /help and tab-completion, and /pr --help prints its own usage. Typing /pr 1234 runs the handler, which submits Review PR #1234 as a turn just as if you had typed it.

Built-in commands always win: a user command whose name or an alias collides with a built-in verb (or another user command) is dropped with a warning in scrollback, so a config can never shadow /quit or /model.

Arguments

Each entry in args becomes a clap argument, so validation, --help, and completion all follow from what you declare. An argument is positional unless you give it a long or short, which makes it a --named option instead:

  • name -- the key the parsed value arrives under in the handler's args table.
  • help -- the description shown in /<cmd> --help.
  • required -- whether the argument must be supplied (positional or option).
  • long / short -- the --long name or -s character; either one makes the argument a named option rather than positional.
  • flag -- a valueless switch (--verbose), delivered to the handler as a boolean: true when present, false otherwise.
  • multiple -- accepts more than one value, delivered as a list of strings.

Parsed values reach the handler keyed by each argument's name: a string for a single value, a list of strings for multiple, true/false for a flag, and nil for an optional argument that was not supplied.

What a handler returns

The handler receives a CommandCtx and the parsed args table. Its methods each build one CommandAction; the handler returns the single action it wants performed, or nil to do nothing.

ctx:submit_prompt(text, opts?) submits text as a user turn. An optional opts applies a model and/or personality override for just that turn, reverting afterwards:

run = function(ctx, _args)
  return ctx:submit_prompt('go', {
    model = 'anthropic/claude-opus-4-8',
    personality = 'review-panel/Execute',
  })
end,

ctx:set_input(text) fills the input box with text for you to edit and send yourself, rather than submitting it. Useful for a command that drafts a prompt you want to tweak first.

Background sub-agents

ctx:spawn_subagent{ ... } launches a detached sub-agent that runs alongside your interactive session, so you can reach a checkpoint, kick off longer work, and keep going in the main session while it runs:

registry:add {
  name = 'panel',
  about = 'Kick off the review panel in the background',
  run = function(ctx, _args)
    return ctx:spawn_subagent {
      personality = 'review-panel',
      mode = 'Execute',
      task = 'review the current branch',
      context = 'fork',
    }
  end,
}
  • personality -- the personality the sub-agent runs as.
  • mode -- the personality's mode to run in; its first when omitted.
  • task -- the prompt the sub-agent starts from.
  • context -- how its conversation is seeded: fresh (the default) starts it with only its task; fork forks the current conversation at this point, so the sub-agent sees the same history you do. The fork is captured when the command runs, so it reflects the checkpoint rather than a later turn.

The run is headless: it reports only its completion in scrollback, with a wa --resume <session> pointer to its own transcript on disk. While it runs it is listed by /jobs, can be cancelled with /kill, and holds back a plain /quit (which reports the outstanding work; /quit --force cancels and quits).

Keeping commands tidy

As with custom tools, a command is best kept in a self-contained module alongside your config (e.g. ~/.config/wallah/commands.lua) that you pull in with require('commands'), so it reads and reuses independently of the rest of your config.