/** * useKeyboardShortcuts - Global keyboard shortcut handler * Handles app-wide keyboard shortcuts for tab management, navigation, and pane management. * * Pane-scoped: Tab cycling (Ctrl+Tab, Cmd+1-9, Cmd+Shift+[/]) operates within the focused pane. * Pane shortcuts: Cmd+Option+1-4 (focus pane), Cmd+\ (split right), Cmd+Option+W (close pane). */ import { useEffect } from 'react'; import { createLogger } from '@shared/utils/logger';import { useShallow } from 'zustand/react/shallow'; import { useStore } from '../store'; const logger = createLogger('Hook:KeyboardShortcuts'); export function useKeyboardShortcuts(): void { const { openTabs, activeTabId, selectedTabIds, openDashboard, closeTab, closeAllTabs, closeTabs, setActiveTab, showSearch, getActiveTab, selectedProjectId, selectedSessionId, fetchSessionDetail, fetchSessions, openCommandPalette, openSettingsTab, toggleSidebar, paneLayout, focusPane, splitPane, closePane, availableContexts, activeContextId, switchContext, isContextSwitching, } = useStore( useShallow((s) => ({ openTabs: s.openTabs, activeTabId: s.activeTabId, selectedTabIds: s.selectedTabIds, openDashboard: s.openDashboard, closeTab: s.closeTab, closeAllTabs: s.closeAllTabs, closeTabs: s.closeTabs, setActiveTab: s.setActiveTab, showSearch: s.showSearch, getActiveTab: s.getActiveTab, selectedProjectId: s.selectedProjectId, selectedSessionId: s.selectedSessionId, fetchSessionDetail: s.fetchSessionDetail, fetchSessions: s.fetchSessions, openCommandPalette: s.openCommandPalette, openSettingsTab: s.openSettingsTab, toggleSidebar: s.toggleSidebar, paneLayout: s.paneLayout, focusPane: s.focusPane, splitPane: s.splitPane, closePane: s.closePane, availableContexts: s.availableContexts, activeContextId: s.activeContextId, switchContext: s.switchContext, isContextSwitching: s.isContextSwitching, })) ); useEffect(() => { function handleKeyDown(event: KeyboardEvent): void { // Check if Cmd (macOS) or Ctrl (Windows/Linux) is pressed const isMod = event.metaKey || event.ctrlKey; // Ctrl+Tab / Ctrl+Shift+Tab: Switch tabs within focused pane (universal shortcut) if (event.ctrlKey && event.key === 'Tab') { event.preventDefault(); const currentIndex = openTabs.findIndex((t) => t.id === activeTabId); if (event.shiftKey) { // Ctrl+Shift+Tab: Previous tab (with wrap-around) if (currentIndex > 0) { setActiveTab(openTabs[currentIndex - 1].id); } else if (openTabs.length > 0) { // Wrap to last tab setActiveTab(openTabs[openTabs.length - 1].id); } } else { // Ctrl+Tab: Next tab (with wrap-around) if (currentIndex !== -1 && currentIndex < openTabs.length - 1) { setActiveTab(openTabs[currentIndex + 1].id); } else if (openTabs.length > 0) { // Wrap to first tab setActiveTab(openTabs[0].id); } } return; } if (!isMod) return; // --- Pane management shortcuts (Cmd+Option) --- // Cmd+Option+1-4: Focus pane by index if (event.altKey && !event.shiftKey) { const numKey = parseInt(event.key); if (numKey >= 1 && numKey <= 4) { event.preventDefault(); const targetPane = paneLayout.panes[numKey - 1]; if (targetPane) { focusPane(targetPane.id); } return; } // Cmd+Option+W: Close current pane if (event.key === 'w') { event.preventDefault(); if (paneLayout.panes.length > 1) { closePane(paneLayout.focusedPaneId); } return; } } // Cmd+\: Split right with current tab if (event.key === '\\' && !event.altKey && !event.shiftKey) { event.preventDefault(); if (activeTabId) { splitPane(paneLayout.focusedPaneId, activeTabId, 'right'); } return; } // Cmd+T: New tab (Dashboard) if (event.key === 't') { event.preventDefault(); openDashboard(); return; } // Cmd+Shift+W: Close all tabs if (event.key === 'w' && event.shiftKey && !event.altKey) { event.preventDefault(); closeAllTabs(); return; } // Cmd+W: Close selected tabs (if multi-selected) or active tab if (event.key === 'w' && !event.altKey) { event.preventDefault(); if (selectedTabIds.length > 0) { closeTabs(selectedTabIds); } else if (activeTabId) { closeTab(activeTabId); } return; } // Cmd+[1-9]: Switch to tab by index within focused pane const numKey = parseInt(event.key); if (numKey >= 1 && numKey <= 9 && !event.altKey) { event.preventDefault(); const targetTab = openTabs[numKey - 1]; if (targetTab) { setActiveTab(targetTab.id); } return; } // Cmd+Shift+]: Next tab within focused pane if (event.key === ']' && event.shiftKey) { event.preventDefault(); const currentIndex = openTabs.findIndex((t) => t.id === activeTabId); if (currentIndex !== -1 && currentIndex < openTabs.length - 1) { setActiveTab(openTabs[currentIndex + 1].id); } return; } // Cmd+Shift+[: Previous tab within focused pane if (event.key === '[' && event.shiftKey) { event.preventDefault(); const currentIndex = openTabs.findIndex((t) => t.id === activeTabId); if (currentIndex > 0) { setActiveTab(openTabs[currentIndex - 1].id); } return; } // Cmd+Option+Right: Next tab (browser-style) within focused pane if (event.key === 'ArrowRight' && event.altKey) { event.preventDefault(); const currentIndex = openTabs.findIndex((t) => t.id === activeTabId); if (currentIndex !== -1 && currentIndex < openTabs.length - 1) { setActiveTab(openTabs[currentIndex + 1].id); } return; } // Cmd+Option+Left: Previous tab (browser-style) within focused pane if (event.key === 'ArrowLeft' && event.altKey) { event.preventDefault(); const currentIndex = openTabs.findIndex((t) => t.id === activeTabId); if (currentIndex > 0) { setActiveTab(openTabs[currentIndex - 1].id); } return; } // Cmd+Shift+K: Cycle to next workspace context if (event.key === 'k' && event.shiftKey) { event.preventDefault(); if (!isContextSwitching && availableContexts.length > 1) { const currentIndex = availableContexts.findIndex((c) => c.id === activeContextId); const nextIndex = (currentIndex + 1) % availableContexts.length; void switchContext(availableContexts[nextIndex].id); } return; } // Cmd+K: Open command palette for global search if (event.key === 'k') { event.preventDefault(); openCommandPalette(); return; } // Cmd+,: Open settings (standard macOS shortcut) if (event.key === ',') { event.preventDefault(); openSettingsTab(); return; } // Cmd+F: Find in session if (event.key === 'f') { event.preventDefault(); const activeTab = getActiveTab(); // Only enable search in session views, not dashboard if (activeTab?.type === 'session') { showSearch(); } return; } // Cmd+O: Open project (placeholder for future implementation) if (event.key === 'o') { event.preventDefault(); logger.debug('Open project shortcut triggered (not yet implemented)'); return; } // Cmd+R: Refresh current session and sidebar session list if (event.key === 'r') { event.preventDefault(); if (selectedProjectId && selectedSessionId) { void Promise.all([ fetchSessionDetail(selectedProjectId, selectedSessionId), fetchSessions(selectedProjectId), ]); } return; } // Cmd+B: Toggle sidebar if (event.key === 'b') { event.preventDefault(); toggleSidebar(); } } window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [ openTabs, activeTabId, selectedTabIds, openDashboard, closeTab, closeAllTabs, closeTabs, setActiveTab, showSearch, getActiveTab, selectedProjectId, selectedSessionId, fetchSessionDetail, fetchSessions, openCommandPalette, openSettingsTab, toggleSidebar, paneLayout, focusPane, splitPane, closePane, availableContexts, activeContextId, switchContext, isContextSwitching, ]);}
/**
* useKeyboardShortcuts - Global keyboard shortcut handler
* Handles app-wide keyboard shortcuts for tab management, navigation, and pane management.
*
* Pane-scoped: Tab cycling (Ctrl+Tab, Cmd+1-9, Cmd+Shift+[/]) operates within the focused pane.
* Pane shortcuts: Cmd+Option+1-4 (focus pane), Cmd+\ (split right), Cmd+Option+W (close pane).
*/
import { useEffect } from 'react';
import { createLogger } from '@shared/utils/logger';
import { useShallow } from 'zustand/react/shallow';
import { useStore } from '../store';
const logger = createLogger('Hook:KeyboardShortcuts');
export function useKeyboardShortcuts(): void {
const {
openTabs,
activeTabId,
selectedTabIds,
openDashboard,
closeTab,
closeAllTabs,
closeTabs,
setActiveTab,
showSearch,
getActiveTab,
selectedProjectId,
selectedSessionId,
fetchSessionDetail,
fetchSessions,
openCommandPalette,
openSettingsTab,
toggleSidebar,
paneLayout,
focusPane,
splitPane,
closePane,
availableContexts,
activeContextId,
switchContext,
isContextSwitching,
} = useStore(
useShallow((s) => ({
openTabs: s.openTabs,
activeTabId: s.activeTabId,
selectedTabIds: s.selectedTabIds,
openDashboard: s.openDashboard,
closeTab: s.closeTab,
closeAllTabs: s.closeAllTabs,
closeTabs: s.closeTabs,
setActiveTab: s.setActiveTab,
showSearch: s.showSearch,
getActiveTab: s.getActiveTab,
selectedProjectId: s.selectedProjectId,
selectedSessionId: s.selectedSessionId,
fetchSessionDetail: s.fetchSessionDetail,
fetchSessions: s.fetchSessions,
openCommandPalette: s.openCommandPalette,
openSettingsTab: s.openSettingsTab,
toggleSidebar: s.toggleSidebar,
paneLayout: s.paneLayout,
focusPane: s.focusPane,
splitPane: s.splitPane,
closePane: s.closePane,
availableContexts: s.availableContexts,
activeContextId: s.activeContextId,
switchContext: s.switchContext,
isContextSwitching: s.isContextSwitching,
}))
);
useEffect(() => {
function handleKeyDown(event: KeyboardEvent): void {
// Check if Cmd (macOS) or Ctrl (Windows/Linux) is pressed
const isMod = event.metaKey || event.ctrlKey;
// Ctrl+Tab / Ctrl+Shift+Tab: Switch tabs within focused pane (universal shortcut)
if (event.ctrlKey && event.key === 'Tab') {
event.preventDefault();
const currentIndex = openTabs.findIndex((t) => t.id === activeTabId);
if (event.shiftKey) {
// Ctrl+Shift+Tab: Previous tab (with wrap-around)
if (currentIndex > 0) {
setActiveTab(openTabs[currentIndex - 1].id);
} else if (openTabs.length > 0) {
// Wrap to last tab
setActiveTab(openTabs[openTabs.length - 1].id);
}
} else {
// Ctrl+Tab: Next tab (with wrap-around)
if (currentIndex !== -1 && currentIndex < openTabs.length - 1) {
setActiveTab(openTabs[currentIndex + 1].id);
} else if (openTabs.length > 0) {
// Wrap to first tab
setActiveTab(openTabs[0].id);
}
}
return;
}
if (!isMod) return;
// --- Pane management shortcuts (Cmd+Option) ---
// Cmd+Option+1-4: Focus pane by index
if (event.altKey && !event.shiftKey) {
const numKey = parseInt(event.key);
if (numKey >= 1 && numKey <= 4) {
event.preventDefault();
const targetPane = paneLayout.panes[numKey - 1];
if (targetPane) {
focusPane(targetPane.id);
}
return;
}
// Cmd+Option+W: Close current pane
if (event.key === 'w') {
event.preventDefault();
if (paneLayout.panes.length > 1) {
closePane(paneLayout.focusedPaneId);
}
return;
}
}
// Cmd+\: Split right with current tab
if (event.key === '\\' && !event.altKey && !event.shiftKey) {
event.preventDefault();
if (activeTabId) {
splitPane(paneLayout.focusedPaneId, activeTabId, 'right');
}
return;
}
// Cmd+T: New tab (Dashboard)
if (event.key === 't') {
event.preventDefault();
openDashboard();
return;
}
// Cmd+Shift+W: Close all tabs
if (event.key === 'w' && event.shiftKey && !event.altKey) {
event.preventDefault();
closeAllTabs();
return;
}
// Cmd+W: Close selected tabs (if multi-selected) or active tab
if (event.key === 'w' && !event.altKey) {
event.preventDefault();
if (selectedTabIds.length > 0) {
closeTabs(selectedTabIds);
} else if (activeTabId) {
closeTab(activeTabId);
}
return;
}
// Cmd+[1-9]: Switch to tab by index within focused pane
const numKey = parseInt(event.key);
if (numKey >= 1 && numKey <= 9 && !event.altKey) {
event.preventDefault();
const targetTab = openTabs[numKey - 1];
if (targetTab) {
setActiveTab(targetTab.id);
}
return;
}
// Cmd+Shift+]: Next tab within focused pane
if (event.key === ']' && event.shiftKey) {
event.preventDefault();
const currentIndex = openTabs.findIndex((t) => t.id === activeTabId);
if (currentIndex !== -1 && currentIndex < openTabs.length - 1) {
setActiveTab(openTabs[currentIndex + 1].id);
}
return;
}
// Cmd+Shift+[: Previous tab within focused pane
if (event.key === '[' && event.shiftKey) {
event.preventDefault();
const currentIndex = openTabs.findIndex((t) => t.id === activeTabId);
if (currentIndex > 0) {
setActiveTab(openTabs[currentIndex - 1].id);
}
return;
}
// Cmd+Option+Right: Next tab (browser-style) within focused pane
if (event.key === 'ArrowRight' && event.altKey) {
event.preventDefault();
const currentIndex = openTabs.findIndex((t) => t.id === activeTabId);
if (currentIndex !== -1 && currentIndex < openTabs.length - 1) {
setActiveTab(openTabs[currentIndex + 1].id);
}
return;
}
// Cmd+Option+Left: Previous tab (browser-style) within focused pane
if (event.key === 'ArrowLeft' && event.altKey) {
event.preventDefault();
const currentIndex = openTabs.findIndex((t) => t.id === activeTabId);
if (currentIndex > 0) {
setActiveTab(openTabs[currentIndex - 1].id);
}
return;
}
// Cmd+Shift+K: Cycle to next workspace context
if (event.key === 'k' && event.shiftKey) {
event.preventDefault();
if (!isContextSwitching && availableContexts.length > 1) {
const currentIndex = availableContexts.findIndex((c) => c.id === activeContextId);
const nextIndex = (currentIndex + 1) % availableContexts.length;
void switchContext(availableContexts[nextIndex].id);
}
return;
}
// Cmd+K: Open command palette for global search
if (event.key === 'k') {
event.preventDefault();
openCommandPalette();
return;
}
// Cmd+,: Open settings (standard macOS shortcut)
if (event.key === ',') {
event.preventDefault();
openSettingsTab();
return;
}
// Cmd+F: Find in session
if (event.key === 'f') {
event.preventDefault();
const activeTab = getActiveTab();
// Only enable search in session views, not dashboard
if (activeTab?.type === 'session') {
showSearch();
}
return;
}
// Cmd+O: Open project (placeholder for future implementation)
if (event.key === 'o') {
event.preventDefault();
logger.debug('Open project shortcut triggered (not yet implemented)');
return;
}
// Cmd+R: Refresh current session and sidebar session list
if (event.key === 'r') {
event.preventDefault();
if (selectedProjectId && selectedSessionId) {
void Promise.all([
fetchSessionDetail(selectedProjectId, selectedSessionId),
fetchSessions(selectedProjectId),
]);
}
return;
}
// Cmd+B: Toggle sidebar
if (event.key === 'b') {
event.preventDefault();
toggleSidebar();
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [
openTabs,
activeTabId,
selectedTabIds,
openDashboard,
closeTab,
closeAllTabs,
closeTabs,
setActiveTab,
showSearch,
getActiveTab,
selectedProjectId,
selectedSessionId,
fetchSessionDetail,
fetchSessions,
openCommandPalette,
openSettingsTab,
toggleSidebar,
paneLayout,
focusPane,
splitPane,
closePane,
availableContexts,
activeContextId,
switchContext,
isContextSwitching,
]);
}