Skip to content

feat(web-ui): precise slash-command matcher#1023

Open
jarvis24young wants to merge 1 commit into
GCWing:mainfrom
jarvis24young:feat/precise-slash-command-matcher
Open

feat(web-ui): precise slash-command matcher#1023
jarvis24young wants to merge 1 commit into
GCWing:mainfrom
jarvis24young:feat/precise-slash-command-matcher

Conversation

@jarvis24young
Copy link
Copy Markdown
Contributor

What this PR does

Replaces the nine loose text.startsWith('/btw') checks scattered across
ChatInput.tsx with a single isSlashCommand(text, command) helper that
enforces a proper word boundary. This fixes two real bugs in one place.

Bug 1 — word-boundary miss (the headline fix)

Before this PR, the chat composer used text.toLowerCase().startsWith('/btw')
to detect the /btw command. That predicate has no word boundary, so
every command that happened to start with the same prefix was
accidentally dispatched as that command.

User types Before this PR After this PR
/btw 你好 /btw (correct) /btw (correct)
/btwextra 你好 /btw 你好 (sent to btw side-session) ✅ generic message
/goals 我的目标 /goal 我的目标 (entered goal mode!) ✅ generic message
/compactor /compact (showed usage error) ✅ generic message
/usage2 /usage (showed usage error) ✅ generic message

Bug 2 — Claude Code 2.1.147: trailing tab / newline

A slash command followed by a stray tab or newline used to be rejected
by the strict regexes in the Send path. The new helper uses the
lookahead (?=\s|$), which treats tab and newline as valid trailing
whitespace.

User types Before this PR After this PR
/goal 修 bug\t ❌ unknown command /goal 修 bug
/btw\n ❌ unknown command /btw
/btw hello /btw hello /btw hello (unchanged)

How it works

A new module at src/flow_chat/utils/slashCommand.ts exports three
helpers:

matchesSlashCommand(text)        // → '/btw' | '/goal' | null
isSlashCommand(text, command)    // → boolean, with word boundary
stripSlashCommand(text, command) // → argument portion, trims trailing WS

The internal regex is ^(\/[a-zA-Z][\w:-]*)(?=\s|$)/ — the
(?=\s|$) lookahead is what enforces a proper boundary at the first
whitespace or end-of-string.

All nine existing call sites in ChatInput.tsx are routed through
isSlashCommand. The scope covers every detection path:

  • handleInputChange (3 sites) — picker activation gating
  • handleSendOrCancel (5 sites) — Send-button dispatch
  • Enter-key handler (1 site) — direct keyboard submit

The call at ChatInput.tsx:2005
(message.toLowerCase().startsWith('/goal') && !isGoalSlashCommand(message))
is deliberately kept as a loose check — it is the fallback that
shows "Use /goal with optional focus text…" when the user typed
something like /goals foo that almost matches but is malformed.

Tests

20 unit tests in src/flow_chat/utils/slashCommand.test.ts:

Test Files  1 passed (1)
     Tests  20 passed (20)

Coverage includes:

  • All four happy-path cases (/btw, /goal, /usage, /DeepReview)
  • All 2.1.147 trailing-whitespace cases (tab, newline, CRLF)
  • All word-boundary false-positive cases (/btwextra, /goals, /compactor, /usage2)
  • MCP prompt commands with : and - in the name
  • Defensive non-string inputs
  • Regex metacharacter handling

Code review

Reviewed by an independent codex review pass. The two highest-severity
findings were:

  1. Initial PR only fixed 2 of 9 call sites (HIGH). Resolved by
    expanding the PR to cover all 9 in this same commit.
  2. Test coverage gap on regex metacharacter escape (LOW). Closed
    with the metacharacter-isolation test.

All other findings were LOW (JSDoc nit on \t/\n reading as
literals; command: string could be a tighter /${string} template
literal type) and are documented but not blocking.

Files changed

src/web-ui/src/flow_chat/utils/slashCommand.ts          (new,  94 lines)
src/web-ui/src/flow_chat/utils/slashCommand.test.ts    (new, 153 lines)
src/web-ui/src/flow_chat/components/ChatInput.tsx      (modified, +15/-10)

Test plan for review

  • cd src/web-ui && npx vitest run slashCommand → 20/20 pass
  • cd src/web-ui && npx tsc --noEmit → 0 errors
  • Manually open BitFun, type /btwextra hello, press Enter → message
    is dispatched as a generic user message (NOT to a btw side-session)
  • Type /goals my goal, press Enter → message dispatched as a generic
    user message (NOT goal mode)
  • Type /btw\t, press Enter → opens a btw side-session as expected
  • Type /goal 修 bug, press Tab (in a non-shift-target context), press
    Enter → enters goal mode as expected

Replace the nine loose `text.startsWith('/btw')` checks scattered
across ChatInput.tsx with a single `isSlashCommand(text, command)`
helper that enforces a proper word boundary. This fixes two real
bugs in one place:

1. Word-boundary miss: `/btwextra hello` used to be treated as
   `/btw hello` and dispatched as a side-question. The new helper
   requires `/btw` to be followed by whitespace or end-of-string,
   so `/btwextra` now correctly falls through to the generic-send
   path. The same fix applies to `/goals`, `/compactor`, `/usage2`,
   etc.

2. Trailing whitespace handling (Claude Code 2.1.147): the new
   `(?=\s|$)` lookahead treats tab and newline as valid
   trailing boundaries, so `/goal focus the bug\t` and
   `/btw\n` are still recognised as their command instead of
   being mis-classified as unknown.

Updated call sites (nine in total) cover all three detection
paths in ChatInput.tsx:

  * `handleInputChange` (3 sites) — picker activation gating
  * `handleSendOrCancel` (5 sites) — Send-button dispatch
  * Enter-key handler (1 site) — direct-keyboard submit

The helper is centralised in `src/flow_chat/utils/slashCommand.ts`
and exported with three signatures:

  * `matchesSlashCommand(text)` — returns the matched token
  * `isSlashCommand(text, command)` — predicate for one command
  * `stripSlashCommand(text, command)` — extracts the argument

20 unit tests cover the happy path, the 2.1.147 trailing-tab /
newline cases, the `/btwextra` boundary cases, the MCP prompt
names (containing `:` and `-`), and defensive non-string inputs.
Copy link
Copy Markdown
Collaborator

@wsp1911 wsp1911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tightening the slash-command matcher. The helper and focused tests look good, and the /btwextra / /usage2 cases are exactly the kind of regression coverage we want here.

One merge-sensitive issue: this PR is currently behind main, and the conflicted area in ChatInput.tsx overlaps with ACP slash-command handling. main added localSlashCommandsEnabled = !isAcpInputSession gates so ACP sessions can pass their own slash commands through instead of being captured by local /btw, /compact, /usage, etc. When resolving the conflict, please keep those gates around the new matcher, e.g. localSlashCommandsEnabled && isSlashCommand(...).

Small consistency issue: there is still one loose /goal prefix check in the send-button path:

message.toLowerCase().startsWith('/goal') && !isGoalSlashCommand(message)

The normal keyboard flow for /goals shows “no matching command” in the picker, but if the picker is still open and the user clicks the send button, this branch fires and shows the /goal usage warning. That preserves the old prefix-conflation behavior for this click path. Could we align this branch with the new boundary matcher as well?

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.

2 participants