feat(app): add focus mode (Cmd+Shift+F) to show only the active pane

Hides ScreenHeader, LeftSidebar, and ExplorerSidebar on desktop,
replacing the split layout with the single focused pane. Respects
macOS traffic light padding in the tab bar when active. Shortcut
is restricted to workspace screens. Also nudges traffic light
buttons up 3px globally.
This commit is contained in:
Mohamed Boudra
2026-03-23 13:05:07 +07:00
parent b9b1601bb8
commit cded2c52ab
8 changed files with 107 additions and 14 deletions
+4 -1
View File
@@ -265,6 +265,8 @@ function AppContainer({
const toggleAgentList = usePanelStore((state) => state.toggleAgentList);
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
const toggleBothSidebars = usePanelStore((state) => state.toggleBothSidebars);
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
@@ -276,6 +278,7 @@ function AppContainer({
selectedAgentId,
toggleFileExplorer,
toggleBothSidebars,
toggleFocusMode,
});
const containerStyle = useMemo(
@@ -286,7 +289,7 @@ function AppContainer({
const content = (
<View style={containerStyle}>
<View style={rowStyle}>
{!isMobile && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
{!isMobile && chromeEnabled && !isFocusModeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
<View style={flexStyle}>{children}</View>
</View>
{isMobile && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
@@ -31,6 +31,8 @@ import { Platform, View, Text } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { ResizeHandle } from "@/components/resize-handle";
import { shouldFocusPaneFromEventTarget } from "@/components/split-container-pane-focus";
import { usePanelStore } from "@/stores/panel-store";
import { useTrafficLightPadding } from "@/utils/desktop-window";
import {
computeTabDropPreview,
type TabDropPreview,
@@ -109,6 +111,7 @@ interface SplitContainerProps {
onResizeSplit: (groupId: string, sizes: number[]) => void;
onReorderTabsInPane: (paneId: string, tabIds: string[]) => void;
renderPaneEmptyState?: () => ReactNode;
focusModeEnabled?: boolean;
}
interface WorkspaceTabDragData {
@@ -262,6 +265,7 @@ export function SplitContainer({
onResizeSplit,
onReorderTabsInPane,
renderPaneEmptyState = () => null,
focusModeEnabled,
}: SplitContainerProps) {
const [activeDragTabId, setActiveDragTabId] = useState<string | null>(null);
const [dropPreview, setDropPreview] = useState<SplitDropZoneHover | null>(null);
@@ -280,6 +284,17 @@ export function SplitContainer({
const panesById = useMemo(() => collectPanesById(layout.root), [layout.root]);
const effectiveRoot = useMemo(() => {
if (!focusModeEnabled) {
return layout.root;
}
const focusedPane = panesById.get(layout.focusedPaneId);
if (!focusedPane) {
return layout.root;
}
return { kind: "pane" as const, pane: focusedPane };
}, [focusModeEnabled, layout.root, layout.focusedPaneId, panesById]);
const handleDragStart = useCallback((event: DragStartEvent) => {
const data = event.active.data.current as WorkspaceTabDragData | undefined;
if (data?.kind !== "workspace-tab") {
@@ -491,7 +506,7 @@ export function SplitContainer({
onDragEnd={handleDragEnd}
>
<SplitNodeView
node={layout.root}
node={effectiveRoot}
workspaceKey={workspaceKey}
uiTabs={uiTabs}
focusedPaneId={layout.focusedPaneId}
@@ -781,6 +796,8 @@ function SplitPaneView({
const { theme } = useUnistyles();
const paneRef = useRef<View | null>(null);
const stableOnFocusPane = useStableEvent(onFocusPane);
const isFocusModeEnabled = usePanelStore((s) => s.desktop.focusModeEnabled);
const trafficLightPadding = useTrafficLightPadding();
const paneState = useMemo(
() =>
deriveWorkspacePaneState({
@@ -852,7 +869,13 @@ function SplitPaneView({
return (
<View ref={paneRef} collapsable={false} style={styles.pane}>
<View style={styles.paneTabs}>
<View
style={[
styles.paneTabs,
isFocusModeEnabled &&
trafficLightPadding.left > 0 && { paddingLeft: trafficLightPadding.left },
]}
>
<WorkspaceDesktopTabsRow
paneId={pane.id}
isFocused={isFocused}
@@ -34,6 +34,7 @@ export function useKeyboardShortcuts({
selectedAgentId,
toggleFileExplorer,
toggleBothSidebars,
toggleFocusMode,
}: {
enabled: boolean;
isMobile: boolean;
@@ -41,6 +42,7 @@ export function useKeyboardShortcuts({
selectedAgentId?: string;
toggleFileExplorer?: () => void;
toggleBothSidebars?: () => void;
toggleFocusMode?: () => void;
}) {
const pathname = usePathname();
const hosts = useHosts();
@@ -250,6 +252,11 @@ export function useKeyboardShortcuts({
}
toggleFileExplorer();
return true;
case "view.toggle.focus":
if (toggleFocusMode) {
toggleFocusMode();
}
return true;
case "command-center.toggle": {
const store = useKeyboardShortcutsStore.getState();
if (!store.commandCenterOpen) {
@@ -404,5 +411,6 @@ export function useKeyboardShortcuts({
selectedAgentId,
toggleAgentList,
toggleFileExplorer,
toggleFocusMode,
]);
}
+1
View File
@@ -41,6 +41,7 @@ export type KeyboardActionId =
| "workspace.terminal.new"
| "worktree.new"
| "worktree.archive"
| "view.toggle.focus"
| "message-input.action";
export type KeyboardShortcutPayload =
@@ -718,6 +718,32 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
},
},
// --- Focus mode ---
{
id: "view-toggle-focus-cmd-shift-f-mac",
action: "view.toggle.focus",
combo: "Cmd+Shift+F",
when: { mac: true, hasSelectedAgent: true, commandCenter: false },
help: {
id: "toggle-focus",
section: "panels",
label: "Toggle focus mode",
keys: ["mod", "shift", "F"],
},
},
{
id: "view-toggle-focus-ctrl-shift-f-non-mac",
action: "view.toggle.focus",
combo: "Ctrl+Shift+F",
when: { mac: false, hasSelectedAgent: true, commandCenter: false, terminal: false },
help: {
id: "toggle-focus",
section: "panels",
label: "Toggle focus mode",
keys: ["mod", "shift", "F"],
},
},
// --- Message input ---
{
id: "message-input-voice-toggle-cmd-shift-d-mac",
@@ -550,6 +550,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0;
const toast = useToast();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
const normalizedWorkspaceId =
@@ -1949,6 +1950,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
) : null}
<View style={styles.threePaneRow}>
<View style={styles.centerColumn}>
{(!isFocusModeEnabled || isMobile) && (
<ScreenHeader
left={
<>
@@ -2135,6 +2137,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
</View>
}
/>
)}
{isMobile ? (
<MobileWorkspaceTabSwitcher
@@ -2165,6 +2168,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
{workspaceLayout && persistenceKey ? (
<SplitContainer
layout={workspaceLayout}
focusModeEnabled={isFocusModeEnabled && !isMobile}
workspaceKey={persistenceKey}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
@@ -2200,13 +2204,15 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
</View>
</View>
<ExplorerSidebar
serverId={normalizedServerId}
workspaceId={normalizedWorkspaceId}
workspaceRoot={normalizedWorkspaceId}
isGit={isGitCheckout}
onOpenFile={handleOpenFileFromExplorer}
/>
{(!isFocusModeEnabled || isMobile) && (
<ExplorerSidebar
serverId={normalizedServerId}
workspaceId={normalizedWorkspaceId}
workspaceRoot={normalizedWorkspaceId}
isGit={isGitCheckout}
onOpenFile={handleOpenFileFromExplorer}
/>
)}
</View>
</View>
);
+29 -3
View File
@@ -33,6 +33,7 @@ type MobilePanelView = "agent" | "agent-list" | "file-explorer";
interface DesktopSidebarState {
agentListOpen: boolean;
fileExplorerOpen: boolean;
focusModeEnabled: boolean;
}
export type SortOption = "name" | "modified" | "size";
@@ -79,6 +80,7 @@ interface PanelState {
toggleAgentList: () => void;
toggleFileExplorer: () => void;
toggleBothSidebars: () => void;
toggleFocusMode: () => void;
// File explorer settings actions
setExplorerTab: (tab: ExplorerTab) => void;
@@ -134,6 +136,7 @@ export const usePanelStore = create<PanelState>()(
desktop: {
agentListOpen: DEFAULT_DESKTOP_OPEN,
fileExplorerOpen: false,
focusModeEnabled: false,
},
// File explorer defaults
@@ -175,6 +178,7 @@ export const usePanelStore = create<PanelState>()(
// On desktop, closing depends on which panel triggered it
// This is called when closing via gesture/backdrop, so we close the currently active mobile panel
desktop: {
...state.desktop,
agentListOpen: state.mobileView === "agent-list" ? false : state.desktop.agentListOpen,
fileExplorerOpen:
state.mobileView === "file-explorer" ? false : state.desktop.fileExplorerOpen,
@@ -217,6 +221,11 @@ export const usePanelStore = create<PanelState>()(
return nextState;
}),
toggleFocusMode: () =>
set((state) => ({
desktop: { ...state.desktop, focusModeEnabled: !state.desktop.focusModeEnabled },
})),
toggleBothSidebars: () =>
set((state) => {
// If either sidebar is open, close both. Otherwise, open both.
@@ -224,13 +233,13 @@ export const usePanelStore = create<PanelState>()(
if (anyOpen) {
return {
mobileView: "agent" as MobilePanelView,
desktop: { agentListOpen: false, fileExplorerOpen: false },
desktop: { ...state.desktop, agentListOpen: false, fileExplorerOpen: false },
};
}
const resolvedTab = resolveExplorerTabFromActiveCheckout(state);
return {
mobileView: "agent" as MobilePanelView,
desktop: { agentListOpen: true, fileExplorerOpen: true },
desktop: { ...state.desktop, agentListOpen: true, fileExplorerOpen: true },
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
@@ -286,7 +295,7 @@ export const usePanelStore = create<PanelState>()(
}),
{
name: "panel-state",
version: 6,
version: 8,
storage: createJSONStorage(() => AsyncStorage),
migrate: (persistedState, version) => {
const state = persistedState as Partial<PanelState> & Record<string, unknown>;
@@ -341,6 +350,23 @@ export const usePanelStore = create<PanelState>()(
state.explorerTabByCheckout = next;
}
if (version < 8) {
const desktop = state.desktop as Record<string, unknown> | undefined;
if (desktop) {
if ("zoomed" in desktop) {
desktop.focusModeEnabled = desktop.zoomed;
delete desktop.zoomed;
}
if ("focused" in desktop) {
desktop.focusModeEnabled = desktop.focused;
delete desktop.focused;
}
if (typeof desktop.focusModeEnabled !== "boolean") {
desktop.focusModeEnabled = false;
}
}
}
if (version < 6 || typeof state.sidebarWidth !== "number") {
state.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
}
+1 -1
View File
@@ -90,7 +90,7 @@ async function createMainWindow(): Promise<void> {
show: false,
...(iconPath ? { icon: iconPath } : {}),
titleBarStyle: isMac ? "hidden" : "default",
trafficLightPosition: isMac ? { x: 16, y: 17 } : undefined,
trafficLightPosition: isMac ? { x: 16, y: 14 } : undefined,
webPreferences: {
preload: getPreloadPath(),
contextIsolation: true,