feat(web): sidebar pills + keyboard shortcuts (claude.ai parity)#133
Open
constkolesnyak wants to merge 5 commits into
Open
feat(web): sidebar pills + keyboard shortcuts (claude.ai parity)#133constkolesnyak wants to merge 5 commits into
constkolesnyak wants to merge 5 commits into
Conversation
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>
This was referenced Jun 20, 2026
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related web UX themes plus their interaction fixes, consolidated into a single PR (originally split as #133 shortcuts and #135 pills):
Keyboard bindings
Cmd/Ctrl + KCmd/Ctrl + ⇧OCmd/Ctrl + /Cmd/Ctrl + ⇧SCmd/Ctrl + ⇧;Cmd/Ctrl + ⇧CCmd/Ctrl + ⇧⌫Cmd/Ctrl + \EscMac/Linux labels (
⌘vsCtrl) 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
useKeyboardShortcuts) instead of per-component handlers — global behaviour stays consistent and we don't pile up listeners.keyboard.tsutils —formatCombo(),matchesCombo(),isSafeInInputCombo(). Cmd/Ctrl combos and Esc fire even when focus is in an input/textarea/contentEditable; printable keys still need explicitallowInInput: 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
feat(web): keyboard shortcuts (claude.ai parity)— base shortcut system + modalfeat(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 focusfeat(sidebar): replace header with Search/New chat pills— pills + fade-in lazy mountfix(chat): restore Cmd+K — mount sidebar search before focusing— Cmd+K stopped working after pills becausegetElementByIdreturned null on an unmounted input. AddrequestSearchFocus()action +searchPinnedstate in the sidebar.fix(chat): Cmd+K race — focus on mount, release pin only after onFocus confirms— race where dropping the pin synchronously could collapseshouldShowSearchmid-focus and fade the input out.Tests
No new tests — pure web feature, no backend touch.
npm run buildclean.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).