feat: hide dotfiles in file explorer (#1516)

* feat: hide dotfiles in file explorer

* refactor: rename to "hidden files" and read flag from store

Rename the explorer dotfile toggle to show-semantics "hidden files"
so copy and code share one cross-platform vocabulary, and stop
prop-drilling the flag now that it lives in the panel store.

- explorerShowHiddenFiles (default true) replaces explorerHideDotFiles;
  i18n keys + translations switched from "dotfiles" to "hidden files"
- imperative helpers read usePanelStore.getState() instead of taking
  the flag as a parameter; flag stays threaded only through the
  reactive tree-row render path
- drop dead usePanelState exposure; collapse migration double-guard

* fix: address Greptile review on hidden-file path matching

- isHiddenExplorerPath no longer treats ".." segments as hidden
  (only "." was exempt before); add tests for ".." and "../sibling"
- assert the showHiddenFiles i18n key alongside hideHiddenFiles

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Yurui Zhou
2026-06-15 12:28:17 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent f4450cda51
commit eb94b70848
13 changed files with 230 additions and 51 deletions
@@ -22,6 +22,8 @@ import {
ChevronRight, ChevronRight,
Copy, Copy,
Download, Download,
Eye,
EyeOff,
MoreVertical, MoreVertical,
RotateCw, RotateCw,
} from "lucide-react-native"; } from "lucide-react-native";
@@ -43,6 +45,7 @@ import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-action
import { usePanelStore, type SortOption } from "@/stores/panel-store"; import { usePanelStore, type SortOption } from "@/stores/panel-store";
import { formatTimeAgo } from "@/utils/time"; import { formatTimeAgo } from "@/utils/time";
import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths"; import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths";
import { filterVisibleExplorerEntries, isHiddenExplorerPath } from "@/file-explorer/visibility";
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar"; import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
import { isWeb } from "@/constants/platform"; import { isWeb } from "@/constants/platform";
@@ -267,7 +270,11 @@ export function FileExplorerPane({
workspaceRoot: normalizedWorkspaceRoot, workspaceRoot: normalizedWorkspaceRoot,
}); });
const sortOption = usePanelStore((state) => state.explorerSortOption); const sortOption = usePanelStore((state) => state.explorerSortOption);
const showHiddenFiles = usePanelStore((state) => state.explorerShowHiddenFiles);
const setSortOption = usePanelStore((state) => state.setExplorerSortOption); const setSortOption = usePanelStore((state) => state.setExplorerSortOption);
const toggleExplorerShowHiddenFiles = usePanelStore(
(state) => state.toggleExplorerShowHiddenFiles,
);
const expandedPathsArray = usePanelStore((state) => const expandedPathsArray = usePanelStore((state) =>
workspaceStateKey ? state.expandedPathsByWorkspace[workspaceStateKey] : undefined, workspaceStateKey ? state.expandedPathsByWorkspace[workspaceStateKey] : undefined,
); );
@@ -379,6 +386,14 @@ export function FileExplorerPane({
setSortOption(SORT_OPTIONS[nextIndex].value); setSortOption(SORT_OPTIONS[nextIndex].value);
}, [sortOption, setSortOption]); }, [sortOption, setSortOption]);
const handleToggleHiddenFiles = useCallback(() => {
const willShow = !usePanelStore.getState().explorerShowHiddenFiles;
toggleExplorerShowHiddenFiles();
if (willShow) {
requestPersistedExpandedPaths({ workspaceStateKey, requestDirectoryListing });
}
}, [requestDirectoryListing, toggleExplorerShowHiddenFiles, workspaceStateKey]);
const refreshExplorer = useCallback( const refreshExplorer = useCallback(
() => () =>
refreshExplorerDirectories({ refreshExplorerDirectories({
@@ -409,8 +424,8 @@ export function FileExplorerPane({
const currentSortLabel = resolveCurrentSortLabel(sortOption, sortLabels); const currentSortLabel = resolveCurrentSortLabel(sortOption, sortLabels);
const treeRows = useMemo( const treeRows = useMemo(
() => resolveTreeRows({ directories, expandedPaths, sortOption }), () => resolveTreeRows({ directories, expandedPaths, sortOption, showHiddenFiles }),
[directories, expandedPaths, sortOption], [directories, expandedPaths, showHiddenFiles, sortOption],
); );
const showInitialLoading = resolveShowInitialLoading({ const showInitialLoading = resolveShowInitialLoading({
@@ -483,6 +498,7 @@ export function FileExplorerPane({
scrollbar={scrollbar} scrollbar={scrollbar}
renderTreeRow={renderTreeRow} renderTreeRow={renderTreeRow}
handleSortCycle={handleSortCycle} handleSortCycle={handleSortCycle}
handleToggleHiddenFiles={handleToggleHiddenFiles}
handleRefresh={handleRefresh} handleRefresh={handleRefresh}
handleBackFromError={handleBackFromError} handleBackFromError={handleBackFromError}
handleRetry={handleRetry} handleRetry={handleRetry}
@@ -505,6 +521,7 @@ interface FileExplorerPaneContentProps {
scrollbar: ReturnType<typeof useWebScrollViewScrollbar>; scrollbar: ReturnType<typeof useWebScrollViewScrollbar>;
renderTreeRow: (info: ListRenderItemInfo<TreeRow>) => ReactElement; renderTreeRow: (info: ListRenderItemInfo<TreeRow>) => ReactElement;
handleSortCycle: () => void; handleSortCycle: () => void;
handleToggleHiddenFiles: () => void;
handleRefresh: () => void; handleRefresh: () => void;
handleBackFromError: () => void; handleBackFromError: () => void;
handleRetry: () => void; handleRetry: () => void;
@@ -527,6 +544,7 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
scrollbar, scrollbar,
renderTreeRow, renderTreeRow,
handleSortCycle, handleSortCycle,
handleToggleHiddenFiles,
handleRefresh, handleRefresh,
handleBackFromError, handleBackFromError,
handleRetry, handleRetry,
@@ -534,6 +552,26 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
iconButtonStyle: iconButtonStyleProp, iconButtonStyle: iconButtonStyleProp,
} = props; } = props;
const showHiddenFiles = usePanelStore((state) => state.explorerShowHiddenFiles);
const hiddenFilesToggleAccessibilityLabel = showHiddenFiles
? t("workspace.fileExplorer.actions.hideHiddenFiles")
: t("workspace.fileExplorer.actions.showHiddenFiles");
const emptyLabel = showHiddenFiles
? t("workspace.fileExplorer.empty.noFiles")
: t("workspace.fileExplorer.empty.noVisibleFiles");
const hiddenFilesToggleStyle = useCallback(
(state: PressableStateCallbackType) => [
iconButtonStyleProp(state),
!showHiddenFiles && styles.iconButtonActive,
],
[showHiddenFiles, iconButtonStyleProp],
);
const hiddenFilesToggleAccessibilityState = useMemo(
() => ({ selected: !showHiddenFiles }),
[showHiddenFiles],
);
if (error) { if (error) {
return ( return (
<View style={styles.centerState}> <View style={styles.centerState}>
@@ -561,14 +599,6 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
); );
} }
if (treeRows.length === 0) {
return (
<View style={styles.centerState}>
<Text style={styles.emptyText}>{t("workspace.fileExplorer.empty.noFiles")}</Text>
</View>
);
}
return ( return (
<View style={TREE_PANE_CONTAINER_STYLE}> <View style={TREE_PANE_CONTAINER_STYLE}>
<View style={styles.paneHeader} testID="files-pane-header"> <View style={styles.paneHeader} testID="files-pane-header">
@@ -576,45 +606,67 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
<Text style={styles.sortTriggerText}>{currentSortLabel}</Text> <Text style={styles.sortTriggerText}>{currentSortLabel}</Text>
<ChevronDown size={12} color={theme.colors.foregroundMuted} /> <ChevronDown size={12} color={theme.colors.foregroundMuted} />
</Pressable> </Pressable>
<Pressable <View style={styles.headerActions}>
onPress={handleRefresh} <Pressable
disabled={isRefreshFetching} onPress={handleToggleHiddenFiles}
hitSlop={8} hitSlop={8}
style={iconButtonStyleProp} style={hiddenFilesToggleStyle}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={ accessibilityLabel={hiddenFilesToggleAccessibilityLabel}
isRefreshFetching accessibilityState={hiddenFilesToggleAccessibilityState}
? t("workspace.fileExplorer.actions.refreshing") >
: t("workspace.fileExplorer.actions.refresh") {showHiddenFiles ? (
} <Eye size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
>
<View style={styles.refreshIcon}>
{isRefreshFetching ? (
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
) : ( ) : (
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> <EyeOff size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
)} )}
</View> </Pressable>
</Pressable> <Pressable
onPress={handleRefresh}
disabled={isRefreshFetching}
hitSlop={8}
style={iconButtonStyleProp}
accessibilityRole="button"
accessibilityLabel={
isRefreshFetching
? t("workspace.fileExplorer.actions.refreshing")
: t("workspace.fileExplorer.actions.refresh")
}
>
<View style={styles.refreshIcon}>
{isRefreshFetching ? (
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
) : (
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
)}
</View>
</Pressable>
</View>
</View> </View>
<FlatList {treeRows.length === 0 ? (
ref={treeListRef} <View style={styles.centerState}>
style={styles.treeList} <Text style={styles.emptyText}>{emptyLabel}</Text>
data={treeRows} </View>
renderItem={renderTreeRow} ) : (
keyExtractor={treeRowKeyExtractor} <FlatList
testID="file-explorer-tree-scroll" ref={treeListRef}
contentContainerStyle={styles.entriesContent} style={styles.treeList}
onLayout={scrollbar.onLayout} data={treeRows}
onScroll={scrollbar.onScroll} renderItem={renderTreeRow}
onContentSizeChange={scrollbar.onContentSizeChange} keyExtractor={treeRowKeyExtractor}
scrollEventThrottle={16} testID="file-explorer-tree-scroll"
showsVerticalScrollIndicator={!showDesktopWebScrollbar} contentContainerStyle={styles.entriesContent}
initialNumToRender={24} onLayout={scrollbar.onLayout}
maxToRenderPerBatch={40} onScroll={scrollbar.onScroll}
windowSize={12} onContentSizeChange={scrollbar.onContentSizeChange}
/> scrollEventThrottle={16}
{scrollbar.overlay} showsVerticalScrollIndicator={!showDesktopWebScrollbar}
initialNumToRender={24}
maxToRenderPerBatch={40}
windowSize={12}
/>
)}
{treeRows.length > 0 ? scrollbar.overlay : null}
</View> </View>
); );
} }
@@ -643,12 +695,14 @@ function buildTreeRows({
directories, directories,
expandedPaths, expandedPaths,
sortOption, sortOption,
showHiddenFiles,
path, path,
depth, depth,
}: { }: {
directories: Map<string, { path: string; entries: ExplorerEntry[] }>; directories: Map<string, { path: string; entries: ExplorerEntry[] }>;
expandedPaths: Set<string>; expandedPaths: Set<string>;
sortOption: SortOption; sortOption: SortOption;
showHiddenFiles: boolean;
path: string; path: string;
depth: number; depth: number;
}): TreeRow[] { }): TreeRow[] {
@@ -658,7 +712,10 @@ function buildTreeRows({
} }
const rows: TreeRow[] = []; const rows: TreeRow[] = [];
const entries = sortEntries(directory.entries, sortOption); const entries = sortEntries(
filterVisibleExplorerEntries(directory.entries, showHiddenFiles),
sortOption,
);
for (const entry of entries) { for (const entry of entries) {
rows.push({ entry, depth }); rows.push({ entry, depth });
@@ -668,6 +725,7 @@ function buildTreeRows({
directories, directories,
expandedPaths, expandedPaths,
sortOption, sortOption,
showHiddenFiles,
path: entry.path, path: entry.path,
depth: depth + 1, depth: depth + 1,
}), }),
@@ -731,15 +789,24 @@ function resolveTreeRows({
directories, directories,
expandedPaths, expandedPaths,
sortOption, sortOption,
showHiddenFiles,
}: { }: {
directories: Map<string, { path: string; entries: ExplorerEntry[] }>; directories: Map<string, { path: string; entries: ExplorerEntry[] }>;
expandedPaths: Set<string>; expandedPaths: Set<string>;
sortOption: SortOption; sortOption: SortOption;
showHiddenFiles: boolean;
}): TreeRow[] { }): TreeRow[] {
if (!directories.get(".")) { if (!directories.get(".")) {
return []; return [];
} }
return buildTreeRows({ directories, expandedPaths, sortOption, path: ".", depth: 0 }); return buildTreeRows({
directories,
expandedPaths,
sortOption,
showHiddenFiles,
path: ".",
depth: 0,
});
} }
type StartDownloadFn = ReturnType<typeof useDownloadStore.getState>["startDownload"]; type StartDownloadFn = ReturnType<typeof useDownloadStore.getState>["startDownload"];
@@ -890,12 +957,13 @@ function requestPersistedExpandedPaths({
opts?: { recordHistory?: boolean; setCurrentPath?: boolean }, opts?: { recordHistory?: boolean; setCurrentPath?: boolean },
) => Promise<boolean>; ) => Promise<boolean>;
}): void { }): void {
const showHiddenFiles = usePanelStore.getState().explorerShowHiddenFiles;
const persistedPaths = usePanelStore.getState().expandedPathsByWorkspace[workspaceStateKey ?? ""]; const persistedPaths = usePanelStore.getState().expandedPathsByWorkspace[workspaceStateKey ?? ""];
if (!persistedPaths) { if (!persistedPaths) {
return; return;
} }
for (const path of persistedPaths) { for (const path of persistedPaths) {
if (path !== ".") { if (path !== "." && (showHiddenFiles || !isHiddenExplorerPath(path))) {
void requestDirectoryListing(path, { void requestDirectoryListing(path, {
recordHistory: false, recordHistory: false,
setCurrentPath: false, setCurrentPath: false,
@@ -919,7 +987,10 @@ async function refreshExplorerDirectories({
if (!hasWorkspaceScope) { if (!hasWorkspaceScope) {
return null; return null;
} }
const directoryPaths = Array.from(expandedPaths); const showHiddenFiles = usePanelStore.getState().explorerShowHiddenFiles;
const directoryPaths = Array.from(expandedPaths).filter(
(path) => showHiddenFiles || !isHiddenExplorerPath(path),
);
if (!directoryPaths.includes(".")) { if (!directoryPaths.includes(".")) {
directoryPaths.unshift("."); directoryPaths.unshift(".");
} }
@@ -1011,6 +1082,11 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
}, },
headerActions: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
treeList: { treeList: {
flex: 1, flex: 1,
minHeight: 0, minHeight: 0,
@@ -1154,6 +1230,9 @@ const styles = StyleSheet.create((theme) => ({
iconButtonHovered: { iconButtonHovered: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
}, },
iconButtonActive: {
backgroundColor: theme.colors.surface2,
},
refreshIcon: { refreshIcon: {
width: 16, width: 16,
height: 16, height: 16,
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import type { ExplorerEntry } from "@/stores/session-store";
import { filterVisibleExplorerEntries, isHiddenExplorerPath } from "./visibility";
function makeEntry(name: string, kind: ExplorerEntry["kind"]): ExplorerEntry {
return {
name,
path: name,
kind,
size: 0,
modifiedAt: "2026-01-01T00:00:00.000Z",
};
}
describe("file explorer visibility", () => {
it("keeps dot-prefixed entries when hidden files are shown", () => {
const entries = [makeEntry(".env", "file"), makeEntry("src", "directory")];
expect(filterVisibleExplorerEntries(entries, true)).toEqual(entries);
});
it("hides dot-prefixed files and directories when hidden files are not shown", () => {
const entries = [
makeEntry(".env", "file"),
makeEntry(".git", "directory"),
makeEntry("README.md", "file"),
makeEntry("src", "directory"),
];
expect(filterVisibleExplorerEntries(entries, false).map((entry) => entry.name)).toEqual([
"README.md",
"src",
]);
});
it("detects paths nested under dot-prefixed directories", () => {
expect(isHiddenExplorerPath(".")).toBe(false);
expect(isHiddenExplorerPath("..")).toBe(false);
expect(isHiddenExplorerPath("../sibling")).toBe(false);
expect(isHiddenExplorerPath("src/components")).toBe(false);
expect(isHiddenExplorerPath(".git")).toBe(true);
expect(isHiddenExplorerPath("src/.cache/output.json")).toBe(true);
});
});
@@ -0,0 +1,17 @@
import type { ExplorerEntry } from "@/stores/session-store";
export function isHiddenExplorerPath(path: string): boolean {
return path
.split("/")
.some((segment) => segment !== "." && segment !== ".." && segment.startsWith("."));
}
export function filterVisibleExplorerEntries(
entries: ExplorerEntry[],
showHiddenFiles: boolean,
): ExplorerEntry[] {
if (showHiddenFiles) {
return entries;
}
return entries.filter((entry) => !entry.name.startsWith("."));
}
+3
View File
@@ -224,7 +224,10 @@ describe("translation resources", () => {
expect(en.importSession.status.connectHost).toBe("Connect to a host to import sessions"); expect(en.importSession.status.connectHost).toBe("Connect to a host to import sessions");
expect(en.importSession.actions.refresh).toBe("Refresh sessions"); expect(en.importSession.actions.refresh).toBe("Refresh sessions");
expect(en.workspace.fileExplorer.sort.name).toBe("Name"); expect(en.workspace.fileExplorer.sort.name).toBe("Name");
expect(en.workspace.fileExplorer.actions.hideHiddenFiles).toBe("Hide hidden files");
expect(en.workspace.fileExplorer.actions.showHiddenFiles).toBe("Show hidden files");
expect(en.workspace.fileExplorer.empty.noFiles).toBe("No files"); expect(en.workspace.fileExplorer.empty.noFiles).toBe("No files");
expect(en.workspace.fileExplorer.empty.noVisibleFiles).toBe("No visible files");
expect(en.workspace.setup.status.running).toBe("Running"); expect(en.workspace.setup.status.running).toBe("Running");
expect(en.workspace.setup.empty.noCommands).toBe("No setup commands ran for this workspace."); expect(en.workspace.setup.empty.noCommands).toBe("No setup commands ran for this workspace.");
expect(en.workspace.browser.unavailable.title).toBe("Browser is desktop-only"); expect(en.workspace.browser.unavailable.title).toBe("Browser is desktop-only");
+3
View File
@@ -355,9 +355,12 @@ export const ar: TranslationResources = {
retry: "أعد المحاولة", retry: "أعد المحاولة",
refresh: "تحديث الملفات", refresh: "تحديث الملفات",
refreshing: "تحديث الملفات", refreshing: "تحديث الملفات",
hideHiddenFiles: "إخفاء الملفات المخفية",
showHiddenFiles: "إظهار الملفات المخفية",
}, },
empty: { empty: {
noFiles: "لا توجد ملفات", noFiles: "لا توجد ملفات",
noVisibleFiles: "لا توجد ملفات مرئية",
}, },
states: { states: {
unavailable: "Workspace غير متوفر", unavailable: "Workspace غير متوفر",
+3
View File
@@ -354,9 +354,12 @@ export const en = {
retry: "Retry", retry: "Retry",
refresh: "Refresh files", refresh: "Refresh files",
refreshing: "Refreshing files", refreshing: "Refreshing files",
hideHiddenFiles: "Hide hidden files",
showHiddenFiles: "Show hidden files",
}, },
empty: { empty: {
noFiles: "No files", noFiles: "No files",
noVisibleFiles: "No visible files",
}, },
states: { states: {
unavailable: "Workspace is unavailable", unavailable: "Workspace is unavailable",
+3
View File
@@ -358,9 +358,12 @@ export const es: TranslationResources = {
retry: "Rever", retry: "Rever",
refresh: "Actualizar archivos", refresh: "Actualizar archivos",
refreshing: "Actualizar archivos", refreshing: "Actualizar archivos",
hideHiddenFiles: "Ocultar archivos ocultos",
showHiddenFiles: "Mostrar archivos ocultos",
}, },
empty: { empty: {
noFiles: "Sin archivos", noFiles: "Sin archivos",
noVisibleFiles: "No hay archivos visibles",
}, },
states: { states: {
unavailable: "Workspaceno está disponible", unavailable: "Workspaceno está disponible",
+3
View File
@@ -358,9 +358,12 @@ export const fr: TranslationResources = {
retry: "Réessayer", retry: "Réessayer",
refresh: "Actualiser les fichiers", refresh: "Actualiser les fichiers",
refreshing: "Actualisation des fichiers", refreshing: "Actualisation des fichiers",
hideHiddenFiles: "Masquer les fichiers cachés",
showHiddenFiles: "Afficher les fichiers cachés",
}, },
empty: { empty: {
noFiles: "Aucun fichier", noFiles: "Aucun fichier",
noVisibleFiles: "Aucun fichier visible",
}, },
states: { states: {
unavailable: "Workspacen'est pas disponible", unavailable: "Workspacen'est pas disponible",
+3
View File
@@ -357,9 +357,12 @@ export const ru: TranslationResources = {
retry: "Повторить попытку", retry: "Повторить попытку",
refresh: "Обновить файлы", refresh: "Обновить файлы",
refreshing: "Обновление файлов", refreshing: "Обновление файлов",
hideHiddenFiles: "Скрыть скрытые файлы",
showHiddenFiles: "Показать скрытые файлы",
}, },
empty: { empty: {
noFiles: "Нет файлов", noFiles: "Нет файлов",
noVisibleFiles: "Нет видимых файлов",
}, },
states: { states: {
unavailable: "Workspace недоступен", unavailable: "Workspace недоступен",
+3
View File
@@ -355,9 +355,12 @@ export const zhCN: TranslationResources = {
retry: "重试", retry: "重试",
refresh: "刷新文件", refresh: "刷新文件",
refreshing: "正在刷新文件", refreshing: "正在刷新文件",
hideHiddenFiles: "隐藏隐藏文件",
showHiddenFiles: "显示隐藏文件",
}, },
empty: { empty: {
noFiles: "没有文件", noFiles: "没有文件",
noVisibleFiles: "没有可见文件",
}, },
states: { states: {
unavailable: "Workspace 不可用", unavailable: "Workspace 不可用",
+7 -1
View File
@@ -75,6 +75,7 @@ export interface PanelState {
sidebarWidth: number; sidebarWidth: number;
explorerWidth: number; explorerWidth: number;
explorerSortOption: SortOption; explorerSortOption: SortOption;
explorerShowHiddenFiles: boolean;
explorerFilesSplitRatio: number; explorerFilesSplitRatio: number;
// Actions // Actions
@@ -101,6 +102,7 @@ export interface PanelState {
setSidebarWidth: (width: number) => void; setSidebarWidth: (width: number) => void;
setExplorerWidth: (width: number) => void; setExplorerWidth: (width: number) => void;
setExplorerSortOption: (option: SortOption) => void; setExplorerSortOption: (option: SortOption) => void;
toggleExplorerShowHiddenFiles: () => void;
setExplorerFilesSplitRatio: (ratio: number) => void; setExplorerFilesSplitRatio: (ratio: number) => void;
} }
@@ -127,6 +129,7 @@ export const usePanelStore = create<PanelState>()(
sidebarWidth: DEFAULT_SIDEBAR_WIDTH, sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH, explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
explorerSortOption: "name", explorerSortOption: "name",
explorerShowHiddenFiles: true,
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO, explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
toggleFocusMode: () => toggleFocusMode: () =>
@@ -262,6 +265,8 @@ export const usePanelStore = create<PanelState>()(
setSidebarWidth: (width) => set({ sidebarWidth: clampSidebarWidth(width) }), setSidebarWidth: (width) => set({ sidebarWidth: clampSidebarWidth(width) }),
setExplorerWidth: (width) => set({ explorerWidth: clampExplorerWidth(width) }), setExplorerWidth: (width) => set({ explorerWidth: clampExplorerWidth(width) }),
setExplorerSortOption: (option) => set({ explorerSortOption: option }), setExplorerSortOption: (option) => set({ explorerSortOption: option }),
toggleExplorerShowHiddenFiles: () =>
set((state) => ({ explorerShowHiddenFiles: !state.explorerShowHiddenFiles })),
setExplorerFilesSplitRatio: (ratio) => setExplorerFilesSplitRatio: (ratio) =>
set({ set({
explorerFilesSplitRatio: Number.isFinite(ratio) explorerFilesSplitRatio: Number.isFinite(ratio)
@@ -271,7 +276,7 @@ export const usePanelStore = create<PanelState>()(
}), }),
{ {
name: "panel-state", name: "panel-state",
version: 10, version: 11,
storage: createJSONStorage(() => AsyncStorage), storage: createJSONStorage(() => AsyncStorage),
migrate: (persistedState, version) => migrate: (persistedState, version) =>
migratePanelState(persistedState, version, { isWeb }) as unknown as PanelState, migratePanelState(persistedState, version, { isWeb }) as unknown as PanelState,
@@ -285,6 +290,7 @@ export const usePanelStore = create<PanelState>()(
sidebarWidth: state.sidebarWidth, sidebarWidth: state.sidebarWidth,
explorerWidth: state.explorerWidth, explorerWidth: state.explorerWidth,
explorerSortOption: state.explorerSortOption, explorerSortOption: state.explorerSortOption,
explorerShowHiddenFiles: state.explorerShowHiddenFiles,
explorerFilesSplitRatio: state.explorerFilesSplitRatio, explorerFilesSplitRatio: state.explorerFilesSplitRatio,
}), }),
}, },
@@ -7,6 +7,7 @@ import {
import { import {
buildOpenFileExplorerPatch, buildOpenFileExplorerPatch,
buildToggleFileExplorerPatch, buildToggleFileExplorerPatch,
migratePanelState,
selectIsAgentListOpen, selectIsAgentListOpen,
selectIsFileExplorerOpen, selectIsFileExplorerOpen,
selectPanelVisibility, selectPanelVisibility,
@@ -96,6 +97,14 @@ describe("panel-store explorer tab resolution", () => {
}); });
}); });
describe("panel-store migration", () => {
it("defaults hidden-file visibility to showing hidden files", () => {
const state = migratePanelState({}, 10, { isWeb: false });
expect(state.explorerShowHiddenFiles).toBe(true);
});
});
describe("panel-store visibility selectors", () => { describe("panel-store visibility selectors", () => {
it("uses mobileView for compact layout visibility", () => { it("uses mobileView for compact layout visibility", () => {
const state = makePanelState({ const state = makePanelState({
@@ -245,6 +245,9 @@ export function migratePanelState(
) { ) {
state.diffExpandedPathsByWorkspace = {}; state.diffExpandedPathsByWorkspace = {};
} }
if (typeof state.explorerShowHiddenFiles !== "boolean") {
state.explorerShowHiddenFiles = true;
}
return state; return state;
} }