Skip to content

feat(web): sidebar pills + keyboard shortcuts (claude.ai parity)#133

Open
constkolesnyak wants to merge 5 commits into
ClickHouse:mainfrom
constkolesnyak:upstream/web-keyboard-shortcuts
Open

feat(web): sidebar pills + keyboard shortcuts (claude.ai parity)#133
constkolesnyak wants to merge 5 commits into
ClickHouse:mainfrom
constkolesnyak:upstream/web-keyboard-shortcuts

Conversation

@constkolesnyak

@constkolesnyak constkolesnyak commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Two related web UX themes plus their interaction fixes, consolidated into a single PR (originally split as #133 shortcuts and #135 pills):

  1. Keyboard shortcuts — claude.ai-parity bindings + help modal
  2. Sidebar pills — replace the static search header with hover-mount Search / New chat pills
  3. Fixes — making Cmd+K work after the pills change unmounts the search input

Keyboard bindings

Shortcut Action
Cmd/Ctrl + K Focus sidebar search
Cmd/Ctrl + ⇧O New chat
Cmd/Ctrl + / Open shortcut help modal
Cmd/Ctrl + ⇧S Toggle session sidebar
Cmd/Ctrl + ⇧; Focus message input
Cmd/Ctrl + ⇧C Copy last response
Cmd/Ctrl + ⇧⌫ Delete current session (confirmed)
Cmd/Ctrl + \ Toggle side panel
Esc Close modal · clear search · stop generation

Mac/Linux labels ( vs Ctrl) and Backspace/Delete aliases render automatically in the modal.

Sidebar pills

Replaces the always-visible search input + static "Conversations" header with two rounded pills: Search sessions (left) and New chat (right). The search input mounts on hover / focus / Cmd+K, fades in over 200ms, and unmounts again when nothing's using it. Reclaims sidebar vertical space without losing functionality.

Architecture

  • One document-level keydown listener (useKeyboardShortcuts) instead of per-component handlers — global behaviour stays consistent and we don't pile up listeners.
  • keyboard.ts utilsformatCombo(), matchesCombo(), isSafeInInputCombo(). Cmd/Ctrl combos and Esc fire even when focus is in an input/textarea/contentEditable; printable keys still need explicit allowInInput: true.
  • ShortcutsModal — grouped reference list, opens on Cmd/Ctrl + /.
  • uiStore — Zustand store for modal open state + sidebar visibility toggle.
  • chatStore.requestSearchFocus() — nonce-bumping action the sidebar subscribes to, so Cmd+K can mount + focus a lazily-mounted input without poking the DOM.

Why pills, not a static field

The previous design committed a full row of vertical space to a permanently-visible search field even though searching is intermittent. The pill stays compact when idle and only grows when you reach for it. The 200ms fade is short enough not to feel laggy on a deliberate hover; the unmount-on-leave keeps the DOM small. Cmd+K still focuses it instantly.

Subjective UI — happy to drop the redesign or rework if it doesn't fit upstream taste.

Commits

  1. feat(web): keyboard shortcuts (claude.ai parity) — base shortcut system + modal
  2. feat(chat): auto-focus input on session change & let mod/Esc shortcuts fire while typing — makes Cmd/Ctrl shortcuts work even when the chat textarea has focus
  3. feat(sidebar): replace header with Search/New chat pills — pills + fade-in lazy mount
  4. fix(chat): restore Cmd+K — mount sidebar search before focusing — Cmd+K stopped working after pills because getElementById returned null on an unmounted input. Add requestSearchFocus() action + searchPinned state in the sidebar.
  5. fix(chat): Cmd+K race — focus on mount, release pin only after onFocus confirms — race where dropping the pin synchronously could collapse shouldShowSearch mid-focus and fade the input out.

Tests

No new tests — pure web feature, no backend touch. npm run build clean.

Verified by hand: pills hover-mount, Cmd+K focuses from any page, Cmd+/ opens help modal, Esc cascades correctly (modal → search → stop generation).

Note

This supersedes #135 (which I'll close in favour of this one).

Generated with Claude Code

Adds a small shortcut system around a single document-level keydown
listener plus a help modal. Mirrors the bindings claude.ai/chat exposes,
skipping ones that don't map (artifacts, force-send, settings).

Global: ⌘⇧O new chat, ⌘K focus search, ⌘/ shortcuts modal,
Esc cascades to modal → search → stop generation.

Chat: ⌘⇧S sidebar, ⌘⇧; focus input, ⌘⇧C copy last response,
⌘⇧⌫ delete current (confirmed), ⌘\ side panel (already existed).

Mac/Linux labels and Backspace/Delete aliases render automatically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s fire while typing

- ChatInput: focus textarea whenever activeSession changes (new chat or session switch)
- useKeyboardShortcuts: default to allowing Cmd/Ctrl combos and Escape even when focus
  is inside an input/textarea/contentEditable; printable keys still need explicit opt-in
- keyboard.ts: add isSafeInInputCombo() helper; clarify allowInInput semantics
- Remove 'Conversations' header label
- Convert search field into a pill button labeled 'Search sessions';
  the input mounts on hover (or focus / when a query exists) with a
  200ms fade and unmounts on leave so it never lingers in the DOM
- Add a 'New chat' pill on the right; the search input overlays it
  while open so the create button is hidden behind the field
- Both controls now share rounded-full styling with a subtle border
  to read clearly as buttons
The sidebar Search pill unmounts its input unless hovered/focused/searched,
so document.getElementById('nerve-sidebar-search') was returning null and
the shortcut silently did nothing.

- chatStore: add searchFocusNonce + requestSearchFocus() trigger
- SessionSidebar: subscribe to the nonce; pin → mount → fade in → focus
- App.tsx: Cmd+K now calls requestSearchFocus() instead of poking the DOM
…s confirms

Previous attempt dropped `searchPinned` synchronously right after `.focus()`,
which can produce a render where pinned=false AND focused=false (onFocus
still in the React event queue) — that collapses `shouldShowSearch` and
starts the 200ms fade-out before focused=true commits.

- SessionSidebar: focus as soon as the input is mounted (don't wait for the
  fade-in); release the pin only once searchFocused becomes true, so the
  input is always kept visible while the focus event is in flight.
- App.tsx: skip setTimeout(0) when already on /chat — only defer when we
  just kicked off a navigate.
@constkolesnyak constkolesnyak changed the title feat(web): keyboard shortcuts (claude.ai parity) feat(web): sidebar pills + keyboard shortcuts (claude.ai parity) Jun 22, 2026
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