feat(app): maximize the Explorer pane

Maximization isolates the existing Explorer split without entering workspace focus mode, and automatically restores the split if that pane disappears.
This commit is contained in:
Mohamed Boudra
2026-08-17 21:15:16 +02:00
parent 60bafad3ee
commit 6ea1067bff
3 changed files with 116 additions and 6 deletions
@@ -34,4 +34,26 @@ describe("split focus root", () => {
resolveSplitContainerRoot({ root, focusedPaneId: "right", focusModeEnabled: false }),
).toEqual({ root, usesFallbackStrip: false });
});
it("renders only a maximized pane without entering focus mode", () => {
expect(
resolveSplitContainerRoot({
root,
focusedPaneId: "left",
focusModeEnabled: false,
maximizedPaneId: "right",
}),
).toEqual({ root: pane("right"), usesFallbackStrip: false });
});
it("keeps normal splits when the maximized pane is unavailable", () => {
expect(
resolveSplitContainerRoot({
root,
focusedPaneId: "left",
focusModeEnabled: false,
maximizedPaneId: "missing",
}),
).toEqual({ root, usesFallbackStrip: false });
});
});
@@ -4,11 +4,15 @@ export function resolveSplitContainerRoot(input: {
root: SplitNode;
focusedPaneId: string | null;
focusModeEnabled: boolean | undefined;
maximizedPaneId?: string | null;
}): { root: SplitNode; usesFallbackStrip: boolean } {
if (!input.focusModeEnabled) return { root: input.root, usesFallbackStrip: false };
const focusedPane = input.focusedPaneId ? findPane(input.root, input.focusedPaneId) : null;
if (!focusedPane) return { root: input.root, usesFallbackStrip: true };
return { root: { kind: "pane", pane: focusedPane }, usesFallbackStrip: false };
const isolatedPaneId = input.focusModeEnabled ? input.focusedPaneId : input.maximizedPaneId;
if (!isolatedPaneId) return { root: input.root, usesFallbackStrip: false };
const isolatedPane = findPane(input.root, isolatedPaneId);
if (!isolatedPane || isolatedPane.hidden === true) {
return { root: input.root, usesFallbackStrip: Boolean(input.focusModeEnabled) };
}
return { root: { kind: "pane", pane: isolatedPane }, usesFallbackStrip: false };
}
function findPane(node: SplitNode, paneId: string): SplitPane | null {
@@ -76,6 +76,7 @@ import {
} from "@/screens/workspace/workspace-tab-presentation";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
import {
findPaneById,
useWorkspaceLayoutStore,
type SplitNode,
type SplitPane,
@@ -85,6 +86,8 @@ import type { WorkspaceTab } from "@/workspace-tabs/model";
import { RenderProfile } from "@/utils/render-profiler";
import { workspaceTabTargetsEqual } from "@/workspace-tabs/identity";
import { isNative } from "@/constants/platform";
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
interface SplitContainerProps {
layout: WorkspaceLayout;
@@ -171,6 +174,9 @@ interface SplitNodeViewProps extends Omit<SplitContainerProps, "layout" | "onMov
node: SplitNode;
uiTabs: WorkspaceTab[];
focusedPaneId: string | null;
explorerPaneId: string | null;
maximizedPaneId: string | null;
onTogglePaneMaximized: (paneId: string) => void;
activeDragTabId: string | null;
showDropZones: boolean;
dropPreview: SplitDropZoneHover | null;
@@ -409,6 +415,61 @@ export function SplitContainer({
const [activeDragTabId, setActiveDragTabId] = useState<string | null>(null);
const [dropPreview, setDropPreview] = useState<SplitDropZoneHover | null>(null);
const [tabDropPreview, setTabDropPreview] = useState<TabDropPreview | null>(null);
const [maximizedPane, setMaximizedPane] = useState<{
workspaceKey: string;
paneId: string;
} | null>(null);
const maximizedPaneId =
maximizedPane?.workspaceKey === workspaceKey ? maximizedPane.paneId : null;
const explorerPaneId = useWorkspaceLayoutStore(
(state) => state.explorerPaneIdByWorkspace[workspaceKey] ?? null,
);
useEffect(() => {
if (!maximizedPaneId) {
return;
}
const isolatedPane = findPaneById(layout.root, maximizedPaneId);
if (focusModeEnabled || !isolatedPane || isolatedPane.hidden === true) {
setMaximizedPane(null);
}
}, [focusModeEnabled, layout.root, maximizedPaneId]);
const handleTogglePaneMaximized = useCallback(
(paneId: string) => {
setMaximizedPane((currentPane) =>
currentPane?.workspaceKey === workspaceKey && currentPane.paneId === paneId
? null
: { workspaceKey, paneId },
);
},
[workspaceKey],
);
const handleExplorerMaximizeAction = useCallback(
(action: KeyboardActionDefinition): boolean => {
if (action.id !== "workspace.explorer.maximize.toggle") {
return false;
}
const explorerPane = findPaneById(layout.root, explorerPaneId);
if (focusModeEnabled || !explorerPane || explorerPane.hidden === true) {
return true;
}
onFocusPane(explorerPane.id);
handleTogglePaneMaximized(explorerPane.id);
return true;
},
[explorerPaneId, focusModeEnabled, handleTogglePaneMaximized, layout.root, onFocusPane],
);
useKeyboardActionHandler({
handlerId: `workspace-explorer-maximize:${workspaceKey}`,
actions: ["workspace.explorer.maximize.toggle"] as const,
enabled: isWorkspaceFocused,
priority: 100,
isActive: () => true,
handle: handleExplorerMaximizeAction,
});
const sensors = useSensors(
useSensor(PointerSensor, {
@@ -428,8 +489,9 @@ export function SplitContainer({
root: layout.root,
focusedPaneId: layout.focusedPaneId,
focusModeEnabled,
maximizedPaneId,
}),
[focusModeEnabled, layout.focusedPaneId, layout.root],
[focusModeEnabled, layout.focusedPaneId, layout.root, maximizedPaneId],
);
const renderRoot = useMemo(() => wrapRootPaneForStableMount(splitRoot.root), [splitRoot.root]);
@@ -592,6 +654,9 @@ export function SplitContainer({
workspaceKey={workspaceKey}
uiTabs={uiTabs}
focusedPaneId={layout.focusedPaneId}
explorerPaneId={explorerPaneId}
maximizedPaneId={maximizedPaneId}
onTogglePaneMaximized={handleTogglePaneMaximized}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
isWorkspaceFocused={isWorkspaceFocused}
@@ -798,6 +863,9 @@ function SplitNodeView({
workspaceKey,
uiTabs,
focusedPaneId,
explorerPaneId,
maximizedPaneId,
onTogglePaneMaximized,
normalizedServerId,
normalizedWorkspaceId,
isWorkspaceFocused,
@@ -874,6 +942,9 @@ function SplitNodeView({
pane={node.pane}
uiTabs={uiTabs}
isFocused={node.pane.id === focusedPaneId}
explorerPaneId={explorerPaneId}
maximizedPaneId={maximizedPaneId}
onTogglePaneMaximized={onTogglePaneMaximized}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
isWorkspaceFocused={isWorkspaceFocused}
@@ -923,6 +994,9 @@ function SplitNodeView({
workspaceKey={workspaceKey}
uiTabs={uiTabs}
focusedPaneId={focusedPaneId}
explorerPaneId={explorerPaneId}
maximizedPaneId={maximizedPaneId}
onTogglePaneMaximized={onTogglePaneMaximized}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
isWorkspaceFocused={isWorkspaceFocused}
@@ -1014,6 +1088,9 @@ function SplitPaneView({
showDropZones,
dropPreview,
tabDropPreview,
explorerPaneId,
maximizedPaneId,
onTogglePaneMaximized,
focusModeEnabled,
onExitFocusMode,
}: SplitPaneViewProps) {
@@ -1114,6 +1191,10 @@ function SplitPaneView({
},
[onReorderTabsInPane, paneId],
);
const handleToggleMaximized = useCallback(
() => onTogglePaneMaximized(paneId),
[onTogglePaneMaximized, paneId],
);
return (
<RenderProfile id={`SplitPaneView:${pane.id}`}>
<View ref={paneRef} collapsable={false} style={styles.pane}>
@@ -1121,7 +1202,7 @@ function SplitPaneView({
<TitlebarDragRegion />
<WorkspaceDesktopTabsRow
paneId={pane.id}
isFocused={isFocused}
isFocused={isFocused && isWorkspaceFocused}
tabs={desktopTabRowItems}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
@@ -1147,6 +1228,9 @@ function SplitPaneView({
tabDropPreviewIndex={
tabDropPreview?.paneId === pane.id ? tabDropPreview.indicatorIndex : null
}
showPaneMaximizeAction={pane.id === explorerPaneId && !focusModeEnabled}
paneMaximized={pane.id === maximizedPaneId}
onTogglePaneMaximized={handleToggleMaximized}
focusModeEnabled={Boolean(focusModeEnabled)}
onExitFocusMode={onExitFocusMode}
/>