diff --git a/packages/app/src/components/split-container-focus.test.ts b/packages/app/src/components/split-container-focus.test.ts index 5e67d784a..1880f924f 100644 --- a/packages/app/src/components/split-container-focus.test.ts +++ b/packages/app/src/components/split-container-focus.test.ts @@ -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 }); + }); }); diff --git a/packages/app/src/components/split-container-focus.ts b/packages/app/src/components/split-container-focus.ts index e16f4571d..aa7ef6db3 100644 --- a/packages/app/src/components/split-container-focus.ts +++ b/packages/app/src/components/split-container-focus.ts @@ -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 { diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 6053d4f4d..c25e8c106 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -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 void; activeDragTabId: string | null; showDropZones: boolean; dropPreview: SplitDropZoneHover | null; @@ -409,6 +415,61 @@ export function SplitContainer({ const [activeDragTabId, setActiveDragTabId] = useState(null); const [dropPreview, setDropPreview] = useState(null); const [tabDropPreview, setTabDropPreview] = useState(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 ( @@ -1121,7 +1202,7 @@ function SplitPaneView({