feat: add Cmd+Shift+T shortcut, menu item, and full wiring

Add "Reopen Closed Tab" to File menu with CmdOrCtrl+Shift+T accelerator.
Wire onReopenClosedTab through useAppKeyboard, useMenuEvents,
useAppCommands, and App.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-11 23:36:42 +01:00
parent 73278f3baf
commit 0d6cce1588
7 changed files with 48 additions and 2 deletions

View File

@@ -13,6 +13,7 @@ const FILE_DAILY_NOTE: &str = "file-daily-note";
const FILE_QUICK_OPEN: &str = "file-quick-open";
const FILE_SAVE: &str = "file-save";
const FILE_CLOSE_TAB: &str = "file-close-tab";
const FILE_REOPEN_CLOSED_TAB: &str = "file-reopen-closed-tab";
const EDIT_FIND_IN_VAULT: &str = "edit-find-in-vault";
const EDIT_TOGGLE_RAW_EDITOR: &str = "edit-toggle-raw-editor";
@@ -62,6 +63,7 @@ const CUSTOM_IDS: &[&str] = &[
FILE_QUICK_OPEN,
FILE_SAVE,
FILE_CLOSE_TAB,
FILE_REOPEN_CLOSED_TAB,
EDIT_FIND_IN_VAULT,
EDIT_TOGGLE_RAW_EDITOR,
EDIT_TOGGLE_DIFF,
@@ -167,6 +169,10 @@ fn build_file_menu(app: &App) -> MenuResult {
.id(FILE_CLOSE_TAB)
.accelerator("CmdOrCtrl+W")
.build(app)?;
let reopen_closed_tab = MenuItemBuilder::new("Reopen Closed Tab")
.id(FILE_REOPEN_CLOSED_TAB)
.accelerator("CmdOrCtrl+Shift+T")
.build(app)?;
Ok(SubmenuBuilder::new(app, "File")
.item(&new_note)
@@ -176,6 +182,7 @@ fn build_file_menu(app: &App) -> MenuResult {
.separator()
.item(&save)
.item(&close_tab)
.item(&reopen_closed_tab)
.build()?)
}

View File

@@ -596,6 +596,7 @@ function App() {
onInstallMcp: installMcp,
onEmptyTrash: handleEmptyTrash,
trashedCount,
onReopenClosedTab: notes.handleReopenClosedTab,
onReindexVault: indexing.triggerFullReindex,
onReloadVault: vault.reloadVault,
onRepairVault: handleRepairVault,

View File

@@ -67,6 +67,7 @@ interface AppCommandsConfig {
onInstallMcp?: () => void
onEmptyTrash?: () => void
trashedCount?: number
onReopenClosedTab?: () => void
onReindexVault?: () => void
onReloadVault?: () => void
onRepairVault?: () => void
@@ -116,6 +117,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
onGoForward: config.onGoForward,
onToggleAIChat: config.onToggleAIChat,
onToggleRawEditor: config.onToggleRawEditor,
onReopenClosedTab: config.onReopenClosedTab,
activeTabPathRef: config.activeTabPathRef,
handleCloseTabRef: config.handleCloseTabRef,
})
@@ -156,6 +158,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
onReloadVault: config.onReloadVault,
onRepairVault: config.onRepairVault,
onEmptyTrash: config.onEmptyTrash,
onReopenClosedTab: config.onReopenClosedTab,
activeTabPathRef: config.activeTabPathRef,
handleCloseTabRef: config.handleCloseTabRef,
activeTabPath: config.activeTabPath,

View File

@@ -183,6 +183,23 @@ describe('useAppKeyboard', () => {
expect(actions.onZoomReset).toHaveBeenCalled()
})
it('Cmd+Shift+T triggers reopen closed tab', () => {
const actions = makeActions()
const onReopenClosedTab = vi.fn()
renderHook(() => useAppKeyboard({ ...actions, onReopenClosedTab }))
fireKey('t', { metaKey: true, shiftKey: true })
expect(onReopenClosedTab).toHaveBeenCalled()
})
it('Cmd+Shift+T does not trigger other shortcuts', () => {
const actions = makeActions()
const onReopenClosedTab = vi.fn()
renderHook(() => useAppKeyboard({ ...actions, onReopenClosedTab }))
fireKey('t', { metaKey: true, shiftKey: true })
expect(actions.onQuickOpen).not.toHaveBeenCalled()
expect(actions.onCreateNote).not.toHaveBeenCalled()
})
it('Cmd+I triggers toggle AI chat', () => {
const actions = makeActions()
const onToggleAIChat = vi.fn()

View File

@@ -19,6 +19,7 @@ interface KeyboardActions {
onGoForward?: () => void
onToggleAIChat?: () => void
onToggleRawEditor?: () => void
onReopenClosedTab?: () => void
activeTabPathRef: React.MutableRefObject<string | null>
handleCloseTabRef: React.MutableRefObject<(path: string) => void>
}
@@ -64,7 +65,7 @@ function handleCmdKey(e: KeyboardEvent, keyMap: Record<string, ShortcutHandler>)
export function useAppKeyboard({
onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote,
onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, activeTabPathRef, handleCloseTabRef,
onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onReopenClosedTab, activeTabPathRef, handleCloseTabRef,
}: KeyboardActions) {
useEffect(() => {
const withActiveTab = (fn: (path: string) => void): ShortcutHandler => () => {
@@ -100,11 +101,17 @@ export function useAppKeyboard({
onSearch()
return
}
// Cmd+Shift+T: reopen last closed tab
if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === 't' || e.key === 'T')) {
e.preventDefault()
onReopenClosedTab?.()
return
}
if (!handleViewModeKey(e, onSetViewMode)) {
handleCmdKey(e, cmdKeyMap)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor])
}, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onReopenClosedTab])
}

View File

@@ -36,6 +36,7 @@ function makeHandlers(): MenuEventHandlers {
onInstallMcp: vi.fn(),
onReindexVault: vi.fn(),
onReloadVault: vi.fn(),
onReopenClosedTab: vi.fn(),
activeTabPathRef: { current: '/vault/test.md' } as React.MutableRefObject<string | null>,
handleCloseTabRef: { current: vi.fn() } as React.MutableRefObject<(path: string) => void>,
activeTabPath: '/vault/test.md',
@@ -313,6 +314,13 @@ describe('dispatchMenuEvent', () => {
expect(h.onReloadVault).toHaveBeenCalled()
})
// File menu: reopen closed tab
it('file-reopen-closed-tab triggers reopen closed tab', () => {
const h = makeHandlers()
dispatchMenuEvent('file-reopen-closed-tab', h)
expect(h.onReopenClosedTab).toHaveBeenCalled()
})
// Edge cases
it('unknown event ID does nothing', () => {
const h = makeHandlers()

View File

@@ -35,6 +35,7 @@ export interface MenuEventHandlers {
onResolveConflicts?: () => void
onViewChanges?: () => void
onInstallMcp?: () => void
onReopenClosedTab?: () => void
onReindexVault?: () => void
onReloadVault?: () => void
onRepairVault?: () => void
@@ -83,6 +84,7 @@ type OptionalHandler =
| 'onCreateTheme' | 'onRestoreDefaultThemes'
| 'onCommitPush' | 'onResolveConflicts' | 'onViewChanges' | 'onInstallMcp' | 'onReindexVault' | 'onReloadVault' | 'onRepairVault'
| 'onEmptyTrash'
| 'onReopenClosedTab'
const OPTIONAL_EVENT_MAP: Record<string, OptionalHandler> = {
'view-go-back': 'onGoBack',
@@ -105,6 +107,7 @@ const OPTIONAL_EVENT_MAP: Record<string, OptionalHandler> = {
'vault-reload': 'onReloadVault',
'vault-repair': 'onRepairVault',
'note-empty-trash': 'onEmptyTrash',
'file-reopen-closed-tab': 'onReopenClosedTab',
}
function dispatchActiveTabEvent(id: string, h: MenuEventHandlers): boolean {