diff --git a/src/__tests__/renderer/hooks/useMainKeyboardHandler.test.ts b/src/__tests__/renderer/hooks/useMainKeyboardHandler.test.ts index 4ed9f4d6..b74c3fdf 100644 --- a/src/__tests__/renderer/hooks/useMainKeyboardHandler.test.ts +++ b/src/__tests__/renderer/hooks/useMainKeyboardHandler.test.ts @@ -460,6 +460,43 @@ describe('useMainKeyboardHandler', () => { expect(mockReopenUnifiedClosedTab).toHaveBeenCalledWith(mockActiveSession); expect(mockSetSessions).toHaveBeenCalled(); }); + + it('should allow toggleMode shortcut (Cmd+J) when only overlays are open', () => { + const { result } = renderHook(() => useMainKeyboardHandler()); + + const mockToggleInputMode = vi.fn(); + const mockActiveSession = { + id: 'test-session', + name: 'Test', + inputMode: 'ai', + aiTabs: [{ id: 'tab-1', name: 'Tab 1', logs: [] }], + activeTabId: 'tab-1', + filePreviewTabs: [{ id: 'file-tab-1', path: '/test.ts' }], + activeFileTabId: 'file-tab-1', // File preview is active + }; + + result.current.keyboardHandlerRef.current = createMockContext({ + hasOpenLayers: () => true, // Overlay is open (file preview) + hasOpenModal: () => false, // But no true modal + isShortcut: (_e: KeyboardEvent, actionId: string) => actionId === 'toggleMode', + activeSessionId: 'test-session', + activeSession: mockActiveSession, + toggleInputMode: mockToggleInputMode, + }); + + act(() => { + window.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'j', + metaKey: true, + bubbles: true, + }) + ); + }); + + // Cmd+J should toggle mode even when file preview overlay is open + expect(mockToggleInputMode).toHaveBeenCalled(); + }); }); describe('navigation handlers delegation', () => { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index bd95b82d..b26792b8 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -757,6 +757,8 @@ function MaestroConsoleInner() { const { showUnreadOnly, setShowUnreadOnly } = useUILayout(); // Track the active tab ID before entering unread filter mode, so we can restore it when exiting const { preFilterActiveTabIdRef } = useUILayout(); + // Track the active file tab ID before switching to terminal mode, so we can restore it when returning to AI mode + const { preTerminalFileTabIdRef } = useUILayout(); // File Explorer State const [filePreviewLoading, setFilePreviewLoading] = useState<{ @@ -9799,14 +9801,27 @@ You are taking over this conversation. Based on the context above, provide a bri prev.map((s) => { if (s.id !== activeSessionId) return s; const newMode = s.inputMode === 'ai' ? 'terminal' : 'ai'; - // Clear file preview when switching to terminal mode - // This ensures cmd+j from file preview goes directly to terminal - const clearFileTab = newMode === 'terminal'; - return { - ...s, - inputMode: newMode, - ...(clearFileTab && { activeFileTabId: null }), - }; + + if (newMode === 'terminal') { + // Switching to terminal mode: save current file tab (if any) and clear it + preTerminalFileTabIdRef.current = s.activeFileTabId; + return { + ...s, + inputMode: newMode, + activeFileTabId: null, + }; + } else { + // Switching to AI mode: restore previous file tab if it still exists + const savedFileTabId = preTerminalFileTabIdRef.current; + const fileTabStillExists = + savedFileTabId && s.filePreviewTabs?.some((t) => t.id === savedFileTabId); + preTerminalFileTabIdRef.current = null; + return { + ...s, + inputMode: newMode, + ...(fileTabStillExists && { activeFileTabId: savedFileTabId }), + }; + } }) ); // Close any open dropdowns when switching modes diff --git a/src/renderer/contexts/UILayoutContext.tsx b/src/renderer/contexts/UILayoutContext.tsx index 6eb6ad5c..c0e8efb5 100644 --- a/src/renderer/contexts/UILayoutContext.tsx +++ b/src/renderer/contexts/UILayoutContext.tsx @@ -50,6 +50,8 @@ export interface UILayoutContextValue { setShowUnreadOnly: React.Dispatch>; toggleShowUnreadOnly: () => void; preFilterActiveTabIdRef: React.MutableRefObject; + // Track the active file tab ID before switching to terminal mode, so we can restore it when returning to AI mode + preTerminalFileTabIdRef: React.MutableRefObject; // Session sidebar selection selectedSidebarIndex: number; @@ -128,6 +130,8 @@ export function UILayoutProvider({ children }: UILayoutProviderProps) { const [showUnreadOnly, setShowUnreadOnly] = useState(false); // Track the active tab ID before entering unread filter mode, so we can restore it when exiting const preFilterActiveTabIdRef = useRef(null); + // Track the active file tab ID before switching to terminal mode, so we can restore it when returning to AI mode + const preTerminalFileTabIdRef = useRef(null); // Session sidebar selection const [selectedSidebarIndex, setSelectedSidebarIndex] = useState(0); @@ -206,6 +210,7 @@ export function UILayoutProvider({ children }: UILayoutProviderProps) { setShowUnreadOnly, toggleShowUnreadOnly, preFilterActiveTabIdRef, + preTerminalFileTabIdRef, // Session sidebar selection selectedSidebarIndex,