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 }), resolveSplitContainerRoot({ root, focusedPaneId: "right", focusModeEnabled: false }),
).toEqual({ root, usesFallbackStrip: 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; root: SplitNode;
focusedPaneId: string | null; focusedPaneId: string | null;
focusModeEnabled: boolean | undefined; focusModeEnabled: boolean | undefined;
maximizedPaneId?: string | null;
}): { root: SplitNode; usesFallbackStrip: boolean } { }): { root: SplitNode; usesFallbackStrip: boolean } {
if (!input.focusModeEnabled) return { root: input.root, usesFallbackStrip: false }; const isolatedPaneId = input.focusModeEnabled ? input.focusedPaneId : input.maximizedPaneId;
const focusedPane = input.focusedPaneId ? findPane(input.root, input.focusedPaneId) : null; if (!isolatedPaneId) return { root: input.root, usesFallbackStrip: false };
if (!focusedPane) return { root: input.root, usesFallbackStrip: true }; const isolatedPane = findPane(input.root, isolatedPaneId);
return { root: { kind: "pane", pane: focusedPane }, usesFallbackStrip: false }; 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 { function findPane(node: SplitNode, paneId: string): SplitPane | null {
@@ -76,6 +76,7 @@ import {
} from "@/screens/workspace/workspace-tab-presentation"; } from "@/screens/workspace/workspace-tab-presentation";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
import { import {
findPaneById,
useWorkspaceLayoutStore, useWorkspaceLayoutStore,
type SplitNode, type SplitNode,
type SplitPane, type SplitPane,
@@ -85,6 +86,8 @@ import type { WorkspaceTab } from "@/workspace-tabs/model";
import { RenderProfile } from "@/utils/render-profiler"; import { RenderProfile } from "@/utils/render-profiler";
import { workspaceTabTargetsEqual } from "@/workspace-tabs/identity"; import { workspaceTabTargetsEqual } from "@/workspace-tabs/identity";
import { isNative } from "@/constants/platform"; import { isNative } from "@/constants/platform";
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
interface SplitContainerProps { interface SplitContainerProps {
layout: WorkspaceLayout; layout: WorkspaceLayout;
@@ -171,6 +174,9 @@ interface SplitNodeViewProps extends Omit<SplitContainerProps, "layout" | "onMov
node: SplitNode; node: SplitNode;
uiTabs: WorkspaceTab[]; uiTabs: WorkspaceTab[];
focusedPaneId: string | null; focusedPaneId: string | null;
explorerPaneId: string | null;
maximizedPaneId: string | null;
onTogglePaneMaximized: (paneId: string) => void;
activeDragTabId: string | null; activeDragTabId: string | null;
showDropZones: boolean; showDropZones: boolean;
dropPreview: SplitDropZoneHover | null; dropPreview: SplitDropZoneHover | null;
@@ -409,6 +415,61 @@ export function SplitContainer({
const [activeDragTabId, setActiveDragTabId] = useState<string | null>(null); const [activeDragTabId, setActiveDragTabId] = useState<string | null>(null);
const [dropPreview, setDropPreview] = useState<SplitDropZoneHover | null>(null); const [dropPreview, setDropPreview] = useState<SplitDropZoneHover | null>(null);
const [tabDropPreview, setTabDropPreview] = useState<TabDropPreview | 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( const sensors = useSensors(
useSensor(PointerSensor, { useSensor(PointerSensor, {
@@ -428,8 +489,9 @@ export function SplitContainer({
root: layout.root, root: layout.root,
focusedPaneId: layout.focusedPaneId, focusedPaneId: layout.focusedPaneId,
focusModeEnabled, focusModeEnabled,
maximizedPaneId,
}), }),
[focusModeEnabled, layout.focusedPaneId, layout.root], [focusModeEnabled, layout.focusedPaneId, layout.root, maximizedPaneId],
); );
const renderRoot = useMemo(() => wrapRootPaneForStableMount(splitRoot.root), [splitRoot.root]); const renderRoot = useMemo(() => wrapRootPaneForStableMount(splitRoot.root), [splitRoot.root]);
@@ -592,6 +654,9 @@ export function SplitContainer({
workspaceKey={workspaceKey} workspaceKey={workspaceKey}
uiTabs={uiTabs} uiTabs={uiTabs}
focusedPaneId={layout.focusedPaneId} focusedPaneId={layout.focusedPaneId}
explorerPaneId={explorerPaneId}
maximizedPaneId={maximizedPaneId}
onTogglePaneMaximized={handleTogglePaneMaximized}
normalizedServerId={normalizedServerId} normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId} normalizedWorkspaceId={normalizedWorkspaceId}
isWorkspaceFocused={isWorkspaceFocused} isWorkspaceFocused={isWorkspaceFocused}
@@ -798,6 +863,9 @@ function SplitNodeView({
workspaceKey, workspaceKey,
uiTabs, uiTabs,
focusedPaneId, focusedPaneId,
explorerPaneId,
maximizedPaneId,
onTogglePaneMaximized,
normalizedServerId, normalizedServerId,
normalizedWorkspaceId, normalizedWorkspaceId,
isWorkspaceFocused, isWorkspaceFocused,
@@ -874,6 +942,9 @@ function SplitNodeView({
pane={node.pane} pane={node.pane}
uiTabs={uiTabs} uiTabs={uiTabs}
isFocused={node.pane.id === focusedPaneId} isFocused={node.pane.id === focusedPaneId}
explorerPaneId={explorerPaneId}
maximizedPaneId={maximizedPaneId}
onTogglePaneMaximized={onTogglePaneMaximized}
normalizedServerId={normalizedServerId} normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId} normalizedWorkspaceId={normalizedWorkspaceId}
isWorkspaceFocused={isWorkspaceFocused} isWorkspaceFocused={isWorkspaceFocused}
@@ -923,6 +994,9 @@ function SplitNodeView({
workspaceKey={workspaceKey} workspaceKey={workspaceKey}
uiTabs={uiTabs} uiTabs={uiTabs}
focusedPaneId={focusedPaneId} focusedPaneId={focusedPaneId}
explorerPaneId={explorerPaneId}
maximizedPaneId={maximizedPaneId}
onTogglePaneMaximized={onTogglePaneMaximized}
normalizedServerId={normalizedServerId} normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId} normalizedWorkspaceId={normalizedWorkspaceId}
isWorkspaceFocused={isWorkspaceFocused} isWorkspaceFocused={isWorkspaceFocused}
@@ -1014,6 +1088,9 @@ function SplitPaneView({
showDropZones, showDropZones,
dropPreview, dropPreview,
tabDropPreview, tabDropPreview,
explorerPaneId,
maximizedPaneId,
onTogglePaneMaximized,
focusModeEnabled, focusModeEnabled,
onExitFocusMode, onExitFocusMode,
}: SplitPaneViewProps) { }: SplitPaneViewProps) {
@@ -1114,6 +1191,10 @@ function SplitPaneView({
}, },
[onReorderTabsInPane, paneId], [onReorderTabsInPane, paneId],
); );
const handleToggleMaximized = useCallback(
() => onTogglePaneMaximized(paneId),
[onTogglePaneMaximized, paneId],
);
return ( return (
<RenderProfile id={`SplitPaneView:${pane.id}`}> <RenderProfile id={`SplitPaneView:${pane.id}`}>
<View ref={paneRef} collapsable={false} style={styles.pane}> <View ref={paneRef} collapsable={false} style={styles.pane}>
@@ -1121,7 +1202,7 @@ function SplitPaneView({
<TitlebarDragRegion /> <TitlebarDragRegion />
<WorkspaceDesktopTabsRow <WorkspaceDesktopTabsRow
paneId={pane.id} paneId={pane.id}
isFocused={isFocused} isFocused={isFocused && isWorkspaceFocused}
tabs={desktopTabRowItems} tabs={desktopTabRowItems}
normalizedServerId={normalizedServerId} normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId} normalizedWorkspaceId={normalizedWorkspaceId}
@@ -1147,6 +1228,9 @@ function SplitPaneView({
tabDropPreviewIndex={ tabDropPreviewIndex={
tabDropPreview?.paneId === pane.id ? tabDropPreview.indicatorIndex : null tabDropPreview?.paneId === pane.id ? tabDropPreview.indicatorIndex : null
} }
showPaneMaximizeAction={pane.id === explorerPaneId && !focusModeEnabled}
paneMaximized={pane.id === maximizedPaneId}
onTogglePaneMaximized={handleToggleMaximized}
focusModeEnabled={Boolean(focusModeEnabled)} focusModeEnabled={Boolean(focusModeEnabled)}
onExitFocusMode={onExitFocusMode} onExitFocusMode={onExitFocusMode}
/> />