fix(app): retain Explorer tab state

This commit is contained in:
Mohamed Boudra
2026-08-17 21:15:16 +02:00
parent b97993f866
commit a54b6c2020
3 changed files with 89 additions and 32 deletions
@@ -17,13 +17,19 @@ import { formatPrTabLabel, PullRequestTabIcon } from "@/git/pull-request-panel";
import { usePanelStore, selectIsFileExplorerOpen, type ExplorerTab } from "@/stores/panel-store"; import { usePanelStore, selectIsFileExplorerOpen, type ExplorerTab } from "@/stores/panel-store";
import { useCloseFileExplorerGesture } from "@/mobile-panels/gestures"; import { useCloseFileExplorerGesture } from "@/mobile-panels/gestures";
import { MobilePanelOverlay } from "@/mobile-panels/presentation"; import { MobilePanelOverlay } from "@/mobile-panels/presentation";
import { HEADER_INNER_HEIGHT } from "@/constants/layout"; import {
HEADER_INNER_HEIGHT,
HEADER_INNER_HEIGHT_MOBILE,
HEADER_TOP_PADDING_MOBILE,
} from "@/constants/layout";
import { ChangesSurface } from "@/git/diff-pane"; import { ChangesSurface } from "@/git/diff-pane";
import { FileExplorerPane } from "./file-explorer-pane"; import { FileExplorerPane } from "./file-explorer-pane";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { shouldUseCompactExplorerKeyboardPadding } from "@/hooks/keyboard-shift-policy";
import { useHasOwnedWindowChromeObstruction, WindowChromeSafeArea } from "@/utils/desktop-window"; import { useHasOwnedWindowChromeObstruction, WindowChromeSafeArea } from "@/utils/desktop-window";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { RetainedPanelActivity } from "@/components/retained-panel"; import { RetainedPanel, RetainedPanelActivity } from "@/components/retained-panel";
import { useMountedTabSet } from "@/screens/workspace/use-mounted-tab-set";
import { SidebarResizeHandle } from "@/components/sidebar-resize-handle"; import { SidebarResizeHandle } from "@/components/sidebar-resize-handle";
import { resolveDesktopExplorerWidth } from "@/components/desktop-sidebar-layout"; import { resolveDesktopExplorerWidth } from "@/components/desktop-sidebar-layout";
import { import {
@@ -82,9 +88,10 @@ export function CompactExplorerSidebar({
workspaceRoot, workspaceRoot,
isGit, isGit,
}); });
const usePanelKeyboardPadding = shouldUseCompactExplorerKeyboardPadding({ isGit, explorerTab });
const { style: mobileKeyboardInsetStyle } = useKeyboardShiftStyle({ const { style: mobileKeyboardInsetStyle } = useKeyboardShiftStyle({
mode: "padding", mode: "padding",
enabled: true, enabled: usePanelKeyboardPadding,
}); });
const { gesture: closeGesture } = useCloseFileExplorerGesture(); const { gesture: closeGesture } = useCloseFileExplorerGesture();
@@ -104,12 +111,19 @@ export function CompactExplorerSidebar({
const mobileSidebarStyle = useMemo( const mobileSidebarStyle = useMemo(
() => [ () => [
{ {
paddingTop: insets.top, paddingTop: insets.top + HEADER_TOP_PADDING_MOBILE,
paddingBottom: usePanelKeyboardPadding ? 0 : insets.bottom,
backgroundColor: theme.colors.surfaceSidebar, backgroundColor: theme.colors.surfaceSidebar,
}, },
mobileKeyboardInsetStyle, mobileKeyboardInsetStyle,
], ],
[insets.top, theme.colors.surfaceSidebar, mobileKeyboardInsetStyle], [
insets.bottom,
insets.top,
mobileKeyboardInsetStyle,
theme.colors.surfaceSidebar,
usePanelKeyboardPadding,
],
); );
return ( return (
@@ -318,6 +332,16 @@ function ExplorerSidebarContent({
!isGit && (activeTab === "changes" || activeTab === "pr") ? "files" : activeTab; !isGit && (activeTab === "changes" || activeTab === "pr") ? "files" : activeTab;
const resolvedTab: ExplorerTab = requestedTab === "pr" && !showPrTab ? "changes" : requestedTab; const resolvedTab: ExplorerTab = requestedTab === "pr" && !showPrTab ? "changes" : requestedTab;
const prTabLabel = formatPrTabLabel(prPane.prNumber); const prTabLabel = formatPrTabLabel(prPane.prNumber);
const availableTabs = useMemo<ExplorerTab[]>(() => {
const tabs: ExplorerTab[] = isGit ? ["changes", "files"] : ["files"];
if (isGit && showPrTab) tabs.push("pr");
return tabs;
}, [isGit, showPrTab]);
const { mountedTabIds } = useMountedTabSet({
activeTabId: resolvedTab,
allTabIds: availableTabs,
cap: availableTabs.length,
});
return ( return (
<View style={styles.sidebarContent} pointerEvents="auto"> <View style={styles.sidebarContent} pointerEvents="auto">
@@ -391,31 +415,37 @@ function ExplorerSidebarContent({
{/* Content based on active tab */} {/* Content based on active tab */}
<View style={styles.contentArea} testID="explorer-content-area"> <View style={styles.contentArea} testID="explorer-content-area">
{resolvedTab === "changes" && ( {mountedTabIds.has("changes") ? (
<ChangedFilesPane <RetainedPanel active={resolvedTab === "changes"}>
serverId={serverId} <ChangedFilesPane
workspaceId={workspaceId} serverId={serverId}
workspaceRoot={workspaceRoot} workspaceId={workspaceId}
isOpen={isOpen} workspaceRoot={workspaceRoot}
onOpenFile={onOpenFile} isOpen={isOpen}
/> onOpenFile={onOpenFile}
)} />
{resolvedTab === "files" && ( </RetainedPanel>
<FilesPane ) : null}
serverId={serverId} {mountedTabIds.has("files") ? (
workspaceId={workspaceId} <RetainedPanel active={resolvedTab === "files"}>
workspaceRoot={workspaceRoot} <FilesPane
onOpenFile={onOpenFile} serverId={serverId}
/> workspaceId={workspaceId}
)} workspaceRoot={workspaceRoot}
{resolvedTab === "pr" && ( onOpenFile={onOpenFile}
<PrTabContent />
serverId={serverId} </RetainedPanel>
workspaceId={workspaceId} ) : null}
cwd={workspaceRoot} {mountedTabIds.has("pr") ? (
prPane={prPane} <RetainedPanel active={resolvedTab === "pr"}>
/> <PrTabContent
)} serverId={serverId}
workspaceId={workspaceId}
cwd={workspaceRoot}
prPane={prPane}
/>
</RetainedPanel>
) : null}
</View> </View>
</View> </View>
); );
@@ -487,7 +517,10 @@ const styles = StyleSheet.create((theme) => ({
}, },
header: { header: {
position: "relative", position: "relative",
height: HEADER_INNER_HEIGHT, height: {
xs: HEADER_INNER_HEIGHT_MOBILE,
md: HEADER_INNER_HEIGHT,
},
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
@@ -1,5 +1,12 @@
export const DEFAULT_IOS_KEYBOARD_INSET_MIN_HEIGHT = 120; export const DEFAULT_IOS_KEYBOARD_INSET_MIN_HEIGHT = 120;
export function shouldUseCompactExplorerKeyboardPadding(input: {
isGit: boolean;
explorerTab: "changes" | "files" | "pr";
}): boolean {
return !input.isGit || input.explorerTab !== "changes";
}
export function resolveKeyboardShift(input: { export function resolveKeyboardShift(input: {
rawKeyboardHeight: number; rawKeyboardHeight: number;
keyboardProgress: number; keyboardProgress: number;
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { resolveKeyboardShift } from "./keyboard-shift-policy"; import {
resolveKeyboardShift,
shouldUseCompactExplorerKeyboardPadding,
} from "./keyboard-shift-policy";
describe("resolveKeyboardShift", () => { describe("resolveKeyboardShift", () => {
it("keeps the existing open-keyboard offset behavior", () => { it("keeps the existing open-keyboard offset behavior", () => {
@@ -38,3 +41,17 @@ describe("resolveKeyboardShift", () => {
).toBe(0); ).toBe(0);
}); });
}); });
describe("shouldUseCompactExplorerKeyboardPadding", () => {
it("keeps the changes viewport stable while preserving padding for other tabs", () => {
expect(shouldUseCompactExplorerKeyboardPadding({ isGit: true, explorerTab: "changes" })).toBe(
false,
);
expect(shouldUseCompactExplorerKeyboardPadding({ isGit: true, explorerTab: "files" })).toBe(
true,
);
expect(shouldUseCompactExplorerKeyboardPadding({ isGit: false, explorerTab: "changes" })).toBe(
true,
);
});
});