feat(commands): quoted arguments and {{ARGUMENTS}} for slash commands - #305
Open
nfertigsw wants to merge 1 commit into
Open
feat(commands): quoted arguments and {{ARGUMENTS}} for slash commands#305nfertigsw wants to merge 1 commit into
nfertigsw wants to merge 1 commit into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:Two additions, both in a command's Message template:
"…"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, sosrc/"my file".tsis 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.tsfor the chat composer andsrc/server/tasks/slash.tsfor commands seeded into task sessions — each with its ownextractTemplateParamsand its ownsplit(/\s+/). Both now delegate tosrc/shared/slash-args.ts, reachable from web through the existing@sharedalias.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
param=?hint counted arguments withsplit(/\s+/), so a quoted value made it skip ahead. It now counts with the tokenizer, andtemplateParamHintsorders{{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 inroutes/commands.ts, so the composer and the task editor get identical hints from one place.{{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
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.Add a note: {{ARGUMENTS}}, then send/cmd rerun the flaky proxy test. The full sentence should land in the prompt.Relis {{file}}. Consignes : {{ARGUMENTS}}— the first token fills{{file}}and{{ARGUMENTS}}still carries the whole line, the same duplication$1+$ARGUMENTSproduces elsewhere./cmd) and confirm the params modal opens and asks, rather than the raw{{file}}reaching the model./cmdand watch the greyname=?hint. It should advance one slot per token — a quoted value counts once, not once per word — and offerARGUMENTS=?only after the positional slots.Tests
45 added.
src/shared/slash-args.test.tscovers 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.tsgains end-to-endresolveSlashLaunchcases for quoted arguments,{{ARGUMENTS}}, the mixed form, the bare-invocation refusal, and quoted workflow parameters;ChatInput.slash.test.tsxgains 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 tohttp://127.0.0.1:1and 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:
Cache Impact
Does this PR affect anything cached — system prompts, tool definitions, skills, or other context?
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.