From 4d7252c78fcfbf32ab7e737153e64359e5b06ab9 Mon Sep 17 00:00:00 2001 From: Luca Rossi Date: Tue, 3 Mar 2026 02:00:48 +0100 Subject: [PATCH] feat: add command palette toggles for all BreadcrumbBar panels (#184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds toggle commands to Cmd+K for: - Toggle Properties Panel (prop/inspector) - Toggle Diff Mode (diff) — disabled without note changes - Toggle Backlinks (back) — disabled without note Updates: - useCommandRegistry.ts: 5 new commands with proper disabled states - useAppCommands.ts: wires onToggleDiff and onToggleBacklinks - Editor.tsx: added diffToggleRef prop (mirrors rawToggleRef pattern) - App.tsx: creates diffToggleRef, passes to Editor, wires commands 10 new tests in useCommandRegistry.test.ts covering all new commands. Co-authored-by: Test Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .claude-pid | 2 +- src/App.tsx | 8 ++- src/components/Editor.tsx | 12 ++++- src/hooks/useAppCommands.ts | 4 ++ src/hooks/useCommandRegistry.test.ts | 77 ++++++++++++++++++++++++++++ src/hooks/useCommandRegistry.ts | 17 ++++-- 6 files changed, 111 insertions(+), 9 deletions(-) diff --git a/.claude-pid b/.claude-pid index 8b137891..f56834e2 100644 --- a/.claude-pid +++ b/.claude-pid @@ -1 +1 @@ - +45402 diff --git a/src/App.tsx b/src/App.tsx index ade057a5..4953bea2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -286,6 +286,8 @@ function App() { // Raw-toggle ref: Editor registers its handleToggleRaw here so the command palette can call it const rawToggleRef = useRef<() => void>(() => {}) + // Diff-toggle ref: Editor registers its handleToggleDiff here so the command palette can call it + const diffToggleRef = useRef<() => void>(() => {}) const { setViewMode, sidebarVisible, noteListVisible } = useViewMode() const zoom = useZoom() @@ -307,7 +309,9 @@ function App() { activeTabPath: notes.activeTabPath, activeTabPathRef: notes.activeTabPathRef, handleCloseTabRef: notes.handleCloseTabRef, tabs: notes.tabs, entries: vault.entries, allContent: vault.allContent, - modifiedCount: vault.modifiedFiles.length, selection, + modifiedCount: vault.modifiedFiles.length, + activeNoteModified: vault.modifiedFiles.some(f => f.path === notes.activeTabPath), + selection, onQuickOpen: dialogs.openQuickOpen, onCommandPalette: dialogs.openCommandPalette, onSearch: dialogs.openSearch, onCreateNote: notes.handleCreateNoteImmediate, @@ -319,6 +323,7 @@ function App() { onArchiveNote: entryActions.handleArchiveNote, onUnarchiveNote: entryActions.handleUnarchiveNote, onCommitPush: commitFlow.openCommitDialog, onSetViewMode: setViewMode, onToggleInspector: () => layout.setInspectorCollapsed(c => !c), + onToggleDiff: () => diffToggleRef.current(), onToggleRawEditor: () => rawToggleRef.current(), onZoomIn: zoom.zoomIn, onZoomOut: zoom.zoomOut, onZoomReset: zoom.zoomReset, zoomLevel: zoom.zoomLevel, @@ -432,6 +437,7 @@ function App() { onSave={handleSave} onTitleSync={handleTitleSync} rawToggleRef={rawToggleRef} + diffToggleRef={diffToggleRef} canGoBack={navHistory.canGoBack} canGoForward={navHistory.canGoForward} onGoBack={handleGoBack} diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index bea1d6e3..b6e3e419 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -66,16 +66,19 @@ interface EditorProps { isDarkTheme?: boolean /** Mutable ref that Editor registers its raw-mode toggle into, for command palette access. */ rawToggleRef?: React.MutableRefObject<() => void> + /** Mutable ref that Editor registers its diff-mode toggle into, for command palette access. */ + diffToggleRef?: React.MutableRefObject<() => void> } function useEditorModeExclusion({ - diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, + diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, diffToggleRef, }: { diffMode: boolean rawMode: boolean handleToggleDiff: () => void | Promise handleToggleRaw: () => void rawToggleRef?: React.MutableRefObject<() => void> + diffToggleRef?: React.MutableRefObject<() => void> }) { const handleToggleDiffExclusive = useCallback(async () => { if (!diffMode && rawMode) handleToggleRaw() @@ -91,6 +94,10 @@ function useEditorModeExclusion({ if (rawToggleRef) rawToggleRef.current = handleToggleRawExclusive }, [rawToggleRef, handleToggleRawExclusive]) + useEffect(() => { + if (diffToggleRef) diffToggleRef.current = handleToggleDiffExclusive + }, [diffToggleRef, handleToggleDiffExclusive]) + return { handleToggleDiffExclusive, handleToggleRawExclusive } } @@ -116,6 +123,7 @@ export const Editor = memo(function Editor({ canGoBack, canGoForward, onGoBack, onGoForward, leftPanelsCollapsed, isDarkTheme, rawToggleRef, + diffToggleRef, }: EditorProps) { const vaultPathRef = useRef(vaultPath) useEffect(() => { vaultPathRef.current = vaultPath }, [vaultPath]) @@ -152,7 +160,7 @@ export const Editor = memo(function Editor({ const { rawMode, handleToggleRaw } = useRawMode({ activeTabPath }) const { handleToggleDiffExclusive, handleToggleRawExclusive } = useEditorModeExclusion({ - diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, + diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, diffToggleRef, }) const isLoadingNewTab = activeTabPath !== null && !activeTab diff --git a/src/hooks/useAppCommands.ts b/src/hooks/useAppCommands.ts index 918387d2..4c252c47 100644 --- a/src/hooks/useAppCommands.ts +++ b/src/hooks/useAppCommands.ts @@ -33,7 +33,9 @@ interface AppCommandsConfig { onCommitPush: () => void onSetViewMode: (mode: ViewMode) => void onToggleInspector: () => void + onToggleDiff?: () => void onToggleRawEditor?: () => void + activeNoteModified: boolean onZoomIn: () => void onZoomOut: () => void onZoomReset: () => void @@ -135,7 +137,9 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] { onCommitPush: config.onCommitPush, onSetViewMode: config.onSetViewMode, onToggleInspector: config.onToggleInspector, + onToggleDiff: config.onToggleDiff, onToggleRawEditor: config.onToggleRawEditor, + activeNoteModified: config.activeNoteModified, onZoomIn: config.onZoomIn, onZoomOut: config.onZoomOut, onZoomReset: config.onZoomReset, diff --git a/src/hooks/useCommandRegistry.test.ts b/src/hooks/useCommandRegistry.test.ts index 50f918ea..46ee0592 100644 --- a/src/hooks/useCommandRegistry.test.ts +++ b/src/hooks/useCommandRegistry.test.ts @@ -36,6 +36,7 @@ function makeConfig(overrides: Record = {}) { activeTabPath: null as string | null, entries: [] as VaultEntry[], modifiedCount: 0, + activeNoteModified: false, onQuickOpen: vi.fn(), onCreateNote: vi.fn(), onCreateNoteOfType: vi.fn(), @@ -264,6 +265,82 @@ describe('useCommandRegistry', () => { expect(onToggleAIChat).toHaveBeenCalled() }) + it('has toggle-diff command in View group', () => { + const { result } = renderHook(() => useCommandRegistry(makeConfig())) + const cmd = result.current.find(c => c.id === 'toggle-diff') + expect(cmd).toBeDefined() + expect(cmd!.group).toBe('View') + expect(cmd!.label).toBe('Toggle Diff Mode') + }) + + it('disables toggle-diff when no note is open', () => { + const { result } = renderHook(() => useCommandRegistry(makeConfig({ activeTabPath: null }))) + const cmd = result.current.find(c => c.id === 'toggle-diff') + expect(cmd!.enabled).toBe(false) + }) + + it('disables toggle-diff when note has no changes', () => { + const { result } = renderHook(() => + useCommandRegistry(makeConfig({ activeTabPath: '/vault/note/test.md', activeNoteModified: false })), + ) + const cmd = result.current.find(c => c.id === 'toggle-diff') + expect(cmd!.enabled).toBe(false) + }) + + it('enables toggle-diff when note has uncommitted changes', () => { + const { result } = renderHook(() => + useCommandRegistry(makeConfig({ activeTabPath: '/vault/note/test.md', activeNoteModified: true })), + ) + const cmd = result.current.find(c => c.id === 'toggle-diff') + expect(cmd!.enabled).toBe(true) + }) + + it('calls onToggleDiff when toggle-diff executes', () => { + const onToggleDiff = vi.fn() + const { result } = renderHook(() => + useCommandRegistry(makeConfig({ activeTabPath: '/vault/note/test.md', activeNoteModified: true, onToggleDiff })), + ) + result.current.find(c => c.id === 'toggle-diff')!.execute() + expect(onToggleDiff).toHaveBeenCalledOnce() + }) + + it('has toggle-backlinks command in View group', () => { + const { result } = renderHook(() => useCommandRegistry(makeConfig())) + const cmd = result.current.find(c => c.id === 'toggle-backlinks') + expect(cmd).toBeDefined() + expect(cmd!.group).toBe('View') + expect(cmd!.label).toBe('Toggle Backlinks') + }) + + it('disables toggle-backlinks when no note is open', () => { + const { result } = renderHook(() => useCommandRegistry(makeConfig({ activeTabPath: null }))) + const cmd = result.current.find(c => c.id === 'toggle-backlinks') + expect(cmd!.enabled).toBe(false) + }) + + it('enables toggle-backlinks when a note is open', () => { + const { result } = renderHook(() => + useCommandRegistry(makeConfig({ activeTabPath: '/vault/note/test.md' })), + ) + const cmd = result.current.find(c => c.id === 'toggle-backlinks') + expect(cmd!.enabled).toBe(true) + }) + + it('calls onToggleInspector when toggle-backlinks executes', () => { + const onToggleInspector = vi.fn() + const { result } = renderHook(() => + useCommandRegistry(makeConfig({ activeTabPath: '/vault/note/test.md', onToggleInspector })), + ) + result.current.find(c => c.id === 'toggle-backlinks')!.execute() + expect(onToggleInspector).toHaveBeenCalledOnce() + }) + + it('toggle-inspector label includes Properties', () => { + const { result } = renderHook(() => useCommandRegistry(makeConfig())) + const cmd = result.current.find(c => c.id === 'toggle-inspector') + expect(cmd!.label).toBe('Toggle Properties Panel') + }) + it('has open-daily-note command with shortcut', () => { const { result } = renderHook(() => useCommandRegistry(makeConfig())) const cmd = result.current.find(c => c.id === 'open-daily-note') diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts index 8cf70c7c..4ab28c09 100644 --- a/src/hooks/useCommandRegistry.ts +++ b/src/hooks/useCommandRegistry.ts @@ -33,8 +33,10 @@ interface CommandRegistryConfig { onCommitPush: () => void onSetViewMode: (mode: ViewMode) => void onToggleInspector: () => void + onToggleDiff?: () => void onToggleRawEditor?: () => void onToggleAIChat?: () => void + activeNoteModified: boolean onCheckForUpdates?: () => void isUpdating?: boolean onZoomIn: () => void @@ -109,8 +111,10 @@ export function buildTypeCommands( export function buildViewCommands( hasActiveNote: boolean, + activeNoteModified: boolean, onSetViewMode: (mode: ViewMode) => void, onToggleInspector: () => void, + onToggleDiff: (() => void) | undefined, onToggleRawEditor: (() => void) | undefined, onToggleAIChat: (() => void) | undefined, zoomLevel: number, @@ -122,9 +126,11 @@ export function buildViewCommands( { id: 'view-editor', label: 'Editor Only', group: 'View', shortcut: '⌘1', keywords: ['layout', 'focus'], enabled: true, execute: () => onSetViewMode('editor-only') }, { id: 'view-editor-list', label: 'Editor + Note List', group: 'View', shortcut: '⌘2', keywords: ['layout'], enabled: true, execute: () => onSetViewMode('editor-list') }, { id: 'view-all', label: 'Full Layout', group: 'View', shortcut: '⌘3', keywords: ['layout', 'sidebar'], enabled: true, execute: () => onSetViewMode('all') }, - { id: 'toggle-inspector', label: 'Toggle Inspector', group: 'View', keywords: ['properties', 'panel', 'right'], enabled: true, execute: onToggleInspector }, + { id: 'toggle-inspector', label: 'Toggle Properties Panel', group: 'View', keywords: ['properties', 'inspector', 'panel', 'right', 'sidebar'], enabled: true, execute: onToggleInspector }, + { id: 'toggle-diff', label: 'Toggle Diff Mode', group: 'View', keywords: ['diff', 'changes', 'git', 'compare', 'version'], enabled: hasActiveNote && activeNoteModified, execute: () => onToggleDiff?.() }, { id: 'toggle-raw-editor', label: 'Toggle Raw Editor', group: 'View', keywords: ['raw', 'source', 'markdown', 'frontmatter', 'code', 'textarea'], enabled: hasActiveNote, execute: () => onToggleRawEditor?.() }, { id: 'toggle-ai-chat', label: 'Toggle AI Chat', group: 'View', shortcut: '⌘I', keywords: ['ai', 'agent', 'chat', 'assistant', 'contextual'], enabled: true, execute: () => onToggleAIChat?.() }, + { id: 'toggle-backlinks', label: 'Toggle Backlinks', group: 'View', keywords: ['backlinks', 'references', 'links', 'mentions', 'incoming'], enabled: hasActiveNote, execute: onToggleInspector }, { id: 'zoom-in', label: `Zoom In (${zoomLevel}%)`, group: 'View', shortcut: '⌘=', keywords: ['zoom', 'bigger', 'larger', 'scale'], enabled: zoomLevel < 150, execute: onZoomIn }, { id: 'zoom-out', label: `Zoom Out (${zoomLevel}%)`, group: 'View', shortcut: '⌘-', keywords: ['zoom', 'smaller', 'scale'], enabled: zoomLevel > 80, execute: onZoomOut }, { id: 'zoom-reset', label: 'Reset Zoom', group: 'View', shortcut: '⌘0', keywords: ['zoom', 'actual', 'default', '100'], enabled: zoomLevel !== 100, execute: onZoomReset }, @@ -173,7 +179,8 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction activeTabPath, entries, modifiedCount, onQuickOpen, onCreateNote, onCreateNoteOfType, onSave, onOpenSettings, onTrashNote, onRestoreNote, onArchiveNote, onUnarchiveNote, - onCommitPush, onSetViewMode, onToggleInspector, onToggleRawEditor, onToggleAIChat, onOpenVault, + onCommitPush, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat, onOpenVault, + activeNoteModified, onZoomIn, onZoomOut, onZoomReset, zoomLevel, onSelect, onOpenDailyNote, onCloseTab, onGoBack, onGoForward, canGoBack, canGoForward, @@ -227,7 +234,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction { id: 'view-changes', label: 'View Pending Changes', group: 'Git', keywords: ['modified', 'diff'], enabled: true, execute: () => onSelect({ kind: 'filter', filter: 'changes' }) }, // View - ...buildViewCommands(hasActiveNote, onSetViewMode, onToggleInspector, onToggleRawEditor, onToggleAIChat, zoomLevel, onZoomIn, onZoomOut, onZoomReset), + ...buildViewCommands(hasActiveNote, activeNoteModified, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat, zoomLevel, onZoomIn, onZoomOut, onZoomReset), // Appearance ...buildThemeCommands(themes, activeThemeId, onSwitchTheme, onCreateTheme, onOpenTheme), @@ -243,10 +250,10 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction return cmds }, [ - hasActiveNote, activeTabPath, isArchived, isTrashed, modifiedCount, + hasActiveNote, activeTabPath, isArchived, isTrashed, modifiedCount, activeNoteModified, onQuickOpen, onCreateNote, onCreateNoteOfType, onCreateType, onSave, onOpenSettings, onTrashNote, onRestoreNote, onArchiveNote, onUnarchiveNote, - onCommitPush, onSetViewMode, onToggleInspector, onToggleRawEditor, onToggleAIChat, onOpenVault, + onCommitPush, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat, onOpenVault, onCheckForUpdates, isUpdating, onZoomIn, onZoomOut, onZoomReset, zoomLevel, onSelect, onOpenDailyNote, onCloseTab,