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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { trpc } from "renderer/lib/trpc";
import { useTabsStore } from "renderer/stores/tabs/store";
import { getNextPaneId, getPreviousPaneId } from "renderer/stores/tabs/utils";
import { HOTKEYS } from "shared/hotkeys";
import { ContentView } from "./ContentView";
import { ResizableSidebar } from "./ResizableSidebar";
Expand All @@ -16,6 +17,7 @@ export function WorkspaceView() {
const addTab = useTabsStore((s) => s.addTab);
const setActiveTab = useTabsStore((s) => s.setActiveTab);
const removePane = useTabsStore((s) => s.removePane);
const setFocusedPane = useTabsStore((s) => s.setFocusedPane);

const tabs = useMemo(
() =>
Expand All @@ -29,6 +31,12 @@ export function WorkspaceView() {
? activeTabIds[activeWorkspaceId]
: null;

// Get the active tab object for layout access
const activeTab = useMemo(
() => (activeTabId ? tabs.find((t) => t.id === activeTabId) : null),
[activeTabId, tabs],
);
Comment thread
AviPeltz marked this conversation as resolved.

// Get focused pane ID for the active tab
const focusedPaneId = activeTabId ? focusedPaneIds[activeTabId] : null;

Expand Down Expand Up @@ -63,6 +71,23 @@ export function WorkspaceView() {
}
}, [activeWorkspaceId, activeTabId, tabs, setActiveTab]);

// Switch between panes within a tab (⌘+⌥+Left/Right)
useHotkeys(HOTKEYS.PREV_PANE.keys, () => {
if (!activeTabId || !activeTab?.layout || !focusedPaneId) return;
const prevPaneId = getPreviousPaneId(activeTab.layout, focusedPaneId);
if (prevPaneId) {
setFocusedPane(activeTabId, prevPaneId);
}
}, [activeTabId, activeTab?.layout, focusedPaneId, setFocusedPane]);

useHotkeys(HOTKEYS.NEXT_PANE.keys, () => {
if (!activeTabId || !activeTab?.layout || !focusedPaneId) return;
const nextPaneId = getNextPaneId(activeTab.layout, focusedPaneId);
if (nextPaneId) {
setFocusedPane(activeTabId, nextPaneId);
}
}, [activeTabId, activeTab?.layout, focusedPaneId, setFocusedPane]);

// Open in last used app shortcut
const { data: lastUsedApp = "cursor" } =
trpc.settings.getLastUsedApp.useQuery();
Expand Down
59 changes: 58 additions & 1 deletion apps/desktop/src/renderer/stores/tabs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,25 @@ export const getTabDisplayName = (tab: Tab): string => {
};

/**
* Extracts all pane IDs from a mosaic layout tree
* Extracts all pane IDs from a mosaic layout tree in visual navigation order:
* left-to-right, top-to-bottom.
*
* For react-mosaic layouts:
* - direction: "row" = horizontal split (first is left, second is right)
* - direction: "column" = vertical split (first is top, second is bottom)
*
* This traversal visits `first` before `second` at each node, which produces
* left-to-right ordering for horizontal splits and top-to-bottom for vertical splits.
*
* Example layout:
* ```
* ┌───────┬───────┐
* │ A │ B │ (row split: first=A, second=B)
* ├───────┼───────┤
* │ C │ D │ (row split: first=C, second=D)
* └───────┴───────┘
* ```
* If the top row is `first` in a column split, order would be: [A, B, C, D]
*/
export const extractPaneIdsFromLayout = (
layout: MosaicNode<string>,
Expand All @@ -32,6 +50,9 @@ export const extractPaneIdsFromLayout = (
];
};

/** Alias for extractPaneIdsFromLayout emphasizing the visual ordering contract */
export const getPaneIdsInVisualOrder = extractPaneIdsFromLayout;

/**
* Options for creating a pane with preset configuration
*/
Expand Down Expand Up @@ -208,6 +229,42 @@ export const getFirstPaneId = (layout: MosaicNode<string>): string => {
return getFirstPaneId(layout.first);
};

/**
* Gets the next pane ID in visual order (left-to-right, top-to-bottom),
* wrapping around to the first if at the end.
*/
export const getNextPaneId = (
layout: MosaicNode<string>,
currentPaneId: string,
): string | null => {
const paneIds = getPaneIdsInVisualOrder(layout);
if (paneIds.length <= 1) return null;

const currentIndex = paneIds.indexOf(currentPaneId);
if (currentIndex === -1) return paneIds[0];

const nextIndex = (currentIndex + 1) % paneIds.length;
return paneIds[nextIndex];
};

/**
* Gets the previous pane ID in visual order (right-to-left, bottom-to-top),
* wrapping around to the last if at the beginning.
*/
export const getPreviousPaneId = (
layout: MosaicNode<string>,
currentPaneId: string,
): string | null => {
const paneIds = getPaneIdsInVisualOrder(layout);
if (paneIds.length <= 1) return null;

const currentIndex = paneIds.indexOf(currentPaneId);
if (currentIndex === -1) return paneIds[paneIds.length - 1];

const prevIndex = (currentIndex - 1 + paneIds.length) % paneIds.length;
return paneIds[prevIndex];
};
Comment thread
AviPeltz marked this conversation as resolved.

/**
* Finds the path to a specific pane ID in a mosaic layout
* Returns the path as an array of MosaicBranch ("first" | "second"), or null if not found
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/shared/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,18 @@ export const HOTKEYS = {
label: "Next Terminal",
category: "Terminal",
}),
PREV_PANE: hotkey({
keys: "meta+alt+left",
label: "Previous Pane",
category: "Terminal",
description: "Focus the previous pane in the current tab",
}),
NEXT_PANE: hotkey({
keys: "meta+alt+right",
label: "Next Pane",
category: "Terminal",
description: "Focus the next pane in the current tab",
}),

// Window
NEW_WINDOW: hotkey({
Expand Down
Loading