fix(chat-ui): let ⌘B reach the sidebar toggle when nothing is selected in the composer

Merged
AV
2d ago
+180-10

Summary

  • On macOS, with the prompt composer focused and nothing selected, ⌘B toggles the workspace sidebar again.
  • Every other bold/italic shortcut behaves as on main: ⌘B on a selection, ⌘I, and Ctrl+B / Ctrl+I on Windows and Linux format in the editor and are kept from app hotkeys.

Why / Context

#9128 added rich text to the composer and stopped propagation of every ⌘B and ⌘I press so formatting would not also trigger app hotkeys. That also meant the sidebar could not be toggled while the composer had focus. The New Workspace home opens with the cursor in the composer, so ⌘B appeared broken there until focus moved.

Only ⌘B on macOS collides with an app binding. Windows and Linux bind the sidebar to Ctrl+Shift+B, which the composer never matched. ⌘I's app binding (terminal rich input) runs only while a terminal pane is active, which a composer pane never is. So only that one chord is handed back.

How It Works

getFormatShortcutTarget(event, { isApple, hasSelection }) (new, pure) returns:

  • "app" for ⌘B with a caret on macOS. The composer's KEY_DOWN_COMMAND handler (COMMAND_PRIORITY_LOW) then calls preventDefault() and returns true. Returning true skips Lexical's bold shortcut (a KEY_DOWN_COMMAND listener at EDITOR priority); preventDefault() blocks Chromium's native formatBold beforeinput. The event still bubbles to document, where useHotkey (enableOnContentEditable: true; react-hotkeys-hook does not check defaultPrevented) toggles the sidebar.
  • "editor" for every other bold/italic shortcut. The handler stops propagation and lets Lexical format, same as main.
  • null for anything else, including Ctrl+B on macOS (native caret move) and Ctrl+Shift+B.

Matching uses Lexical's isExactShortcutMatch with the same modifier mask Lexical formats with, so non-Latin layouts match by key code.

Manual QA Checklist

Driven over CDP with real Input.dispatchKeyEvent / mouse input in the desktop dev app (new shell, /new-workspace home composer), on the first commit. Baseline taken by hot-swapping the main version of ComposerBody.tsx into the same running app. The second commit leaves the macOS ⌘B path unchanged:

StepmainThis PR
Type hello world, ⌘B with caretSidebar stays open; next typed character is boldSidebar collapses; nothing bold
Double-click hello, ⌘Bhello bold, sidebar unchangedhello bold, sidebar unchanged
console.error during the runnonenone
  • macOS: ⌘B with caret toggles the workspace sidebar from the home composer
  • macOS: ⌘B on a selection bolds it and does not toggle the sidebar
  • macOS: ⌘I with caret starts italic and does not reach app hotkeys (not re-run live after the second commit; covered by getFormatShortcutTarget tests)
  • Windows / Linux: Ctrl+B / Ctrl+I unchanged from main (not run; covered by the editor tests, which run on happy-dom's non-Apple platform)

Testing

  • bun test --isolate in packages/chat-ui (Bun 1.4.3): 100 pass, 0 fail.
    • getFormatShortcutTarget.test.ts: the decision table for both platforms (⌘B caret / selection / non-Latin layout, ⌘I, ⌘⇧B, Ctrl+B on macOS, Ctrl+B / Ctrl+I / Ctrl+Shift+B elsewhere, unrelated keys).
    • ComposerBody.test.tsx: in the real editor, mod+B / mod+I on a selection format and stay out of document; Ctrl+B / Ctrl+I with a caret format the next text and stay out of document.
  • tsc --noEmit in packages/chat-ui
  • biome check on the changed files

Known Limitations

  • On macOS, ⌘B with only a caret no longer turns on bold for the next typed text, including inside a bold run; select the text first.
  • happy-dom reports a non-Apple platform, so the editor tests cannot reach the macOS "app" wiring (preventDefault + return true). That path is covered by the decision-table test and the CDP run above.
  • The two selection tests print a Lexical Attempted to assign to readonly property error without failing. happy-dom dispatches selectionchange synchronously inside Lexical's commit (Chromium queues it). It does not occur in the app.

Follow-ups

  • The Channels composer (MessageComposerBody) has no guard at all on main: ⌘B on a selection there bolds the text and also toggles the workspace sidebar. A shared helper for both composers would fix it.