refactor(commands): share app command manifest

This commit is contained in:
lucaronin
2026-05-02 02:57:45 +02:00
parent 062de2656e
commit 1e5d83d4a3
10 changed files with 1199 additions and 877 deletions

View File

@@ -334,6 +334,12 @@ The renderer uses `viewOrdering` helpers to convert drag or command-palette move
- Plain click / `Enter` open the focused note without replacing the current Neighborhood.
- Cmd/Ctrl-click and Cmd/Ctrl-`Enter` open the note and pivot the note list into that note's Neighborhood.
## Command Surface
`src/shared/appCommandManifest.json` is the cross-runtime source for stable app command IDs, menu structure, display labels, accelerators, deterministic shortcut QA metadata, and native menu enablement groups. The renderer imports it through `src/hooks/appCommandCatalog.ts`, which derives `APP_COMMAND_IDS`, shortcut lookup maps, Linux titlebar menu sections, native-menu command membership, and test helpers. Tauri includes the same JSON in `src-tauri/src/menu.rs` and uses it to build custom menu items, emit overridden menu item IDs such as the quick-open alias as their primary command IDs, and toggle state-dependent menu items from manifest groups.
Domain command builders still own context-sensitive command-palette entries, availability, and execution callbacks. The manifest owns metadata that must stay identical across native menus, renderer shortcuts, deterministic QA bridges, and the Linux fallback menu; OS-native menu items such as Undo, Copy/Paste, Services, Quit, and Window controls remain local to the native menu implementation.
## File System Integration
### Vault Scanning (Rust)

View File

@@ -822,7 +822,7 @@ No Redux or global context. State lives in the root `App.tsx` and custom hooks:
| `useUnifiedSearch` | Query, results, loading state | Keyword search |
| `useSettings` | App settings (telemetry, release channel, theme mode, UI language, auto-sync interval, AutoGit thresholds, default AI agent, Gitignored-content visibility, All Notes file visibility) | Persistent settings |
| `useVaultConfig` | Per-vault UI preferences, AI permission mode | Vault-specific config |
| `appCommandDispatcher` | Canonical shortcut/menu command IDs | Shared execution path for renderer and native menu commands |
| `appCommandDispatcher` | Manifest-backed shortcut/menu command IDs | Shared execution path for renderer and native menu commands |
Data flows unidirectionally: `App` passes data and callbacks as props to child components. No child-to-child communication — everything goes through `App`.
@@ -847,13 +847,14 @@ Selection-dependent actions are wired through the command palette and the native
Shortcut routing is explicit:
- `appCommandCatalog.ts` is the shared shortcut manifest for command IDs, modifier rules, and deterministic QA metadata
- `src/shared/appCommandManifest.json` is the shared command/menu metadata source for command IDs, menu labels, accelerators, native-menu enablement groups, and deterministic QA flags
- `appCommandCatalog.ts` derives renderer command IDs, shortcut lookup maps, Linux menu sections, and QA metadata from that manifest
- `formatShortcutDisplay()` derives platform-accurate visible shortcut labels (`⌘` on macOS, `Ctrl` on Windows/Linux) from that same manifest so menus, tooltips, and command-palette copy stay aligned with real accelerators
- `useAppKeyboard` is the primary execution path for real shortcut keypresses, including Tauri runs
- macOS browser-reserved chords such as `Cmd+O`, `Cmd+F`, and `Cmd+Shift+L` are unblocked at webview init via `tauri-plugin-prevent-default`, then continue through the same renderer-first command path
- `Cmd+Shift+V` uses the same command path for "Paste without Formatting"; `plainTextPaste.ts` reads text through the native clipboard command in Tauri and inserts it through the active rich/raw editor target or the focused browser text control
- `Cmd+F` is surface-aware: editor focus opens current-note find/replace in raw CodeMirror, note-list focus preserves note-list search, and native menu enablement follows focus availability events so only one `Cmd+F` menu item is active
- `menu.rs`, `useMenuEvents`, and Linux's `LinuxMenuButton` emit the same command IDs for native menu clicks, accelerators, and custom titlebar menu actions
- `menu.rs`, `useMenuEvents`, and Linux's `LinuxMenuButton` emit the same manifest-derived command IDs for native menu clicks, accelerators, and custom titlebar menu actions
- `appCommandDispatcher.ts` suppresses the paired native-menu/renderer echo from a single shortcut so the command runs once
- Deterministic QA uses two explicit proof paths from the shared manifest:
- renderer shortcut-event proof through `window.__laputaTest.triggerShortcutCommand()`

View File

@@ -0,0 +1,31 @@
---
type: ADR
id: "0106"
title: "Shared app command manifest"
status: active
date: 2026-05-02
---
## Context
Tolaria command metadata was split across several runtime surfaces: TypeScript owned shortcut lookup and command-palette shortcut display, Rust owned native menu IDs, labels, accelerators, aliases, and enablement groups, and the Linux titlebar fallback menu duplicated another command list. Adding or changing a command required carefully editing multiple files that could drift while still compiling.
The existing renderer-first shortcut model and native-menu dedupe remain correct, but they need a single source for metadata that must be identical across those surfaces.
## Decision
**Tolaria stores cross-runtime app command metadata in `src/shared/appCommandManifest.json`, and both the renderer and Tauri native menu derive their command/menu IDs, accelerators, menu labels, menu aliases, enablement groups, and deterministic QA metadata from it.** Context-sensitive command-palette builders still own availability and execution callbacks, and OS-native menu entries remain local to the native menu implementation.
## Options considered
- **Shared JSON manifest included by TypeScript and Rust** (chosen): works in both runtimes without code generation, keeps menu metadata reviewable, and lets tests validate drift directly.
- **Generate TypeScript and Rust constants from a schema**: gives stronger compile-time types but adds a build step and a generated-file maintenance burden for a small manifest.
- **Keep duplicated constants with more tests**: reduces immediate refactor scope, but still forces every command change through parallel manual edits.
## Consequences
- New app commands that appear in native menus or shortcut QA must be added to `src/shared/appCommandManifest.json`.
- `appCommandCatalog.ts` is responsible for turning the manifest into typed renderer helpers such as `APP_COMMAND_IDS`, shortcut lookup maps, Linux menu sections, and deterministic QA definitions.
- `src-tauri/src/menu.rs` includes the same manifest JSON, builds custom menu items from it, maps overridden menu item IDs such as `file-quick-open-alias` back to their primary command IDs, and resolves state-dependent enablement groups from manifest entries.
- Platform-native menu items such as Undo, Redo, Copy, Paste, Select All, Services, Quit, and Window controls stay in Rust because they are OS affordances, not Tolaria app commands.
- Command-palette builders continue to own dynamic labels, filtering, enabled state, and callbacks where those depend on current app state.

View File

@@ -158,3 +158,4 @@ proposed → active → superseded
| [0103](0103-adapter-specific-ai-permission-semantics.md) | Adapter-specific AI permission semantics | active |
| [0104](0104-tauri-frontend-readiness-watchdog.md) | Tauri frontend readiness watchdog | active |
| [0105](0105-editor-correctness-and-responsiveness-contract.md) | Editor correctness and responsiveness contract | active |
| [0106](0106-shared-app-command-manifest.md) | Shared app command manifest | active |

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
import { invoke } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window'
import type { AppCommandId } from '../hooks/appCommandCatalog'
import { APP_COMMAND_DEFINITIONS, APP_COMMAND_IDS } from '../hooks/appCommandCatalog'
import { APP_COMMAND_MENU_SECTIONS } from '../hooks/appCommandCatalog'
import { Button } from './ui/button'
import {
DropdownMenu,
@@ -17,7 +16,13 @@ import {
type MenuItem =
| { kind: 'separator' }
| { kind: 'command'; commandId: MenuCommandId; label: string }
| {
kind: 'command'
commandId: string
label: string
menuItemId: string
shortcut?: string
}
| { kind: 'action'; action: () => void; label: string; shortcut?: string }
type MenuSection = {
@@ -25,91 +30,8 @@ type MenuSection = {
label: string
}
type MenuCommandId = AppCommandId | 'edit-toggle-note-list-search'
const MENU_SECTIONS: ReadonlyArray<MenuSection> = [
{
label: 'File',
items: [
{ kind: 'command', label: 'New Note', commandId: APP_COMMAND_IDS.fileNewNote },
{ kind: 'command', label: 'New Type', commandId: APP_COMMAND_IDS.fileNewType },
{ kind: 'command', label: 'Quick Open', commandId: APP_COMMAND_IDS.fileQuickOpen },
{ kind: 'separator' },
{ kind: 'command', label: 'Save', commandId: APP_COMMAND_IDS.fileSave },
],
},
{
label: 'Edit',
items: [
{ kind: 'command', label: 'Find in Note', commandId: APP_COMMAND_IDS.editFindInNote },
{ kind: 'command', label: 'Replace in Note', commandId: APP_COMMAND_IDS.editReplaceInNote },
{ kind: 'command', label: 'Find in Vault', commandId: APP_COMMAND_IDS.editFindInVault },
{ kind: 'command', label: 'Paste without Formatting', commandId: APP_COMMAND_IDS.editPastePlainText },
{ kind: 'command', label: 'Toggle Note List Search', commandId: 'edit-toggle-note-list-search' },
{ kind: 'command', label: 'Toggle Diff Mode', commandId: APP_COMMAND_IDS.editToggleDiff },
],
},
{
label: 'View',
items: [
{ kind: 'command', label: 'Editor Only', commandId: APP_COMMAND_IDS.viewEditorOnly },
{ kind: 'command', label: 'Editor + Notes', commandId: APP_COMMAND_IDS.viewEditorList },
{ kind: 'command', label: 'All Panels', commandId: APP_COMMAND_IDS.viewAll },
{ kind: 'separator' },
{ kind: 'command', label: 'Toggle Properties Panel', commandId: APP_COMMAND_IDS.viewToggleProperties },
{ kind: 'separator' },
{ kind: 'command', label: 'Zoom In', commandId: APP_COMMAND_IDS.viewZoomIn },
{ kind: 'command', label: 'Zoom Out', commandId: APP_COMMAND_IDS.viewZoomOut },
{ kind: 'command', label: 'Actual Size', commandId: APP_COMMAND_IDS.viewZoomReset },
{ kind: 'separator' },
{ kind: 'command', label: 'Command Palette', commandId: APP_COMMAND_IDS.viewCommandPalette },
],
},
{
label: 'Go',
items: [
{ kind: 'command', label: 'All Notes', commandId: APP_COMMAND_IDS.goAllNotes },
{ kind: 'command', label: 'Archived', commandId: APP_COMMAND_IDS.goArchived },
{ kind: 'command', label: 'Changes', commandId: APP_COMMAND_IDS.goChanges },
{ kind: 'command', label: 'Inbox', commandId: APP_COMMAND_IDS.goInbox },
{ kind: 'separator' },
{ kind: 'command', label: 'Go Back', commandId: APP_COMMAND_IDS.viewGoBack },
{ kind: 'command', label: 'Go Forward', commandId: APP_COMMAND_IDS.viewGoForward },
],
},
{
label: 'Note',
items: [
{ kind: 'command', label: 'Toggle Organized', commandId: APP_COMMAND_IDS.noteToggleOrganized },
{ kind: 'command', label: 'Archive Note', commandId: APP_COMMAND_IDS.noteArchive },
{ kind: 'command', label: 'Delete Note', commandId: APP_COMMAND_IDS.noteDelete },
{ kind: 'command', label: 'Restore Deleted Note', commandId: APP_COMMAND_IDS.noteRestoreDeleted },
{ kind: 'separator' },
{ kind: 'command', label: 'Open in New Window', commandId: APP_COMMAND_IDS.noteOpenInNewWindow },
{ kind: 'separator' },
{ kind: 'command', label: 'Toggle Raw Editor', commandId: APP_COMMAND_IDS.editToggleRawEditor },
{ kind: 'command', label: 'Toggle AI Panel', commandId: APP_COMMAND_IDS.viewToggleAiChat },
{ kind: 'command', label: 'Toggle Backlinks', commandId: APP_COMMAND_IDS.viewToggleBacklinks },
],
},
{
label: 'Vault',
items: [
{ kind: 'command', label: 'Open Vault…', commandId: APP_COMMAND_IDS.vaultOpen },
{ kind: 'command', label: 'Remove Vault from List', commandId: APP_COMMAND_IDS.vaultRemove },
{ kind: 'command', label: 'Restore Getting Started', commandId: APP_COMMAND_IDS.vaultRestoreGettingStarted },
{ kind: 'separator' },
{ kind: 'command', label: 'Add Remote…', commandId: APP_COMMAND_IDS.vaultAddRemote },
{ kind: 'command', label: 'Commit & Push', commandId: APP_COMMAND_IDS.vaultCommitPush },
{ kind: 'command', label: 'Pull from Remote', commandId: APP_COMMAND_IDS.vaultPull },
{ kind: 'command', label: 'Resolve Conflicts', commandId: APP_COMMAND_IDS.vaultResolveConflicts },
{ kind: 'command', label: 'View Pending Changes', commandId: APP_COMMAND_IDS.vaultViewChanges },
{ kind: 'separator' },
{ kind: 'command', label: 'Reload Vault', commandId: APP_COMMAND_IDS.vaultReload },
{ kind: 'command', label: 'Repair Vault', commandId: APP_COMMAND_IDS.vaultRepair },
{ kind: 'command', label: 'Set Up External AI Tools…', commandId: APP_COMMAND_IDS.vaultInstallMcp },
],
},
...APP_COMMAND_MENU_SECTIONS,
{
label: 'Window',
items: [
@@ -121,31 +43,8 @@ const MENU_SECTIONS: ReadonlyArray<MenuSection> = [
},
]
function formatShortcutKey(key: string): string {
switch (key) {
case 'ArrowLeft':
return '←'
case 'ArrowRight':
return '→'
default:
return key.length === 1 ? key.toUpperCase() : key
}
}
function getLinuxShortcut(commandId: MenuCommandId): string | null {
if (commandId === 'edit-toggle-note-list-search') {
return 'Ctrl+F'
}
const shortcut = APP_COMMAND_DEFINITIONS[commandId].shortcut
if (!shortcut) return null
const modifier = shortcut.combo === 'command-or-ctrl' ? 'Ctrl' : 'Ctrl+Shift'
return `${modifier}+${formatShortcutKey(shortcut.key)}`
}
function triggerMenuCommand(commandId: MenuCommandId): void {
void invoke('trigger_menu_command', { id: commandId }).catch(() => {})
function triggerMenuCommand(menuItemId: string): void {
void invoke('trigger_menu_command', { id: menuItemId }).catch(() => {})
}
function HamburgerIcon() {
@@ -184,15 +83,14 @@ export function LinuxMenuButton() {
}
if (item.kind === 'command') {
const shortcut = getLinuxShortcut(item.commandId)
return (
<DropdownMenuItem
key={item.commandId}
onSelect={() => triggerMenuCommand(item.commandId)}
key={item.menuItemId}
onSelect={() => triggerMenuCommand(item.menuItemId)}
>
<span>{item.label}</span>
{shortcut && (
<DropdownMenuShortcut>{shortcut}</DropdownMenuShortcut>
{item.shortcut && (
<DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut>
)}
</DropdownMenuItem>
)

View File

@@ -24,6 +24,7 @@ const {
}))
vi.mock('../utils/platform', () => ({
isMac: () => false,
shouldUseLinuxWindowChrome: vi.fn(),
}))

View File

@@ -1,56 +1,11 @@
import appCommandManifest from '../shared/appCommandManifest.json' with { type: 'json' }
import type { SidebarFilter } from '../types'
import { isMac } from '../utils/platform'
import type { ViewMode } from './useViewMode'
export const APP_COMMAND_IDS = {
appSettings: 'app-settings',
appCheckForUpdates: 'app-check-for-updates',
fileNewNote: 'file-new-note',
fileNewType: 'file-new-type',
fileQuickOpen: 'file-quick-open',
fileSave: 'file-save',
editFindInNote: 'edit-find-in-note',
editReplaceInNote: 'edit-replace-in-note',
editFindInVault: 'edit-find-in-vault',
editPastePlainText: 'edit-paste-plain-text',
editToggleRawEditor: 'edit-toggle-raw-editor',
editToggleDiff: 'edit-toggle-diff',
viewEditorOnly: 'view-editor-only',
viewEditorList: 'view-editor-list',
viewAll: 'view-all',
viewToggleProperties: 'view-toggle-properties',
viewToggleAiChat: 'view-toggle-ai-chat',
viewToggleBacklinks: 'view-toggle-backlinks',
viewCommandPalette: 'view-command-palette',
viewZoomIn: 'view-zoom-in',
viewZoomOut: 'view-zoom-out',
viewZoomReset: 'view-zoom-reset',
viewGoBack: 'view-go-back',
viewGoForward: 'view-go-forward',
goAllNotes: 'go-all-notes',
goArchived: 'go-archived',
goChanges: 'go-changes',
goInbox: 'go-inbox',
noteToggleOrganized: 'note-toggle-organized',
noteToggleFavorite: 'note-toggle-favorite',
noteArchive: 'note-archive',
noteDelete: 'note-delete',
noteOpenInNewWindow: 'note-open-in-new-window',
noteRestoreDeleted: 'note-restore-deleted',
vaultOpen: 'vault-open',
vaultRemove: 'vault-remove',
vaultRestoreGettingStarted: 'vault-restore-getting-started',
vaultAddRemote: 'vault-add-remote',
vaultCommitPush: 'vault-commit-push',
vaultPull: 'vault-pull',
vaultResolveConflicts: 'vault-resolve-conflicts',
vaultViewChanges: 'vault-view-changes',
vaultInstallMcp: 'vault-install-mcp',
vaultReload: 'vault-reload',
vaultRepair: 'vault-repair',
} as const
type AppCommandKey = keyof typeof appCommandManifest.commands
type ShortcutEventLike = Pick<KeyboardEvent, 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'key' | 'code'>
export type AppCommandId = (typeof APP_COMMAND_IDS)[keyof typeof APP_COMMAND_IDS]
export type AppCommandShortcutCombo =
| 'command-or-ctrl'
| 'command-or-ctrl-shift'
@@ -58,7 +13,6 @@ export type AppCommandShortcutCombo =
export type AppCommandDeterministicQaMode =
| 'renderer-shortcut-event'
| 'native-menu-command'
type ShortcutEventLike = Pick<KeyboardEvent, 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'key' | 'code'>
export interface AppCommandDeterministicQaDefinition {
preferredMode: AppCommandDeterministicQaMode
@@ -131,6 +85,19 @@ interface AppCommandShortcutDefinition {
display: string
}
interface AppCommandManifestShortcutDefinition extends AppCommandShortcutDefinition {
accelerator: string
requiresManualNativeAcceleratorQa?: boolean
}
interface AppCommandManifestDefinition {
id: string
route: AppCommandRoute
menuOwned: boolean
shortcut?: AppCommandManifestShortcutDefinition
preferredShortcutQaMode?: AppCommandDeterministicQaMode
}
export interface AppCommandDefinition {
route: AppCommandRoute
menuOwned: boolean
@@ -138,241 +105,183 @@ export interface AppCommandDefinition {
preferredShortcutQaMode?: AppCommandDeterministicQaMode
}
export const APP_COMMAND_DEFINITIONS: Record<AppCommandId, AppCommandDefinition> = {
[APP_COMMAND_IDS.appSettings]: {
route: { kind: 'handler', handler: 'onOpenSettings' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: ',', display: '⌘,' },
},
[APP_COMMAND_IDS.appCheckForUpdates]: {
route: { kind: 'handler', handler: 'onCheckForUpdates' },
menuOwned: true,
},
[APP_COMMAND_IDS.fileNewNote]: {
route: { kind: 'handler', handler: 'onCreateNote' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: 'n', code: 'KeyN', display: '⌘N' },
},
[APP_COMMAND_IDS.fileNewType]: {
route: { kind: 'handler', handler: 'onCreateType' },
menuOwned: true,
},
[APP_COMMAND_IDS.fileQuickOpen]: {
route: { kind: 'handler', handler: 'onQuickOpen' },
menuOwned: true,
shortcut: {
combo: 'command-or-ctrl',
key: 'p',
aliases: ['o'],
code: 'KeyP',
display: '⌘P / ⌘O',
},
},
[APP_COMMAND_IDS.fileSave]: {
route: { kind: 'handler', handler: 'onSave' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: 's', code: 'KeyS', display: '⌘S' },
},
[APP_COMMAND_IDS.editFindInNote]: {
route: { kind: 'handler', handler: 'onFindInNote' },
menuOwned: true,
preferredShortcutQaMode: 'renderer-shortcut-event',
shortcut: { combo: 'command-or-ctrl', key: 'f', code: 'KeyF', display: '⌘F' },
},
[APP_COMMAND_IDS.editReplaceInNote]: {
route: { kind: 'handler', handler: 'onReplaceInNote' },
menuOwned: true,
},
[APP_COMMAND_IDS.editFindInVault]: {
route: { kind: 'handler', handler: 'onSearch' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl-shift', key: 'f', code: 'KeyF', display: '⌘⇧F' },
},
[APP_COMMAND_IDS.editPastePlainText]: {
route: { kind: 'handler', handler: 'onPastePlainText' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl-shift', key: 'v', code: 'KeyV', display: '⌘⇧V' },
},
[APP_COMMAND_IDS.editToggleRawEditor]: {
route: { kind: 'handler', handler: 'onToggleRawEditor' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: '\\', display: '⌘\\' },
},
[APP_COMMAND_IDS.editToggleDiff]: {
route: { kind: 'handler', handler: 'onToggleDiff' },
menuOwned: true,
},
[APP_COMMAND_IDS.viewEditorOnly]: {
route: { kind: 'view-mode', value: 'editor-only' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: '1', display: '⌘1' },
},
[APP_COMMAND_IDS.viewEditorList]: {
route: { kind: 'view-mode', value: 'editor-list' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: '2', display: '⌘2' },
},
[APP_COMMAND_IDS.viewAll]: {
route: { kind: 'view-mode', value: 'all' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: '3', display: '⌘3' },
},
[APP_COMMAND_IDS.viewToggleProperties]: {
route: { kind: 'handler', handler: 'onToggleInspector' },
menuOwned: true,
preferredShortcutQaMode: 'renderer-shortcut-event',
shortcut: { combo: 'command-or-ctrl-shift', key: 'i', code: 'KeyI', display: '⌘⇧I' },
},
[APP_COMMAND_IDS.viewToggleAiChat]: {
route: { kind: 'handler', handler: 'onToggleAIChat' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl-shift', key: 'l', code: 'KeyL', display: '⌘⇧L' },
},
[APP_COMMAND_IDS.viewToggleBacklinks]: {
route: { kind: 'handler', handler: 'onToggleInspector' },
menuOwned: true,
},
[APP_COMMAND_IDS.viewCommandPalette]: {
route: { kind: 'handler', handler: 'onCommandPalette' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: 'k', code: 'KeyK', display: '⌘K' },
},
[APP_COMMAND_IDS.viewZoomIn]: {
route: { kind: 'handler', handler: 'onZoomIn' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: '=', aliases: ['+'], display: '⌘=' },
},
[APP_COMMAND_IDS.viewZoomOut]: {
route: { kind: 'handler', handler: 'onZoomOut' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: '-', display: '⌘-' },
},
[APP_COMMAND_IDS.viewZoomReset]: {
route: { kind: 'handler', handler: 'onZoomReset' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: '0', display: '⌘0' },
},
[APP_COMMAND_IDS.viewGoBack]: {
route: { kind: 'handler', handler: 'onGoBack' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: 'ArrowLeft', code: 'ArrowLeft', display: '⌘←' },
},
[APP_COMMAND_IDS.viewGoForward]: {
route: { kind: 'handler', handler: 'onGoForward' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: 'ArrowRight', code: 'ArrowRight', display: '⌘→' },
},
[APP_COMMAND_IDS.goAllNotes]: {
route: { kind: 'filter', value: 'all' },
menuOwned: true,
},
[APP_COMMAND_IDS.goArchived]: {
route: { kind: 'filter', value: 'archived' },
menuOwned: true,
},
[APP_COMMAND_IDS.goChanges]: {
route: { kind: 'filter', value: 'changes' },
menuOwned: true,
},
[APP_COMMAND_IDS.goInbox]: {
route: { kind: 'filter', value: 'inbox' },
menuOwned: true,
},
[APP_COMMAND_IDS.noteToggleOrganized]: {
route: { kind: 'active-tab-handler', handler: 'onToggleOrganized' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: 'e', code: 'KeyE', display: '⌘E' },
},
[APP_COMMAND_IDS.noteToggleFavorite]: {
route: { kind: 'active-tab-handler', handler: 'onToggleFavorite' },
menuOwned: false,
shortcut: { combo: 'command-or-ctrl', key: 'd', code: 'KeyD', display: '⌘D' },
},
[APP_COMMAND_IDS.noteArchive]: {
route: { kind: 'active-tab-handler', handler: 'onArchiveNote' },
menuOwned: true,
},
[APP_COMMAND_IDS.noteDelete]: {
route: { kind: 'active-tab-handler', handler: 'onDeleteNote' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl', key: 'Backspace', aliases: ['Delete'], display: '⌘⌫' },
},
[APP_COMMAND_IDS.noteOpenInNewWindow]: {
route: { kind: 'handler', handler: 'onOpenInNewWindow' },
menuOwned: true,
shortcut: { combo: 'command-or-ctrl-shift', key: 'o', code: 'KeyO', display: '⌘⇧O' },
},
[APP_COMMAND_IDS.noteRestoreDeleted]: {
route: { kind: 'handler', handler: 'onRestoreDeletedNote' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultOpen]: {
route: { kind: 'handler', handler: 'onOpenVault' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultRemove]: {
route: { kind: 'handler', handler: 'onRemoveActiveVault' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultRestoreGettingStarted]: {
route: { kind: 'handler', handler: 'onRestoreGettingStarted' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultAddRemote]: {
route: { kind: 'handler', handler: 'onAddRemote' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultCommitPush]: {
route: { kind: 'handler', handler: 'onCommitPush' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultPull]: {
route: { kind: 'handler', handler: 'onPull' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultResolveConflicts]: {
route: { kind: 'handler', handler: 'onResolveConflicts' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultViewChanges]: {
route: { kind: 'handler', handler: 'onViewChanges' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultInstallMcp]: {
route: { kind: 'handler', handler: 'onInstallMcp' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultReload]: {
route: { kind: 'handler', handler: 'onReloadVault' },
menuOwned: true,
},
[APP_COMMAND_IDS.vaultRepair]: {
route: { kind: 'handler', handler: 'onRepairVault' },
menuOwned: true,
},
type PlatformLabel = string | {
macos?: string
windows?: string
linux?: string
default: string
}
type AppCommandMenuManifestItem =
| { kind: 'separator' }
| {
kind: 'command'
command: AppCommandKey
id?: string
label: PlatformLabel
accelerator?: string | null
enabled?: boolean
}
| {
kind: 'menu-event'
id: string
label: PlatformLabel
accelerator?: string | null
enabled?: boolean
}
interface AppCommandMenuManifestSection {
label: string
items: AppCommandMenuManifestItem[]
}
export type AppCommandMenuItem =
| { kind: 'separator' }
| {
kind: 'command'
commandId: string
menuItemId: string
label: string
shortcut?: string
enabled?: boolean
}
type AppCommandMenuStateGroupReference =
| { command: AppCommandKey }
| { id: string }
type AppCommandMenuStateGroupName = keyof typeof appCommandManifest.menuStateGroups
const APP_COMMAND_MANIFEST_COMMANDS = appCommandManifest.commands as Record<AppCommandKey, AppCommandManifestDefinition>
const APP_COMMAND_MANIFEST_MENUS = appCommandManifest.menus as AppCommandMenuManifestSection[]
const APP_COMMAND_MANIFEST_APP_MENU = appCommandManifest.appMenu as AppCommandMenuManifestItem[]
const APP_COMMAND_MANIFEST_STATE_GROUPS = appCommandManifest.menuStateGroups as Record<
AppCommandMenuStateGroupName,
AppCommandMenuStateGroupReference[]
>
export const APP_COMMAND_IDS = Object.fromEntries(
Object.entries(APP_COMMAND_MANIFEST_COMMANDS).map(([key, command]) => [key, command.id]),
) as { readonly [K in AppCommandKey]: string }
export type AppCommandId = (typeof APP_COMMAND_IDS)[AppCommandKey]
const APP_COMMAND_SET = new Set<string>(Object.values(APP_COMMAND_IDS))
function toShortcutDefinition(
shortcut: AppCommandManifestShortcutDefinition | undefined,
): AppCommandShortcutDefinition | undefined {
if (!shortcut) return undefined
return {
combo: shortcut.combo,
key: shortcut.key,
aliases: shortcut.aliases,
code: shortcut.code,
display: shortcut.display,
}
}
export const APP_COMMAND_DEFINITIONS = Object.fromEntries(
Object.values(APP_COMMAND_MANIFEST_COMMANDS).map((command) => [
command.id,
{
route: command.route,
menuOwned: command.menuOwned,
shortcut: toShortcutDefinition(command.shortcut),
preferredShortcutQaMode: command.preferredShortcutQaMode,
},
]),
) as Record<AppCommandId, AppCommandDefinition>
function resolvePlatformLabel(label: PlatformLabel): string {
if (typeof label === 'string') return label
if (isMac() && label.macos) return label.macos
return label.default
}
function formatAcceleratorDisplay(accelerator: string): string {
const commandPrefix = isMac() ? '⌘' : 'Ctrl+'
const commandShiftPrefix = isMac() ? '⌘⇧' : 'Ctrl+Shift+'
return accelerator
.replaceAll('CmdOrCtrl+Shift+', commandShiftPrefix)
.replaceAll('CmdOrCtrl+', commandPrefix)
.replaceAll('Backspace', isMac() ? '⌫' : 'Backspace')
.replaceAll('Delete', isMac() ? '⌦' : 'Delete')
.replaceAll('Left', isMac() ? '←' : 'Left')
.replaceAll('Right', isMac() ? '→' : 'Right')
.replaceAll('Enter', isMac() ? '↵' : 'Enter')
}
function menuShortcutForCommand(
item: Extract<AppCommandMenuManifestItem, { kind: 'command' }>,
command: AppCommandManifestDefinition,
): string | undefined {
if (typeof item.accelerator === 'string') return formatAcceleratorDisplay(item.accelerator)
if (command.shortcut) return formatShortcutDisplay(command.shortcut)
return undefined
}
function toMenuItem(item: AppCommandMenuManifestItem): AppCommandMenuItem {
if (item.kind === 'separator') return { kind: 'separator' }
if (item.kind === 'menu-event') {
return {
kind: 'command',
commandId: item.id,
menuItemId: item.id,
label: resolvePlatformLabel(item.label),
shortcut: typeof item.accelerator === 'string'
? formatAcceleratorDisplay(item.accelerator)
: undefined,
enabled: item.enabled,
}
}
const command = APP_COMMAND_MANIFEST_COMMANDS[item.command]
return {
kind: 'command',
commandId: command.id,
menuItemId: item.id ?? command.id,
label: resolvePlatformLabel(item.label),
shortcut: menuShortcutForCommand(item, command),
enabled: item.enabled,
}
}
function menuCommandIds(items: AppCommandMenuItem[]): string[] {
return items.flatMap(item => item.kind === 'command' ? [item.commandId] : [])
}
export const APP_COMMAND_MENU_SECTIONS = APP_COMMAND_MANIFEST_MENUS.map(section => ({
label: section.label,
items: section.items.map(toMenuItem),
}))
const APP_COMMAND_APP_MENU_ITEMS = APP_COMMAND_MANIFEST_APP_MENU.map(toMenuItem)
export const APP_COMMAND_MENU_STATE_GROUPS = Object.fromEntries(
Object.entries(APP_COMMAND_MANIFEST_STATE_GROUPS).map(([name, references]) => [
name,
references.map(reference => 'command' in reference
? APP_COMMAND_MANIFEST_COMMANDS[reference.command].id
: reference.id),
]),
) as Record<AppCommandMenuStateGroupName, string[]>
const NATIVE_MENU_COMMAND_SET = new Set<string>(
(Object.entries(APP_COMMAND_DEFINITIONS) as Array<[AppCommandId, AppCommandDefinition]>)
.filter(([, definition]) => definition.menuOwned)
.map(([id]) => id),
[
...APP_COMMAND_MENU_SECTIONS.flatMap(section => menuCommandIds(section.items)),
...menuCommandIds(APP_COMMAND_APP_MENU_ITEMS),
].filter(id => APP_COMMAND_SET.has(id)),
)
const MANUAL_NATIVE_ACCELERATOR_QA_COMMAND_SET = new Set<AppCommandId>([
APP_COMMAND_IDS.appSettings,
APP_COMMAND_IDS.fileNewNote,
APP_COMMAND_IDS.fileQuickOpen,
APP_COMMAND_IDS.fileSave,
APP_COMMAND_IDS.editFindInNote,
APP_COMMAND_IDS.editFindInVault,
APP_COMMAND_IDS.editPastePlainText,
APP_COMMAND_IDS.viewToggleAiChat,
APP_COMMAND_IDS.viewCommandPalette,
APP_COMMAND_IDS.noteToggleOrganized,
APP_COMMAND_IDS.noteToggleFavorite,
])
const MANUAL_NATIVE_ACCELERATOR_QA_COMMAND_SET = new Set<AppCommandId>(
Object.values(APP_COMMAND_MANIFEST_COMMANDS)
.filter(command => command.shortcut?.requiresManualNativeAcceleratorQa)
.map(command => command.id),
)
const shortcutKeyMaps = {
'command-or-ctrl': new Map<string, AppCommandId>(),

View File

@@ -13,6 +13,8 @@ import {
} from './appCommandDispatcher'
import {
getDeterministicShortcutQaDefinition,
APP_COMMAND_MENU_SECTIONS,
APP_COMMAND_MENU_STATE_GROUPS,
getShortcutEventInit,
} from './appCommandCatalog'
@@ -103,6 +105,31 @@ describe('appCommandDispatcher', () => {
expect(isNativeMenuCommandId(APP_COMMAND_IDS.noteToggleFavorite)).toBe(false)
})
it('derives native menu command IDs from the shared command menu manifest', () => {
const menuCommandIds = APP_COMMAND_MENU_SECTIONS.flatMap(section =>
section.items.flatMap(item => item.kind === 'command' ? [item.commandId] : []),
)
expect(menuCommandIds).toContain(APP_COMMAND_IDS.fileNewNote)
expect(menuCommandIds).toContain(APP_COMMAND_IDS.editPastePlainText)
expect(menuCommandIds).toContain(APP_COMMAND_IDS.viewGoBack)
expect(menuCommandIds).not.toContain(APP_COMMAND_IDS.noteToggleFavorite)
})
it('keeps native menu state groups inside the shared command menu manifest', () => {
const menuCommandIds = new Set(
APP_COMMAND_MENU_SECTIONS.flatMap(section =>
section.items.flatMap(item => item.kind === 'command' ? [item.menuItemId] : []),
),
)
for (const commandIds of Object.values(APP_COMMAND_MENU_STATE_GROUPS)) {
for (const commandId of commandIds) {
expect(menuCommandIds.has(commandId)).toBe(true)
}
}
})
it('finds raw editor, AI, and plain-text paste shortcuts from the shared catalog', () => {
expect(findShortcutCommandId('command-or-ctrl', 'o', 'KeyO')).toBe(APP_COMMAND_IDS.fileQuickOpen)
expect(findShortcutCommandId('command-or-ctrl', '\\')).toBe(APP_COMMAND_IDS.editToggleRawEditor)

View File

@@ -0,0 +1,536 @@
{
"commands": {
"appSettings": {
"id": "app-settings",
"route": { "kind": "handler", "handler": "onOpenSettings" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": ",",
"display": "⌘,",
"accelerator": "CmdOrCtrl+,",
"requiresManualNativeAcceleratorQa": true
}
},
"appCheckForUpdates": {
"id": "app-check-for-updates",
"route": { "kind": "handler", "handler": "onCheckForUpdates" },
"menuOwned": true
},
"fileNewNote": {
"id": "file-new-note",
"route": { "kind": "handler", "handler": "onCreateNote" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "n",
"code": "KeyN",
"display": "⌘N",
"accelerator": "CmdOrCtrl+N",
"requiresManualNativeAcceleratorQa": true
}
},
"fileNewType": {
"id": "file-new-type",
"route": { "kind": "handler", "handler": "onCreateType" },
"menuOwned": true
},
"fileQuickOpen": {
"id": "file-quick-open",
"route": { "kind": "handler", "handler": "onQuickOpen" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "p",
"aliases": ["o"],
"code": "KeyP",
"display": "⌘P / ⌘O",
"accelerator": "CmdOrCtrl+P",
"requiresManualNativeAcceleratorQa": true
}
},
"fileSave": {
"id": "file-save",
"route": { "kind": "handler", "handler": "onSave" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "s",
"code": "KeyS",
"display": "⌘S",
"accelerator": "CmdOrCtrl+S",
"requiresManualNativeAcceleratorQa": true
}
},
"editFindInNote": {
"id": "edit-find-in-note",
"route": { "kind": "handler", "handler": "onFindInNote" },
"menuOwned": true,
"preferredShortcutQaMode": "renderer-shortcut-event",
"shortcut": {
"combo": "command-or-ctrl",
"key": "f",
"code": "KeyF",
"display": "⌘F",
"accelerator": "CmdOrCtrl+F",
"requiresManualNativeAcceleratorQa": true
}
},
"editReplaceInNote": {
"id": "edit-replace-in-note",
"route": { "kind": "handler", "handler": "onReplaceInNote" },
"menuOwned": true
},
"editFindInVault": {
"id": "edit-find-in-vault",
"route": { "kind": "handler", "handler": "onSearch" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl-shift",
"key": "f",
"code": "KeyF",
"display": "⌘⇧F",
"accelerator": "CmdOrCtrl+Shift+F",
"requiresManualNativeAcceleratorQa": true
}
},
"editPastePlainText": {
"id": "edit-paste-plain-text",
"route": { "kind": "handler", "handler": "onPastePlainText" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl-shift",
"key": "v",
"code": "KeyV",
"display": "⌘⇧V",
"accelerator": "CmdOrCtrl+Shift+V",
"requiresManualNativeAcceleratorQa": true
}
},
"editToggleRawEditor": {
"id": "edit-toggle-raw-editor",
"route": { "kind": "handler", "handler": "onToggleRawEditor" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "\\",
"display": "⌘\\",
"accelerator": "CmdOrCtrl+\\"
}
},
"editToggleDiff": {
"id": "edit-toggle-diff",
"route": { "kind": "handler", "handler": "onToggleDiff" },
"menuOwned": true
},
"viewEditorOnly": {
"id": "view-editor-only",
"route": { "kind": "view-mode", "value": "editor-only" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "1",
"display": "⌘1",
"accelerator": "CmdOrCtrl+1"
}
},
"viewEditorList": {
"id": "view-editor-list",
"route": { "kind": "view-mode", "value": "editor-list" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "2",
"display": "⌘2",
"accelerator": "CmdOrCtrl+2"
}
},
"viewAll": {
"id": "view-all",
"route": { "kind": "view-mode", "value": "all" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "3",
"display": "⌘3",
"accelerator": "CmdOrCtrl+3"
}
},
"viewToggleProperties": {
"id": "view-toggle-properties",
"route": { "kind": "handler", "handler": "onToggleInspector" },
"menuOwned": true,
"preferredShortcutQaMode": "renderer-shortcut-event",
"shortcut": {
"combo": "command-or-ctrl-shift",
"key": "i",
"code": "KeyI",
"display": "⌘⇧I",
"accelerator": "CmdOrCtrl+Shift+I"
}
},
"viewToggleAiChat": {
"id": "view-toggle-ai-chat",
"route": { "kind": "handler", "handler": "onToggleAIChat" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl-shift",
"key": "l",
"code": "KeyL",
"display": "⌘⇧L",
"accelerator": "CmdOrCtrl+Shift+L",
"requiresManualNativeAcceleratorQa": true
}
},
"viewToggleBacklinks": {
"id": "view-toggle-backlinks",
"route": { "kind": "handler", "handler": "onToggleInspector" },
"menuOwned": true
},
"viewCommandPalette": {
"id": "view-command-palette",
"route": { "kind": "handler", "handler": "onCommandPalette" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "k",
"code": "KeyK",
"display": "⌘K",
"accelerator": "CmdOrCtrl+K",
"requiresManualNativeAcceleratorQa": true
}
},
"viewZoomIn": {
"id": "view-zoom-in",
"route": { "kind": "handler", "handler": "onZoomIn" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "=",
"aliases": ["+"],
"display": "⌘=",
"accelerator": "CmdOrCtrl+="
}
},
"viewZoomOut": {
"id": "view-zoom-out",
"route": { "kind": "handler", "handler": "onZoomOut" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "-",
"display": "⌘-",
"accelerator": "CmdOrCtrl+-"
}
},
"viewZoomReset": {
"id": "view-zoom-reset",
"route": { "kind": "handler", "handler": "onZoomReset" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "0",
"display": "⌘0",
"accelerator": "CmdOrCtrl+0"
}
},
"viewGoBack": {
"id": "view-go-back",
"route": { "kind": "handler", "handler": "onGoBack" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "ArrowLeft",
"code": "ArrowLeft",
"display": "⌘←",
"accelerator": "CmdOrCtrl+Left"
}
},
"viewGoForward": {
"id": "view-go-forward",
"route": { "kind": "handler", "handler": "onGoForward" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "ArrowRight",
"code": "ArrowRight",
"display": "⌘→",
"accelerator": "CmdOrCtrl+Right"
}
},
"goAllNotes": {
"id": "go-all-notes",
"route": { "kind": "filter", "value": "all" },
"menuOwned": true
},
"goArchived": {
"id": "go-archived",
"route": { "kind": "filter", "value": "archived" },
"menuOwned": true
},
"goChanges": {
"id": "go-changes",
"route": { "kind": "filter", "value": "changes" },
"menuOwned": true
},
"goInbox": {
"id": "go-inbox",
"route": { "kind": "filter", "value": "inbox" },
"menuOwned": true
},
"noteToggleOrganized": {
"id": "note-toggle-organized",
"route": { "kind": "active-tab-handler", "handler": "onToggleOrganized" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "e",
"code": "KeyE",
"display": "⌘E",
"accelerator": "CmdOrCtrl+E",
"requiresManualNativeAcceleratorQa": true
}
},
"noteToggleFavorite": {
"id": "note-toggle-favorite",
"route": { "kind": "active-tab-handler", "handler": "onToggleFavorite" },
"menuOwned": false,
"shortcut": {
"combo": "command-or-ctrl",
"key": "d",
"code": "KeyD",
"display": "⌘D",
"accelerator": "CmdOrCtrl+D",
"requiresManualNativeAcceleratorQa": true
}
},
"noteArchive": {
"id": "note-archive",
"route": { "kind": "active-tab-handler", "handler": "onArchiveNote" },
"menuOwned": true
},
"noteDelete": {
"id": "note-delete",
"route": { "kind": "active-tab-handler", "handler": "onDeleteNote" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl",
"key": "Backspace",
"aliases": ["Delete"],
"display": "⌘⌫",
"accelerator": "CmdOrCtrl+Backspace"
}
},
"noteOpenInNewWindow": {
"id": "note-open-in-new-window",
"route": { "kind": "handler", "handler": "onOpenInNewWindow" },
"menuOwned": true,
"shortcut": {
"combo": "command-or-ctrl-shift",
"key": "o",
"code": "KeyO",
"display": "⌘⇧O",
"accelerator": "CmdOrCtrl+Shift+O"
}
},
"noteRestoreDeleted": {
"id": "note-restore-deleted",
"route": { "kind": "handler", "handler": "onRestoreDeletedNote" },
"menuOwned": true
},
"vaultOpen": {
"id": "vault-open",
"route": { "kind": "handler", "handler": "onOpenVault" },
"menuOwned": true
},
"vaultRemove": {
"id": "vault-remove",
"route": { "kind": "handler", "handler": "onRemoveActiveVault" },
"menuOwned": true
},
"vaultRestoreGettingStarted": {
"id": "vault-restore-getting-started",
"route": { "kind": "handler", "handler": "onRestoreGettingStarted" },
"menuOwned": true
},
"vaultAddRemote": {
"id": "vault-add-remote",
"route": { "kind": "handler", "handler": "onAddRemote" },
"menuOwned": true
},
"vaultCommitPush": {
"id": "vault-commit-push",
"route": { "kind": "handler", "handler": "onCommitPush" },
"menuOwned": true
},
"vaultPull": {
"id": "vault-pull",
"route": { "kind": "handler", "handler": "onPull" },
"menuOwned": true
},
"vaultResolveConflicts": {
"id": "vault-resolve-conflicts",
"route": { "kind": "handler", "handler": "onResolveConflicts" },
"menuOwned": true
},
"vaultViewChanges": {
"id": "vault-view-changes",
"route": { "kind": "handler", "handler": "onViewChanges" },
"menuOwned": true
},
"vaultInstallMcp": {
"id": "vault-install-mcp",
"route": { "kind": "handler", "handler": "onInstallMcp" },
"menuOwned": true
},
"vaultReload": {
"id": "vault-reload",
"route": { "kind": "handler", "handler": "onReloadVault" },
"menuOwned": true
},
"vaultRepair": {
"id": "vault-repair",
"route": { "kind": "handler", "handler": "onRepairVault" },
"menuOwned": true
}
},
"menus": [
{
"label": "File",
"items": [
{ "kind": "command", "command": "fileNewNote", "label": "New Note" },
{ "kind": "command", "command": "fileNewType", "label": "New Type" },
{ "kind": "command", "command": "fileQuickOpen", "label": "Quick Open" },
{
"kind": "command",
"command": "fileQuickOpen",
"id": "file-quick-open-alias",
"label": { "macos": "Quick Open (Cmd+O)", "default": "Quick Open (Ctrl+O)" },
"accelerator": "CmdOrCtrl+O"
},
{ "kind": "separator" },
{ "kind": "command", "command": "fileSave", "label": "Save" }
]
},
{
"label": "Edit",
"items": [
{ "kind": "command", "command": "editPastePlainText", "label": "Paste without Formatting" },
{ "kind": "separator" },
{ "kind": "command", "command": "editFindInNote", "label": "Find in Note", "enabled": false },
{ "kind": "command", "command": "editReplaceInNote", "label": "Replace in Note", "enabled": false },
{ "kind": "command", "command": "editFindInVault", "label": "Find in Vault" },
{
"kind": "menu-event",
"id": "edit-toggle-note-list-search",
"label": "Toggle Note List Search",
"accelerator": "CmdOrCtrl+F",
"enabled": false
},
{ "kind": "command", "command": "editToggleDiff", "label": "Toggle Diff Mode" }
]
},
{
"label": "View",
"items": [
{ "kind": "command", "command": "viewEditorOnly", "label": "Editor Only" },
{ "kind": "command", "command": "viewEditorList", "label": "Editor + Notes" },
{ "kind": "command", "command": "viewAll", "label": "All Panels" },
{ "kind": "separator" },
{
"kind": "command",
"command": "viewToggleProperties",
"label": "Toggle Properties Panel",
"accelerator": null
},
{ "kind": "separator" },
{ "kind": "command", "command": "viewZoomIn", "label": "Zoom In" },
{ "kind": "command", "command": "viewZoomOut", "label": "Zoom Out" },
{ "kind": "command", "command": "viewZoomReset", "label": "Actual Size" },
{ "kind": "separator" },
{ "kind": "command", "command": "viewCommandPalette", "label": "Command Palette" }
]
},
{
"label": "Go",
"items": [
{ "kind": "command", "command": "goAllNotes", "label": "All Notes" },
{ "kind": "command", "command": "goArchived", "label": "Archived" },
{ "kind": "command", "command": "goChanges", "label": "Changes" },
{ "kind": "command", "command": "goInbox", "label": "Inbox" },
{ "kind": "separator" },
{ "kind": "command", "command": "viewGoBack", "label": "Go Back" },
{ "kind": "command", "command": "viewGoForward", "label": "Go Forward" }
]
},
{
"label": "Note",
"items": [
{ "kind": "command", "command": "noteToggleOrganized", "label": "Toggle Organized" },
{ "kind": "command", "command": "noteArchive", "label": "Archive Note" },
{ "kind": "command", "command": "noteDelete", "label": "Delete Note" },
{ "kind": "command", "command": "noteRestoreDeleted", "label": "Restore Deleted Note", "enabled": false },
{ "kind": "separator" },
{ "kind": "command", "command": "noteOpenInNewWindow", "label": "Open in New Window" },
{ "kind": "separator" },
{ "kind": "command", "command": "editToggleRawEditor", "label": "Toggle Raw Editor" },
{ "kind": "command", "command": "viewToggleAiChat", "label": "Toggle AI Panel" },
{ "kind": "command", "command": "viewToggleBacklinks", "label": "Toggle Backlinks" }
]
},
{
"label": "Vault",
"items": [
{ "kind": "command", "command": "vaultOpen", "label": "Open Vault…" },
{ "kind": "command", "command": "vaultRemove", "label": "Remove Vault from List" },
{ "kind": "command", "command": "vaultRestoreGettingStarted", "label": "Restore Getting Started" },
{ "kind": "separator" },
{ "kind": "command", "command": "vaultAddRemote", "label": "Add Remote…", "enabled": false },
{ "kind": "command", "command": "vaultCommitPush", "label": "Commit & Push" },
{ "kind": "command", "command": "vaultPull", "label": "Pull from Remote" },
{ "kind": "command", "command": "vaultResolveConflicts", "label": "Resolve Conflicts", "enabled": false },
{ "kind": "command", "command": "vaultViewChanges", "label": "View Pending Changes" },
{ "kind": "separator" },
{ "kind": "command", "command": "vaultReload", "label": "Reload Vault" },
{ "kind": "command", "command": "vaultRepair", "label": "Repair Vault" },
{ "kind": "command", "command": "vaultInstallMcp", "label": "Set Up External AI Tools…" }
]
}
],
"appMenu": [
{ "kind": "command", "command": "appCheckForUpdates", "label": "Check for Updates..." },
{ "kind": "separator" },
{ "kind": "command", "command": "appSettings", "label": "Settings..." }
],
"menuStateGroups": {
"noteDependent": [
{ "command": "fileSave" },
{ "command": "noteToggleOrganized" },
{ "command": "noteArchive" },
{ "command": "noteDelete" },
{ "command": "editToggleRawEditor" },
{ "command": "editToggleDiff" },
{ "command": "viewToggleBacklinks" },
{ "command": "noteOpenInNewWindow" }
],
"editorFindDependent": [
{ "command": "editFindInNote" },
{ "command": "editReplaceInNote" }
],
"noteListSearchDependent": [
{ "id": "edit-toggle-note-list-search" }
],
"restoreDeletedDependent": [
{ "command": "noteRestoreDeleted" }
],
"gitCommitDependent": [
{ "command": "vaultCommitPush" }
],
"gitConflictDependent": [
{ "command": "vaultResolveConflicts" }
],
"gitNoRemoteDependent": [
{ "command": "vaultAddRemote" }
]
}
}