Skip to content

Add Claude-Code-style slash command autosuggest to the main chat composer - #275

Merged
oldsj-mainloop[bot] merged 1 commit into
mainfrom
mainloop/slash-command-autosuggest
Oct 18, 2026
Merged

oldsj-mainloop[bot] merged 1 commit into
mainfrom
mainloop/slash-command-autosuggest

Conversation

@oldsj-mainloop

Copy link
Copy Markdown
Contributor

Summary

PR #266 added /model as a minimal client-side parser (slashCommand.ts) plus command logic (modelCommand.ts), but it only recognized a fully-typed command on submit — there was no autosuggest UI. This builds the autosuggest layer on top of that, without touching /model's own semantics.

Registry. slashCommand.ts now exports SLASH_COMMANDS, a { name, summary }[] registry (currently just /model), plus two small pure helpers: matchSlashCommands(registry, query) (prefix match, case-insensitive) and activeSlashToken(text, caret) (is the caret inside the leading /name token, and what's typed so far). A future slash command just needs an entry in SLASH_COMMANDS to show up in the dropdown.

Dropdown. There's no existing combobox/dropdown component in the app to reuse (checked web/src/components/ui and the chat/explorer features) — the closest patterns are ExplorerTree's roving-highlight list and RowMenu's floating popover, so the new menu borrows their conventions: bg-popover/border-border/shadow-md for the floating surface and bg-accent text-accent-foreground for the highlighted row, with role="listbox"/role="option"/aria-selected. It's a plain absolutely-positioned <ul> anchored to the composer's own relative input bar (bottom-full), not a portal, since it only ever needs to sit directly above that one box.

Where it lives. Composer takes an optional commands prop; when set, it tracks the caret and offers the dropdown. ThreadBody forwards the prop; ChatPage passes SLASH_COMMANDS. SessionChatSlot (the session composer, which shares ThreadBody/Composer) doesn't pass it, so that composer is unaffected — matching today's behavior where /model only means anything on the main thread.

Keyboard model, matching the task's spec:

  • Up/Down moves the highlight, wrapping both ways.
  • Tab always accepts the highlighted command: inserts /<name> and moves the caret just past the space.
  • Enter accepts the highlighted command unless what's typed is already an exact command name, in which case it sends the message instead (so /model{Enter} keeps working exactly as Add a /model slash command to the main chat composer #266 shipped it — verified by the existing /model tests, which I didn't touch).
  • Escape closes the dropdown without changing the text; typing further reopens it once the query changes.
  • A mouse click on an option accepts it without blurring the box (preventDefault on mousedown).

Deviation from the CLI: in Claude Code's terminal, Enter always accepts an open suggestion menu, and you'd use a different key (or an empty line) to send. Here Enter is also the composer's send key, so I added the "already exact" escape hatch — if I hadn't, /model{Enter} would re-accept a no-op completion instead of running the command, which is worse than the CLI's own behavior in a chat box. Tab remains the unambiguous accept-only key, same as the CLI.

Scope cuts, per the task: no argument-level suggestions after /model (nice-to-have, skipped to keep this to the command-name autosuggest), no new slash commands, no change to /model's behavior.

Test plan

  • vitest run src/features/chat (344 tests) and src/features/sessions (100 tests, since they share Composer/ThreadBody) — all green.
  • New slashCommand.test.ts cases for matchSlashCommands and activeSlashToken.
  • New Composer.test.tsx cases: dropdown open/close on a leading slash vs. ordinary text, prefix narrowing, closing once the caret passes into arguments, Up/Down with wraparound, Tab-complete, the Enter exact-vs-suggestion split, Escape, mouse accept, and that plain typing/Enter-to-send is unaffected when commands are registered.
  • tsc -b and eslint clean on the changed files; make lint clean (one formatting autofix applied).
  • mainloop-dev smoke: blocked — dev_sync failed three times in a row on an unrelated backend-pod issue (copy to backend was interrupted ... container not found, backend pod at 9 restarts) that a frontend-only TS/React change cannot cause. dev_status reports all pods Running/ready. Flagging as an infra blocker rather than retrying further.

🤖 Generated with Claude Code

…oser

Typing `/` at the start of the composer now opens a dropdown of matching
slash commands (currently just /model), narrowed as more is typed. Up/Down
moves the highlight, Tab or Enter accepts it (inserting the name plus a
trailing space), and Escape dismisses the dropdown. Enter still sends the
message once the typed name is already exact, so /model{Enter} keeps working
as before.

Builds on the registry and token-matching helpers added to slashCommand.ts,
which future slash commands join automatically.
@oldsj-mainloop
oldsj-mainloop Bot merged commit ef1b8cb into main Oct 18, 2026
6 checks passed
@oldsj-mainloop
oldsj-mainloop Bot deleted the mainloop/slash-command-autosuggest branch October 18, 2026 23:57
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