Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions apps/desktop/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ For Electron interprocess communnication, ALWAYS use trpc as defined in `src/lib
Please use alias as defined in `tsconfig.json` when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.



# Code quality
```
Code is clean if it can be understood easily – by everyone on the team. Clean code can be read and enhanced by a developer other than its original author. With understandability comes readability, changeability, extensibility and maintainability.
Expand Down
158 changes: 158 additions & 0 deletions apps/desktop/src/renderer/hooks/useGlobalShortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useEffect, useMemo } from "react";
import { createShortcutHandler } from "../lib/keyboard-shortcuts";
import {
createSplitPaneShortcuts,
createTabShortcuts,
createWorkspaceShortcuts,
} from "../lib/shortcuts";
import { useSidebarStore } from "../stores/sidebar-state";
import {
useActiveTabIds,
useAddTab,
useRemoveTab,
useSetActiveTab,
useSplitTabHorizontal,
useSplitTabVertical,
useTabs,
} from "../stores/tabs";
import { useWorkspacesStore } from "../stores/workspaces";

function findWorkspaceIndex(
workspaces: Array<{ id: string }>,
id: string | null,
) {
if (!id) return -1;
return workspaces.findIndex((w) => w.id === id);
}

function findTabIndex(tabs: Array<{ id: string }>, id: string | null) {
if (!id) return -1;
return tabs.findIndex((t) => t.id === id);
}

export function useGlobalShortcuts() {
const { workspaces, activeWorkspaceId, setActiveWorkspace } =
useWorkspacesStore();
const { toggleSidebar } = useSidebarStore();
const tabs = useTabs();
const activeTabIds = useActiveTabIds();
const setActiveTab = useSetActiveTab();
const addTab = useAddTab();
const removeTab = useRemoveTab();
const splitTabVertical = useSplitTabVertical();
const splitTabHorizontal = useSplitTabHorizontal();

const workspaceTabs = useMemo(() => {
if (!activeWorkspaceId) return [];
return tabs.filter(
(t) => t.workspaceId === activeWorkspaceId && !t.parentId,
);
}, [tabs, activeWorkspaceId]);

const activeTabId = activeWorkspaceId
? activeTabIds[activeWorkspaceId]
: null;

useEffect(() => {
const workspaceHandlers = {
switchToPrevWorkspace: () => {
if (!activeWorkspaceId) return;
const index = findWorkspaceIndex(workspaces, activeWorkspaceId);
if (index > 0) {
setActiveWorkspace(workspaces[index - 1].id);
}
},
switchToNextWorkspace: () => {
if (!activeWorkspaceId) return;
const index = findWorkspaceIndex(workspaces, activeWorkspaceId);
if (index < workspaces.length - 1) {
setActiveWorkspace(workspaces[index + 1].id);
}
},
toggleSidebar,
splitVertical: () => {
if (activeWorkspaceId) {
splitTabVertical(activeWorkspaceId);
}
},
splitHorizontal: () => {
if (activeWorkspaceId) {
splitTabHorizontal(activeWorkspaceId);
}
},
};

const tabHandlers = {
switchToPrevTab: () => {
if (!activeWorkspaceId || !activeTabId) return;
const index = findTabIndex(workspaceTabs, activeTabId);
if (index > 0) {
setActiveTab(activeWorkspaceId, workspaceTabs[index - 1].id);
}
},
switchToNextTab: () => {
if (!activeWorkspaceId || !activeTabId) return;
const index = findTabIndex(workspaceTabs, activeTabId);
if (index < workspaceTabs.length - 1) {
setActiveTab(activeWorkspaceId, workspaceTabs[index + 1].id);
}
},
newTab: () => {
if (activeWorkspaceId) {
addTab(activeWorkspaceId);
}
},
closeTab: () => {
if (activeTabId) {
removeTab(activeTabId);
}
},
reopenClosedTab: () => {
console.log("Reopen closed tab");
},
jumpToTab: (index: number) => {
if (!activeWorkspaceId) return;
const targetTab = workspaceTabs[index - 1];
if (targetTab) {
setActiveTab(activeWorkspaceId, targetTab.id);
}
},
};

const splitPaneHandlers = {
focusPaneLeft: () => console.log("Focus pane left"),
focusPaneRight: () => console.log("Focus pane right"),
focusPaneUp: () => console.log("Focus pane up"),
focusPaneDown: () => console.log("Focus pane down"),
};

const workspaceShortcuts = createWorkspaceShortcuts(workspaceHandlers);
const tabShortcuts = createTabShortcuts(tabHandlers);
const splitPaneShortcuts = createSplitPaneShortcuts(splitPaneHandlers);

const allShortcuts = [
...workspaceShortcuts.shortcuts,
...tabShortcuts.shortcuts,
...splitPaneShortcuts.shortcuts,
];

const handleKeyDown = createShortcutHandler(allShortcuts);
window.addEventListener("keydown", handleKeyDown);

return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [
workspaces,
activeWorkspaceId,
workspaceTabs,
activeTabId,
setActiveWorkspace,
toggleSidebar,
setActiveTab,
addTab,
removeTab,
splitTabVertical,
splitTabHorizontal,
]);
}
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/lib/keyboard-shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface KeyboardShortcut {
key: string;
modifiers?: ModifierKey[];
description: string;
handler: (event: KeyboardEvent) => boolean | void;
handler: (event: KeyboardEvent) => boolean;
}

export interface KeyboardShortcutGroup {
Expand Down
76 changes: 67 additions & 9 deletions apps/desktop/src/renderer/lib/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export interface ShortcutHandlers {
switchToPrevWorkspace: () => void;
switchToNextWorkspace: () => void;
toggleSidebar: () => void;
createSplitView: () => void;
createVerticalSplit: () => void;
splitHorizontal: () => void;
splitVertical: () => void;

// Tab management
switchToPrevTab: () => void;
Expand All @@ -23,6 +23,12 @@ export interface ShortcutHandlers {
reopenClosedTab: () => void;
jumpToTab: (index: number) => void;

// Split pane navigation
focusPaneLeft: () => void;
focusPaneRight: () => void;
focusPaneUp: () => void;
focusPaneDown: () => void;

// Terminal specific
clearTerminal: () => void;
closeTerminal: () => void;
Expand All @@ -34,8 +40,8 @@ export function createWorkspaceShortcuts(
| "switchToPrevWorkspace"
| "switchToNextWorkspace"
| "toggleSidebar"
| "createSplitView"
| "createVerticalSplit"
| "splitVertical"
| "splitHorizontal"
>,
): KeyboardShortcutGroup {
return {
Expand Down Expand Up @@ -74,20 +80,20 @@ export function createWorkspaceShortcuts(
{
key: "d",
modifiers: ["meta"],
description: "Create split view (horizontal)",
description: "Split window vertically",
handler: (event) => {
event.preventDefault();
handlers.createSplitView();
handlers.splitVertical();
return false;
},
},
{
key: "d",
modifiers: ["meta", "shift"],
description: "Create split view (vertical)",
description: "Split window horizontally",
handler: (event) => {
event.preventDefault();
handlers.createVerticalSplit();
handlers.splitHorizontal();
return false;
},
},
Expand Down Expand Up @@ -159,7 +165,6 @@ export function createTabShortcuts(
},
];

// Add Cmd+1-9 shortcuts for jumping to tabs
for (let i = 1; i <= 9; i++) {
shortcuts.push({
key: i.toString(),
Expand All @@ -179,6 +184,59 @@ export function createTabShortcuts(
};
}

export function createSplitPaneShortcuts(
handlers: Pick<
ShortcutHandlers,
"focusPaneLeft" | "focusPaneRight" | "focusPaneUp" | "focusPaneDown"
>,
): KeyboardShortcutGroup {
return {
name: "Split Pane Navigation",
shortcuts: [
{
key: "ArrowLeft",
modifiers: ["meta", "alt"],
description: "Focus left pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneLeft();
return false;
},
},
{
key: "ArrowRight",
modifiers: ["meta", "alt"],
description: "Focus right pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneRight();
return false;
},
},
{
key: "ArrowUp",
modifiers: ["meta", "alt"],
description: "Focus upper pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneUp();
return false;
},
},
{
key: "ArrowDown",
modifiers: ["meta", "alt"],
description: "Focus lower pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneDown();
return false;
},
},
],
};
}
Comment on lines +187 to +238
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Shortcut key conflicts with workspace and tab navigation.

The pane navigation shortcuts conflict with existing shortcuts:

  1. Meta+Alt+ArrowLeft/Right: Used by both workspace switching (lines 51, 61) and pane focus (lines 198, 208)
  2. Meta+Alt+ArrowUp/Down: Used by both tab switching (lines 117, 127) and pane focus (lines 218, 228)

When all shortcuts are registered together in useGlobalShortcuts, only the first matching shortcut will fire, making either workspace/tab navigation or pane navigation inaccessible.

Consider using different modifiers for pane navigation, such as:

  • Option 1: Use Cmd+Opt+Ctrl+Arrows for pane focus (keeping workspace/tab nav as-is)
  • Option 2: Use Cmd+[ / Cmd+] style shortcuts (similar to tmux/vim)
  • Option 3: Make shortcuts contextual - pane navigation only when a split view is active

Apply this diff for Option 1:

 export function createSplitPaneShortcuts(
 	handlers: Pick<
 		ShortcutHandlers,
 		"focusPaneLeft" | "focusPaneRight" | "focusPaneUp" | "focusPaneDown"
 	>,
 ): KeyboardShortcutGroup {
 	return {
 		name: "Split Pane Navigation",
 		shortcuts: [
 			{
 				key: "ArrowLeft",
-				modifiers: ["meta", "alt"],
+				modifiers: ["meta", "alt", "ctrl"],
 				description: "Focus left pane",
 				handler: (event) => {
 					event.preventDefault();
 					handlers.focusPaneLeft();
 					return false;
 				},
 			},
 			{
 				key: "ArrowRight",
-				modifiers: ["meta", "alt"],
+				modifiers: ["meta", "alt", "ctrl"],
 				description: "Focus right pane",
 				handler: (event) => {
 					event.preventDefault();
 					handlers.focusPaneRight();
 					return false;
 				},
 			},
 			{
 				key: "ArrowUp",
-				modifiers: ["meta", "alt"],
+				modifiers: ["meta", "alt", "ctrl"],
 				description: "Focus upper pane",
 				handler: (event) => {
 					event.preventDefault();
 					handlers.focusPaneUp();
 					return false;
 				},
 			},
 			{
 				key: "ArrowDown",
-				modifiers: ["meta", "alt"],
+				modifiers: ["meta", "alt", "ctrl"],
 				description: "Focus lower pane",
 				handler: (event) => {
 					event.preventDefault();
 					handlers.focusPaneDown();
 					return false;
 				},
 			},
 		],
 	};
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function createSplitPaneShortcuts(
handlers: Pick<
ShortcutHandlers,
"focusPaneLeft" | "focusPaneRight" | "focusPaneUp" | "focusPaneDown"
>,
): KeyboardShortcutGroup {
return {
name: "Split Pane Navigation",
shortcuts: [
{
key: "ArrowLeft",
modifiers: ["meta", "alt"],
description: "Focus left pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneLeft();
return false;
},
},
{
key: "ArrowRight",
modifiers: ["meta", "alt"],
description: "Focus right pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneRight();
return false;
},
},
{
key: "ArrowUp",
modifiers: ["meta", "alt"],
description: "Focus upper pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneUp();
return false;
},
},
{
key: "ArrowDown",
modifiers: ["meta", "alt"],
description: "Focus lower pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneDown();
return false;
},
},
],
};
}
export function createSplitPaneShortcuts(
handlers: Pick<
ShortcutHandlers,
"focusPaneLeft" | "focusPaneRight" | "focusPaneUp" | "focusPaneDown"
>,
): KeyboardShortcutGroup {
return {
name: "Split Pane Navigation",
shortcuts: [
{
key: "ArrowLeft",
modifiers: ["meta", "alt", "ctrl"],
description: "Focus left pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneLeft();
return false;
},
},
{
key: "ArrowRight",
modifiers: ["meta", "alt", "ctrl"],
description: "Focus right pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneRight();
return false;
},
},
{
key: "ArrowUp",
modifiers: ["meta", "alt", "ctrl"],
description: "Focus upper pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneUp();
return false;
},
},
{
key: "ArrowDown",
modifiers: ["meta", "alt", "ctrl"],
description: "Focus lower pane",
handler: (event) => {
event.preventDefault();
handlers.focusPaneDown();
return false;
},
},
],
};
}
🤖 Prompt for AI Agents
In apps/desktop/src/renderer/lib/shortcuts.ts around lines 188 to 239, pane
navigation uses the same Meta+Alt+Arrow combinations as workspace/tab navigation
causing conflicts; update each pane-focus shortcut's modifiers array to include
"ctrl" so they become ["meta","alt","ctrl"] (Cmd+Opt+Ctrl+Arrow) for ArrowLeft,
ArrowRight, ArrowUp, and ArrowDown, leaving keys and handlers unchanged so pane
navigation no longer blocks workspace/tab shortcuts.


export function createTerminalShortcuts(
handlers: Pick<ShortcutHandlers, "clearTerminal" | "closeTerminal">,
): KeyboardShortcutGroup {
Expand Down
Loading
Loading