Skip to content

desktop: declare the menu bar, and fold its chords into the keyboard registry - #123

Merged
samkeen merged 2 commits into
mainfrom
claude/gh-119-22lj0p
Jul 28, 2026
Merged

desktop: declare the menu bar, and fold its chords into the keyboard registry#123
samkeen merged 2 commits into
mainfrom
claude/gh-119-22lj0p

Conversation

@samkeen

@samkeen samkeen commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

tauri.conf.json set no menu, so Tauri installed Menu::default() and a
dozen chords came with it — ⌘Q ⌘W ⌘M ⌘H ⌥⌘H ⌘Z ⇧⌘Z ⌘X ⌘C ⌘V ⌘A ⌃⌘F.
They were live in the window and invisible twice over. Nothing enumerates
that default, so no documentation could list them; and AppKit dispatches
a menu key equivalent inside NSApplication.sendEvent, before the key
window's responder chain, so they never reach the webview's keydown and
the keyboard registry could not observe them either. Since #118 every
binding is discoverable by construction — shortcuts.test.ts fails on
one no row documents — but these are not bindings, so they sat outside
that guarantee entirely. K1 promises a keyboard path that is findable.

So the menu is B2's own data now. crates/b2-desktop/src/menu.rs holds
one table — sections, items, and the chord macOS gives each — with two
readers: build, which is what the window gets, and chords, which the
new menu_chords command hands the UI. The items stay
PredefinedMenuItems deliberately: the Edit menu is load-bearing rather
than decorative, since those native items are what route cut/copy/paste
into the webview. The consequence is that B2 doesn't choose these
accelerators — muda assigns them and exposes no getter — so the table
restates them, and says so.

Two departures from the default, neither touching a chord: its Window
menu repeats Close Window (⌘W), which already lives in File, and its Help
menu is empty on macOS.

On the UI side ui/src/menukeys.ts is the third keyboard, beside
bindings.ts (B2's own) and editorkeys.ts (CodeMirror's). It mirrors the
host's declaration for the two jobs a runtime fetch can't do — the
suite's gate runs in node with no host to ask, and the sheet has to paint
before the first invoke resolves — and the mirror is checked against
the host at every boot (menuDrift), the "change them together" posture
WRITE_CONFLICT_MESSAGE and VAULT_CHANGED_EVENT already use across
this seam. What the reader sees comes from the host: render.ts passes
state.menuChords into shortcuts(), so the sheet's new "The menu bar"
group is the menu the app installed, not the UI's copy of it.

The gate is menuOverlaps, not more rows in conflicts(), and the
difference is the point. conflicts() asks a same-scope question,
because scope is how an inner surface legitimately answers first — the
rename field's Esc before the overlay cascade, the Settings rail's ⌃Tab
before the Tab trap. Against the menu that move buys nothing: the
keystroke is taken before the webview is consulted, so an editor-scoped
⌘Z is not "nearer the user", it is dead. So the comparison ignores scope
entirely. Nothing collides today; menukeys.test.ts proves the check can
fail rather than only that it passes.

One thing the new enumeration surfaces, pinned as its own case: the menu
takes ⌘Z, ⇧⌘Z and ⌘A from CodeMirror, which binds all three. The note
editor's undo, redo and select-all are therefore the webview's native
ones rather than CodeMirror's history and selection commands. That was
already true; there was nowhere to write it down.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_015SJgCnKnUTKNHVsSxrXq7h

Summary by CodeRabbit

  • New Features

    • Added an explicit desktop menu bar with defined application actions and reserved menu keyboard shortcuts.
    • The Keyboard settings panel now uses the desktop-provided menu shortcuts.
    • Added background loading and verification to keep the in-app keyboard reference in sync with the actual menu bar.
  • Bug Fixes

    • Improved detection of conflicts between menu-bar shortcuts, editor shortcuts, and custom keybindings.
    • Enhanced drift reporting when the menu shortcuts differ from the in-app reference.
  • Tests

    • Added coverage for menu chord parsing, overlap detection, drift checks, and keyboard panel rendering.

…registry

`tauri.conf.json` set no menu, so Tauri installed `Menu::default()` and a
dozen chords came with it — ⌘Q ⌘W ⌘M ⌘H ⌥⌘H ⌘Z ⇧⌘Z ⌘X ⌘C ⌘V ⌘A ⌃⌘F.
They were live in the window and invisible twice over. Nothing enumerates
that default, so no documentation could list them; and AppKit dispatches
a menu key equivalent inside `NSApplication.sendEvent`, before the key
window's responder chain, so they never reach the webview's keydown and
the keyboard registry could not observe them either. Since #118 every
*binding* is discoverable by construction — `shortcuts.test.ts` fails on
one no row documents — but these are not bindings, so they sat outside
that guarantee entirely. K1 promises a keyboard path that is findable.

So the menu is B2's own data now. `crates/b2-desktop/src/menu.rs` holds
one table — sections, items, and the chord macOS gives each — with two
readers: `build`, which is what the window gets, and `chords`, which the
new `menu_chords` command hands the UI. The items stay
`PredefinedMenuItem`s deliberately: the Edit menu is load-bearing rather
than decorative, since those native items are what route cut/copy/paste
into the webview. The consequence is that B2 doesn't *choose* these
accelerators — muda assigns them and exposes no getter — so the table
restates them, and says so.

Two departures from the default, neither touching a chord: its Window
menu repeats Close Window (⌘W), which already lives in File, and its Help
menu is empty on macOS.

On the UI side `ui/src/menukeys.ts` is the third keyboard, beside
bindings.ts (B2's own) and editorkeys.ts (CodeMirror's). It mirrors the
host's declaration for the two jobs a runtime fetch can't do — the
suite's gate runs in node with no host to ask, and the sheet has to paint
before the first `invoke` resolves — and the mirror is checked against
the host at every boot (`menuDrift`), the "change them together" posture
`WRITE_CONFLICT_MESSAGE` and `VAULT_CHANGED_EVENT` already use across
this seam. What the reader *sees* comes from the host: render.ts passes
`state.menuChords` into `shortcuts()`, so the sheet's new "The menu bar"
group is the menu the app installed, not the UI's copy of it.

The gate is `menuOverlaps`, not more rows in `conflicts()`, and the
difference is the point. `conflicts()` asks a same-scope question,
because scope is how an inner surface legitimately answers first — the
rename field's Esc before the overlay cascade, the Settings rail's ⌃Tab
before the Tab trap. Against the menu that move buys nothing: the
keystroke is taken before the webview is consulted, so an editor-scoped
⌘Z is not "nearer the user", it is dead. So the comparison ignores scope
entirely. Nothing collides today; `menukeys.test.ts` proves the check can
fail rather than only that it passes.

One thing the new enumeration surfaces, pinned as its own case: the menu
takes ⌘Z, ⇧⌘Z and ⌘A from CodeMirror, which binds all three. The note
editor's undo, redo and select-all are therefore the webview's native
ones rather than CodeMirror's history and selection commands. That was
already true; there was nowhere to write it down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015SJgCnKnUTKNHVsSxrXq7h
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7636c948-a025-4466-a0cd-36630a7ad6d3

📥 Commits

Reviewing files that changed from the base of the PR and between e5a30ab and b7262a2.

📒 Files selected for processing (1)
  • ui/src/main.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • ui/src/main.ts

📝 Walkthrough

Walkthrough

The desktop host now defines an explicit native menu, exposes its accelerators through Tauri, and supplies them to UI keyboard validation and rendering. The UI detects menu conflicts and mirror drift, while tests and documentation cover the integration.

Changes

Menu chord integration

Layer / File(s) Summary
Native menu definition and installation
crates/b2-desktop/src/menu.rs, crates/b2-desktop/src/main.rs, CLAUDE.md
The desktop app defines explicit menu data, builds the Tauri menu, exports ordered chord rows, and validates menu uniqueness and chord compatibility.
Host chord IPC and startup state
crates/b2-desktop/src/commands.rs, ui/src/types.ts, ui/src/api.ts, ui/src/state.ts, ui/src/main.ts
A synchronous host command returns MenuChord[]; startup fetches the rows, stores them in state, and logs mirror drift.
UI conflict checks and dynamic keyboard sheet
ui/src/menukeys.ts, ui/src/shortcuts.ts, ui/src/render.ts, ui/src/*test.ts, crates/b2-desktop/CLAUDE.md, docs/design/invariants.md
The UI detects menu overlaps, generates menu rows from host data, renders them in settings, and tests menu parsing, conflicts, drift, and display coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant AppBoot
  participant TauriHost
  participant KeyboardState
  participant SettingsPanel
  AppBoot->>TauriHost: invoke menu_chords
  TauriHost-->>KeyboardState: return MenuChord[]
  KeyboardState->>KeyboardState: run menuDrift
  KeyboardState->>SettingsPanel: provide state.menuChords
  SettingsPanel->>SettingsPanel: build shortcuts(state.menuChords)
  SettingsPanel-->>AppBoot: render host menu rows
Loading

Poem

A rabbit hops through chords so bright,
Native menus guide the night.
The host sends keys; the UI knows,
Drift and clashes meet their foes.
The shortcut sheet now blooms—
With carrots near the menu rooms.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: declaring the desktop menu bar and incorporating its chords into the keyboard registry.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/gh-119-22lj0p

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/src/main.ts`:
- Around line 4103-4107: Update loadMenuChords so that after assigning the
fetched chords to state.menuChords, it triggers the existing keyboard-panel
render/update mechanism, ensuring an already-open panel refreshes when the
asynchronous host response arrives; apply the same behavior to the corresponding
path around the additional referenced location without changing the initial boot
flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 818a8cf7-e669-40ea-ba78-672f979296b8

📥 Commits

Reviewing files that changed from the base of the PR and between 90c90a9 and e5a30ab.

📒 Files selected for processing (17)
  • CLAUDE.md
  • crates/b2-desktop/CLAUDE.md
  • crates/b2-desktop/src/commands.rs
  • crates/b2-desktop/src/main.rs
  • crates/b2-desktop/src/menu.rs
  • docs/design/invariants.md
  • ui/src/api.ts
  • ui/src/bindings.ts
  • ui/src/main.ts
  • ui/src/menukeys.test.ts
  • ui/src/menukeys.ts
  • ui/src/render.test.ts
  • ui/src/render.ts
  • ui/src/shortcuts.test.ts
  • ui/src/shortcuts.ts
  • ui/src/state.ts
  • ui/src/types.ts

Comment thread ui/src/main.ts
Review catch (PR #123). `boot` fires `loadMenuChords` without awaiting it,
and `wireEvents` has already bound ⌘, by then — so Settings can be open
before the host answers, and the assignment to `state.menuChords` had no
repaint behind it. The reader would sit looking at menukeys.ts's mirror,
which is the one thing the host list exists to replace.

Guarded on `settingsOpen` rather than unconditional: during boot the
answer normally lands *before* the first `render()` (a static-data IPC
against a vault read and a note list), and painting there would flash the
empty shell ahead of the vault. The guard is false at that point, so the
boot flow is untouched.

Worth noting what the repaint costs when nothing is wrong: nothing. The
mirror and the host agree in the healthy case, so the HTML is identical
and `paintModal`'s memo skips the swap entirely — the only case where the
DOM actually changes is drift, which is the case worth showing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015SJgCnKnUTKNHVsSxrXq7h
@samkeen
samkeen merged commit 98fbb0f into main Jul 28, 2026
2 checks passed
@samkeen
samkeen deleted the claude/gh-119-22lj0p branch July 28, 2026 13:12
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.

2 participants