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
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)
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
/planin the composer and tapping the round Send button results in Claude replying: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:<textarea>'sonKeyDownhandler (viauseSlashCommands).<button>callsonSenddirectly — there is noresolveCommand(draft)check on that path.Relevant code:
And in
session-detail.tsx:So the Send button's path is:
onSend()→send()→POST /sessions/:id/messageswith the raw"/plan"text → worker → SDK → "isn't available".Why mobile is hit 100%
onKeyDowncorrectly resolves and dispatches. But desktop is also broken if you click Send instead of pressing Enter — just less visible.Secondary UX bug
resolveCommand('/plan')returnsnullwhentakesArgs && !args(seecommands.ts:48-54andcommands.test.ts:37). By design, bare/planfalls 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
/plan refactor Xand tapping the round Send button runs the plan command (worker receivesplanMode: true), not a raw text message./clear,/help,/abort— clicking Send fires the command, never the raw text./plan do Xfires the command (previously broken too, less visible)./planand submitting (either path) shows an inline hint that an argument is required and does not send"/plan"as a normal message.draft === '/plan do X'callsonCommand(plan, "do X"), notonSend. Vitest + RTL, style consistent with neighboringcommands.test.ts.Proposed fix
Extract a single
submit()incomposer.tsxthat both the Send button and the Enter branch funnel through:onClickand the Enter branch throughsubmit().onCommanddispatch inuseSlashCommands.onKeyDownso there is one submission path (avoid logic forking).Affected files
packages/web/src/features/sessions/session-detail/composer.tsxpackages/web/src/features/sessions/session-detail/use-slash-commands.tspackages/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 alongsideRefs
packages/web/src/features/sessions/session-detail/composer.tsxpackages/web/src/features/sessions/session-detail/use-slash-commands.tspackages/web/src/features/sessions/session-detail/commands.tspackages/web/src/features/sessions/session-detail/commands.test.tspackages/web/src/features/sessions/session-detail.tsx(runCommand)/planend-to-end origin: commite7649a0(feat: real plan mode — /plan runs the turn read-only)