Skip to content

web: slash commands (/plan, /clear, /help, /abort) sent as plain text when using the Send button — mobile 100% broken #3

@bencode

Description

@bencode

Goal

Slash commands (/plan, /clear, /help, /abort) must fire as commands regardless of how the user submits — Enter on desktop AND the round Send button on mobile/desktop. Today only the Enter path works; the Send button bypasses slash resolution and ships the raw text as a normal message, breaking 100% of mobile usage.

Symptom

On iOS, typing /plan in the composer and tapping the round Send button results in Claude replying:

/plan isn't available in this environment.

That string is not in baton's codebase (grepped the whole repo) — it comes from the Claude Code SDK responding to an unknown slash command. So baton sent the literal "/plan" as a regular user message instead of intercepting it.

Root cause

packages/web/src/features/sessions/session-detail/composer.tsx:

  • Slash-command resolution is wired only into the <textarea>'s onKeyDown handler (via useSlashCommands).
  • The round Send <button> calls onSend directly — there is no resolveCommand(draft) check on that path.

Relevant code:

// composer.tsx ~154-161 — Send button: no slash check
<button type="button" onClick={onSend} ...>

// composer.tsx ~116-124 — textarea: the only slash entry point
onKeyDown={e => {
  if (slash.onKeyDown(e)) return        // ← resolves /command on Enter
  if (e.key === 'Enter' && !e.shiftKey) {
    if (canSend) onSend()
  }
}}

And in session-detail.tsx:

onSend={() => void send()}              // → api.sessions.sendMessage(...)

So the Send button's path is: onSend()send()POST /sessions/:id/messages with the raw "/plan" text → worker → SDK → "isn't available".

Why mobile is hit 100%

  • iPhone has no physical Enter key; users tap the round Send button. That path skips slash resolution every single time.
  • Desktop users pressing Enter still work — the textarea onKeyDown correctly resolves and dispatches. But desktop is also broken if you click Send instead of pressing Enter — just less visible.

Secondary UX bug

resolveCommand('/plan') returns null when takesArgs && !args (see commands.ts:48-54 and commands.test.ts:37). By design, bare /plan falls through to a newline. But on mobile a user types /plan, taps Send, and gets no hint that args are required — the text ships as a plain message. There should be an inline hint like "type the task after /plan" rather than silently turning it into a regular chat message.

Verification

  • On iOS Safari and Chrome mobile: typing /plan refactor X and tapping the round Send button runs the plan command (worker receives planMode: true), not a raw text message.
  • Same for /clear, /help, /abort — clicking Send fires the command, never the raw text.
  • On desktop: pressing Enter still works (no regression on the existing path).
  • On desktop: clicking the Send button on /plan do X fires the command (previously broken too, less visible).
  • Typing bare /plan and submitting (either path) shows an inline hint that an argument is required and does not send "/plan" as a normal message.
  • Regression test added: clicking Send while draft === '/plan do X' calls onCommand(plan, "do X"), not onSend. Vitest + RTL, style consistent with neighboring commands.test.ts.

Proposed fix

Extract a single submit() in composer.tsx that both the Send button and the Enter branch funnel through:

const submit = () => {
  const resolved = resolveCommand(draft)
  if (resolved) {
    onCommand(resolved.command, resolved.args)
    setDraft('')
    return
  }
  // known slash but missing required args (e.g. bare "/plan") → inline hint, don't ship as text
  const parsed = parseSlash(draft)
  if (parsed && SLASH_COMMANDS.some(c => c.name === parsed.name)) {
    setSlashHint(`/${parsed.name} needs an argument`)
    return
  }
  if (canSend) onSend()
}
  • Wire both the Send button's onClick and the Enter branch through submit().
  • Remove the duplicate onCommand dispatch in useSlashCommands.onKeyDown so there is one submission path (avoid logic forking).

Affected files

  • packages/web/src/features/sessions/session-detail/composer.tsx
  • packages/web/src/features/sessions/session-detail/use-slash-commands.ts
  • packages/web/src/features/sessions/session-detail/commands.ts — optionally a helper to detect "known-but-missing-args"
  • packages/web/src/features/sessions/session-detail/commands.test.ts — extend; add the Send-button regression test alongside

Refs

  • Composer: packages/web/src/features/sessions/session-detail/composer.tsx
  • Slash hook: packages/web/src/features/sessions/session-detail/use-slash-commands.ts
  • Slash registry & parser: packages/web/src/features/sessions/session-detail/commands.ts
  • Existing tests: packages/web/src/features/sessions/session-detail/commands.test.ts
  • Dispatch site: packages/web/src/features/sessions/session-detail.tsx (runCommand)
  • /plan end-to-end origin: commit e7649a0 (feat: real plan mode — /plan runs the turn read-only)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions