Skip to content

feat(commands): quoted arguments and {{ARGUMENTS}} for slash commands - #305

Open
nfertigsw wants to merge 1 commit into
co-l:developfrom
nfertigsw:feat/slash-command-arguments
Open

feat(commands): quoted arguments and {{ARGUMENTS}} for slash commands#305
nfertigsw wants to merge 1 commit into
co-l:developfrom
nfertigsw:feat/slash-command-arguments

Conversation

@nfertigsw

Copy link
Copy Markdown

Summary

Command templates already took positional {{name}} placeholders, but the invocation was split on whitespace with no notion of quoting, and nothing could capture a free-form instruction. Both gaps send the user back to editing the command every time they want to vary it:

/revue src/a.ts "gestion des erreurs"   →  {{angle}} got `"gestion`, the rest was dropped
/note rerun the flaky proxy test        →  no placeholder could hold this

Two additions, both in a command's Message template:

  • Quoted values — wrap an argument in "…" or '…' and it fills one placeholder whole. Inside double quotes a backslash escapes the next character; single quotes are literal, as in POSIX shells. Touching fragments join, so src/"my file".ts is one token. An unterminated quote takes the rest of the line rather than erroring — someone mid-keystroke has one most of the time, and the inline hint reads the same buffer.
  • {{ARGUMENTS}} — receives everything typed after the command id, verbatim. Excluded from positional numbering so it never steals a token another placeholder was waiting for, and case-sensitive: {{arguments}} stays an ordinary slot.

Both flow through the existing unfilled-placeholder path, so a command invoked bare still opens the params modal instead of shipping {{file}} to the model.

Full design rationale and edge cases: docs/DESIGN-SLASH-ARGUMENTS.md (added by this PR).

One implementation instead of two

Parsing lived in two near-duplicates: web/src/lib/parse-slash-command.ts for the chat composer and src/server/tasks/slash.ts for commands seeded into task sessions — each with its own extractTemplateParams and its own split(/\s+/). Both now delegate to src/shared/slash-args.ts, reachable from web through the existing @shared alias.

That was the point of the server-side copy existing in the first place: a command typed in chat and the same command seeded into a task should expand identically. They now do so by construction rather than by two implementations agreeing.

Two surfaces that follow from the parser

  • The composer's param=? hint counted arguments with split(/\s+/), so a quoted value made it skip ahead. It now counts with the tokenizer, and templateParamHints orders {{ARGUMENTS}} last regardless of where it sits in the template, so the hint walks the slots in the order they have to be typed. Computed server-side in routes/commands.ts, so the composer and the task editor get identical hints from one place.
  • The command editor gained a line describing the argument forms — including the {{name}} form, which nothing documented anywhere before.

Backward compatibility

With no {{ARGUMENTS}} in the template, positional resolution is exactly what it was. With no quotes in the invocation, tokenization is exactly what it was. Every pre-existing test passes with only the two new fields (args, rest) added to the parse result.

Workflows share the tokenizer, so quoted values now work for workflow parameters too. {{ARGUMENTS}} does not apply to them — they declare typed parameters rather than a template.

What to test

  • A multi-word value. Give a command two placeholders, e.g. Relis {{file}} en te concentrant sur {{angle}}. Send /cmd src/a.ts "gestion des erreurs" and confirm the second placeholder gets all three words. Without the quotes it should still take just the first word, as before.
  • Free-form. Write a command whose whole body is Add a note: {{ARGUMENTS}}, then send /cmd rerun the flaky proxy test. The full sentence should land in the prompt.
  • Mixed. Relis {{file}}. Consignes : {{ARGUMENTS}} — the first token fills {{file}} and {{ARGUMENTS}} still carries the whole line, the same duplication $1 + $ARGUMENTS produces elsewhere.
  • Nothing typed. Send the same commands bare (/cmd) and confirm the params modal opens and asks, rather than the raw {{file}} reaching the model.
  • The inline hint. Start typing /cmd and watch the grey name=? hint. It should advance one slot per token — a quoted value counts once, not once per word — and offer ARGUMENTS=? only after the positional slots.
  • Nothing regressed. Any existing command with plain positional arguments should behave exactly as before.
  • Tasks. Seed a task whose prompt is a slash command with a quoted argument; the session should start with the expanded prompt, matching what chat produces.

Tests

45 added. src/shared/slash-args.test.ts covers the tokenizer (quotes, escapes, touching fragments, unterminated quotes, empty quoted argument), parsing, hint ordering, and expansion. The two callers keep their own suites: src/server/tasks/slash.test.ts gains end-to-end resolveSlashLaunch cases for quoted arguments, {{ARGUMENTS}}, the mixed form, the bare-invocation refusal, and quoted workflow parameters; ChatInput.slash.test.tsx gains composer-level cases for a quoted value reaching the template and for an unfilled placeholder surviving to the modal.

Local run on WSL: npm run typecheck ✅, eslint src/ web/src/ ✅ (0 issues), npm run duplicate ✅ (0 clones), prettier --check ✅, e2e ✅ (335 passed), unit 5000 passed with one failure — inspect-proxy.test.ts > handles unreachable target gracefully. That test proxies to http://127.0.0.1:1 and expects a refused connection; on this machine the connect hangs instead (verified independently: a raw TCP connect to port 1 times out rather than being refused), so the proxy never answers and the test hits its 15s cap. It fails identically on a clean checkout without this branch. CI on ubuntu-latest should be unaffected.

AI-Enhanced Development

Tell what models helped shape this PR:

  • AI Models: Claude Opus 5 (1M context), via Claude Code

Cache Impact

Does this PR affect anything cached — system prompts, tool definitions, skills, or other context?

  • No. Command templates are not part of the system prompt, the tool definitions, or the skill metadata that feed computeDynamicContextHash — an expanded command becomes an ordinary user message. This PR changes only how that message's text is assembled before it is sent, so nothing cached is invalidated or reshaped.

Command templates already took positional `{{name}}` placeholders, but the
invocation was split on whitespace with no notion of quoting, and nothing could
capture a free-form instruction. Both gaps send the user back to editing the
command every time they want to vary it:

    /revue src/a.ts "gestion des erreurs"   -> {{angle}} got `"gestion`
    /note rerun the flaky proxy test        -> no placeholder could hold this

- Quote a value with `"…"` or `'…'` and it fills one placeholder whole. Inside
  double quotes a backslash escapes the next character; single quotes are
  literal. Touching fragments join, so `src/"my file".ts` is one token. An
  unterminated quote takes the rest of the line rather than erroring — someone
  mid-keystroke has one most of the time.
- `{{ARGUMENTS}}` receives everything typed after the id, verbatim. It is
  excluded from positional numbering so it never steals a token another
  placeholder was waiting for, and it is case-sensitive: `{{arguments}}` stays
  an ordinary slot.

Both flow through the existing unfilled-placeholder path, so a command invoked
bare still opens the params modal instead of shipping `{{file}}` to the model.

Parsing lived in two near-duplicates — `web/src/lib/parse-slash-command.ts` for
the composer and `src/server/tasks/slash.ts` for commands seeded into tasks,
each with its own `extractTemplateParams` and its own split. Both now delegate
to `src/shared/slash-args.ts`, so a command typed in chat and the same command
seeded into a task expand identically, which was the point of the server-side
copy existing at all.

Two surfaces follow from the parser:

- the composer's `param=?` hint counted args by whitespace and skipped ahead on
  a quoted value; it now counts with the tokenizer, and `{{ARGUMENTS}}` is
  ordered last so the hint walks the slots in the order they must be typed
- the command editor gained a line describing all of this, including the
  `{{name}}` form that nothing documented before

Backward compatible: with no `{{ARGUMENTS}}` in the template positional
resolution is unchanged, and with no quotes in the invocation tokenization is
unchanged. Existing tests pass with only the two new parse-result fields added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant