Improve workspace navigation and render isolation

This commit is contained in:
Mohamed Boudra
2026-04-20 16:14:22 +07:00
parent 403fb03c97
commit a6423f7f0a
46 changed files with 2801 additions and 862 deletions
@@ -3,21 +3,3 @@ export interface ExplorerCheckoutContext {
cwd: string;
isGit: boolean;
}
let activeExplorerCheckout: ExplorerCheckoutContext | null = null;
export function getActiveExplorerCheckout(): ExplorerCheckoutContext | null {
return activeExplorerCheckout;
}
export function setActiveExplorerCheckout(checkout: ExplorerCheckoutContext | null) {
if (
activeExplorerCheckout?.serverId === checkout?.serverId &&
activeExplorerCheckout?.cwd === checkout?.cwd &&
activeExplorerCheckout?.isGit === checkout?.isGit
) {
return;
}
activeExplorerCheckout = checkout;
}
@@ -0,0 +1,137 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/constants/platform", () => ({
isWeb: true,
}));
function installWindowStub(pathname: string) {
const windowStub = {
location: {
href: "",
origin: "http://localhost",
pathname: "",
search: "",
hash: "",
},
history: {
pushState: vi.fn((_state: unknown, _title: string, url: string) => {
updateLocation(url);
}),
replaceState: vi.fn((_state: unknown, _title: string, url: string) => {
updateLocation(url);
}),
},
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
};
function updateLocation(url: string) {
const next = new URL(url, windowStub.location.origin);
windowStub.location = {
href: next.href,
origin: next.origin,
pathname: next.pathname,
search: next.search,
hash: next.hash,
};
}
updateLocation(pathname);
vi.stubGlobal("window", windowStub);
return windowStub;
}
function createNavigationRef(serverId: string, workspaceId: string) {
return {
current: {
getCurrentRoute: () => ({
params: { serverId, workspaceId },
}),
},
};
}
function createNavigationPathRef(path: string) {
return {
current: {
getCurrentRoute: () => ({
path,
}),
},
};
}
function createNavigationPathWithParamsRef(path: string, serverId: string, workspaceId: string) {
return {
current: {
getCurrentRoute: () => ({
path,
params: { serverId, workspaceId },
}),
},
};
}
describe("navigation active workspace store", () => {
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("keeps explicit web workspace activation ahead of a stale navigation route sync", async () => {
installWindowStub("/h/server-1/workspace/workspace-a");
const store = await import("@/stores/navigation-active-workspace-store");
store.syncNavigationActiveWorkspace(createNavigationRef("server-1", "workspace-a"));
expect(store.getNavigationActiveWorkspaceSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-a",
});
store.activateNavigationWorkspaceSelection(
{ serverId: "server-1", workspaceId: "workspace-b" },
{ updateBrowserHistory: true, historyMode: "push" },
);
store.syncNavigationActiveWorkspace(createNavigationRef("server-1", "workspace-a"));
expect(store.getNavigationActiveWorkspaceSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-b",
});
});
it("clears a stale browser workspace when navigation sync reports a non-workspace route", async () => {
installWindowStub("/h/server-1/workspace/workspace-a");
const store = await import("@/stores/navigation-active-workspace-store");
store.syncNavigationActiveWorkspace(createNavigationRef("server-1", "workspace-a"));
expect(store.getNavigationActiveWorkspaceSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-a",
});
store.syncNavigationActiveWorkspace(createNavigationPathRef("/h/server-1/sessions"));
expect(store.getNavigationActiveWorkspaceSelection()).toBeNull();
});
it("clears stale workspace params when navigation sync reports a non-workspace path", async () => {
installWindowStub("/h/server-1/workspace/workspace-a");
const store = await import("@/stores/navigation-active-workspace-store");
store.syncNavigationActiveWorkspace(createNavigationRef("server-1", "workspace-a"));
expect(store.getNavigationActiveWorkspaceSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-a",
});
store.syncNavigationActiveWorkspace(
createNavigationPathWithParamsRef("/h/server-1/sessions", "server-1", "workspace-a"),
);
expect(store.getNavigationActiveWorkspaceSelection()).toBeNull();
});
});
@@ -1,4 +1,5 @@
import { useSyncExternalStore } from "react";
import { useMemo, useSyncExternalStore } from "react";
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
import {
buildHostWorkspaceRoute,
decodeWorkspaceIdFromPathSegment,
@@ -16,15 +17,24 @@ interface ActivateWorkspaceSelectionOptions {
historyMode?: "push" | "replace";
}
type NavigationRouteLike = {
name?: unknown;
params?: unknown;
path?: unknown;
type NavigationRouteParams = {
serverId?: string | string[];
workspaceId?: string | string[];
};
type NavigationRouteLike = {
params?: NavigationRouteParams | null;
path?: string | null;
};
type NavigationWorkspaceRouteState =
| { kind: "workspace"; selection: ActiveWorkspaceSelection }
| { kind: "nonWorkspace" }
| { kind: "unknown" };
interface NavigationObserverRef {
current: {
getCurrentRoute(): unknown;
getCurrentRoute(): NavigationRouteLike | null | undefined;
} | null;
}
@@ -83,7 +93,7 @@ function writeBrowserWorkspaceUrl(
}
function extractActiveWorkspaceFromRoute(
route: NavigationRouteLike | undefined,
route: NavigationRouteLike | null | undefined,
): ActiveWorkspaceSelection | null {
if (!route) {
return null;
@@ -96,17 +106,12 @@ function extractActiveWorkspaceFromRoute(
}
}
const params =
route.params && typeof route.params === "object"
? (route.params as {
serverId?: string | string[];
workspaceId?: string | string[];
})
: null;
const serverValue = Array.isArray(params?.serverId) ? params?.serverId[0] : params?.serverId;
const workspaceValue = Array.isArray(params?.workspaceId)
? params?.workspaceId[0]
: params?.workspaceId;
const serverValue = Array.isArray(route.params?.serverId)
? route.params.serverId[0]
: route.params?.serverId;
const workspaceValue = Array.isArray(route.params?.workspaceId)
? route.params.workspaceId[0]
: route.params?.workspaceId;
const serverId = typeof serverValue === "string" ? serverValue.trim() : "";
const workspaceId =
typeof workspaceValue === "string"
@@ -120,20 +125,55 @@ function extractActiveWorkspaceFromRoute(
return { serverId, workspaceId };
}
function classifyNavigationWorkspaceRoute(
route: NavigationRouteLike | null | undefined,
): NavigationWorkspaceRouteState {
if (!route) {
return { kind: "unknown" };
}
if (typeof route.path === "string") {
const selection = parseHostWorkspaceRouteFromPathname(route.path);
if (selection) {
return { kind: "workspace", selection };
}
return { kind: "nonWorkspace" };
}
const selection = extractActiveWorkspaceFromRoute(route);
if (selection) {
return { kind: "workspace", selection };
}
return { kind: "unknown" };
}
function getActiveWorkspaceForNavigationSync(
route: NavigationRouteLike | null | undefined,
): ActiveWorkspaceSelection | null {
const routeState = classifyNavigationWorkspaceRoute(route);
if (routeState.kind === "workspace") {
const browserWorkspace = getBrowserLocationWorkspace();
return browserWorkspace ?? routeState.selection;
}
if (routeState.kind === "nonWorkspace") {
return null;
}
return getBrowserLocationWorkspace();
}
export function syncNavigationActiveWorkspace(navigationRef: NavigationObserverRef) {
emitIfChanged(
extractActiveWorkspaceFromRoute(
navigationRef.current?.getCurrentRoute() as NavigationRouteLike | undefined,
),
);
emitIfChanged(getActiveWorkspaceForNavigationSync(navigationRef.current?.getCurrentRoute()));
}
export function activateNavigationWorkspaceSelection(
next: ActiveWorkspaceSelection,
options: ActivateWorkspaceSelectionOptions = {},
) {
emitIfChanged(next);
writeBrowserWorkspaceUrl(next, options);
emitIfChanged(next);
}
export function getNavigationActiveWorkspaceSelection(): ActiveWorkspaceSelection | null {
@@ -161,3 +201,45 @@ export function addBrowserActiveWorkspaceLocationListener(): () => void {
export function useNavigationActiveWorkspaceSelection(): ActiveWorkspaceSelection | null {
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
export function useIsNavigationWorkspaceSelected(input: {
serverId: string | null;
workspaceId: string | null;
enabled?: boolean;
}): boolean {
const enabled = input.enabled !== false;
return useSyncExternalStoreWithSelector(
subscribe,
getSnapshot,
getSnapshot,
(selection) =>
enabled &&
Boolean(input.serverId) &&
Boolean(input.workspaceId) &&
selection?.serverId === input.serverId &&
selection.workspaceId === input.workspaceId,
Object.is,
);
}
export function useIsNavigationProjectActive(input: {
serverId: string | null;
workspaceIds: readonly string[];
enabled?: boolean;
}): boolean {
const enabled = input.enabled !== false;
const workspaceIdsKey = input.workspaceIds.join("\0");
const workspaceIdSet = useMemo(() => new Set(input.workspaceIds), [workspaceIdsKey]);
return useSyncExternalStoreWithSelector(
subscribe,
getSnapshot,
getSnapshot,
(selection) =>
enabled &&
Boolean(input.serverId) &&
selection?.serverId === input.serverId &&
workspaceIdSet.has(selection.workspaceId),
Object.is,
);
}
+174 -1
View File
@@ -1,8 +1,64 @@
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@react-native-async-storage/async-storage", () => {
const storage = new Map<string, string>();
return {
default: {
getItem: vi.fn(async (key: string) => storage.get(key) ?? null),
setItem: vi.fn(async (key: string, value: string) => {
storage.set(key, value);
}),
removeItem: vi.fn(async (key: string) => {
storage.delete(key);
}),
},
};
});
import {
buildExplorerCheckoutKey,
resolveExplorerTabForCheckout,
} from "@/stores/explorer-tab-memory";
import {
selectIsAgentListOpen,
selectIsFileExplorerOpen,
selectPanelVisibility,
usePanelStore,
type PanelState,
} from "@/stores/panel-store";
function resetPanelStore() {
usePanelStore.setState({
mobileView: "agent",
desktop: {
agentListOpen: false,
fileExplorerOpen: false,
focusModeEnabled: false,
},
explorerTab: "changes",
explorerTabByCheckout: {},
});
}
function createPanelState(input: {
mobileView: PanelState["mobileView"];
agentListOpen: boolean;
fileExplorerOpen: boolean;
}): PanelState {
return {
...usePanelStore.getState(),
mobileView: input.mobileView,
desktop: {
...usePanelStore.getState().desktop,
agentListOpen: input.agentListOpen,
fileExplorerOpen: input.fileExplorerOpen,
},
};
}
beforeEach(() => {
resetPanelStore();
});
describe("panel-store explorer tab resolution", () => {
const serverId = "server-1";
@@ -72,3 +128,120 @@ describe("panel-store explorer tab resolution", () => {
).toBe("files");
});
});
describe("panel-store visibility selectors", () => {
it("uses mobileView for compact layout visibility", () => {
const state = createPanelState({
mobileView: "file-explorer",
agentListOpen: true,
fileExplorerOpen: false,
});
expect(selectPanelVisibility(state, { isCompact: true })).toEqual({
isAgentListOpen: false,
isFileExplorerOpen: true,
});
expect(selectIsAgentListOpen(state, { isCompact: true })).toBe(false);
expect(selectIsFileExplorerOpen(state, { isCompact: true })).toBe(true);
});
it("uses desktop flags for expanded layout visibility", () => {
const state = createPanelState({
mobileView: "file-explorer",
agentListOpen: true,
fileExplorerOpen: false,
});
expect(selectPanelVisibility(state, { isCompact: false })).toEqual({
isAgentListOpen: true,
isFileExplorerOpen: false,
});
expect(selectIsAgentListOpen(state, { isCompact: false })).toBe(true);
expect(selectIsFileExplorerOpen(state, { isCompact: false })).toBe(false);
});
});
describe("panel-store checkout-intent file explorer actions", () => {
it("opens the compact explorer and resolves the tab from the explicit checkout", () => {
const checkout = { serverId: "server-1", cwd: "/tmp/repo", isGit: true };
const key = buildExplorerCheckoutKey(checkout.serverId, checkout.cwd)!;
usePanelStore.setState({
explorerTab: "changes",
explorerTabByCheckout: { [key]: "files" },
});
usePanelStore.getState().openFileExplorerForCheckout({
isCompact: true,
checkout,
});
expect(usePanelStore.getState().mobileView).toBe("file-explorer");
expect(usePanelStore.getState().desktop.fileExplorerOpen).toBe(false);
expect(usePanelStore.getState().explorerTab).toBe("files");
});
it("opens the expanded explorer and resolves the tab from the explicit checkout", () => {
const checkout = { serverId: "server-1", cwd: "/tmp/repo", isGit: true };
const key = buildExplorerCheckoutKey(checkout.serverId, checkout.cwd)!;
usePanelStore.setState({
explorerTab: "changes",
explorerTabByCheckout: { [key]: "files" },
});
usePanelStore.getState().openFileExplorerForCheckout({
isCompact: false,
checkout,
});
expect(usePanelStore.getState().mobileView).toBe("agent");
expect(usePanelStore.getState().desktop.fileExplorerOpen).toBe(true);
expect(usePanelStore.getState().explorerTab).toBe("files");
});
it("toggles the explorer closed without changing the active tab", () => {
usePanelStore.setState({
desktop: {
agentListOpen: false,
fileExplorerOpen: true,
focusModeEnabled: false,
},
explorerTab: "files",
});
usePanelStore.getState().toggleFileExplorerForCheckout({
isCompact: false,
checkout: { serverId: "server-1", cwd: "/tmp/repo", isGit: true },
});
expect(usePanelStore.getState().desktop.fileExplorerOpen).toBe(false);
expect(usePanelStore.getState().explorerTab).toBe("files");
});
it("coerces changes to files for a non-git checkout", () => {
const checkout = { serverId: "server-1", cwd: "/tmp/repo", isGit: false };
const key = buildExplorerCheckoutKey(checkout.serverId, checkout.cwd)!;
usePanelStore.setState({
explorerTab: "changes",
explorerTabByCheckout: { [key]: "changes" },
});
usePanelStore.getState().openFileExplorerForCheckout({
isCompact: false,
checkout,
});
expect(usePanelStore.getState().explorerTab).toBe("files");
});
it("opens with the default files tab for an explicit non-git checkout with no stored tab", () => {
usePanelStore.setState({ explorerTab: "changes", explorerTabByCheckout: {} });
usePanelStore.getState().openFileExplorerForCheckout({
isCompact: false,
checkout: { serverId: "server-1", cwd: "/tmp/non-git", isGit: false },
});
expect(usePanelStore.getState().desktop.fileExplorerOpen).toBe(true);
expect(usePanelStore.getState().explorerTab).toBe("files");
});
});
+142 -113
View File
@@ -8,10 +8,7 @@ import {
resolveExplorerTabForCheckout,
type ExplorerTab,
} from "./explorer-tab-memory";
import {
getActiveExplorerCheckout,
type ExplorerCheckoutContext,
} from "./explorer-checkout-context";
import { type ExplorerCheckoutContext } from "./explorer-checkout-context";
import { isWeb } from "@/constants/platform";
export type { ExplorerTab } from "./explorer-tab-memory";
export type { ExplorerCheckoutContext } from "./explorer-checkout-context";
@@ -27,7 +24,7 @@ export type { ExplorerCheckoutContext } from "./explorer-checkout-context";
* This makes impossible states unrepresentable - you cannot have both
* sidebars open at the same time on mobile.
*/
type MobilePanelView = "agent" | "agent-list" | "file-explorer";
export type MobilePanelView = "agent" | "agent-list" | "file-explorer";
/**
* Desktop sidebar state.
@@ -55,7 +52,20 @@ export const DEFAULT_EXPLORER_FILES_SPLIT_RATIO = 0.38;
export const MIN_EXPLORER_FILES_SPLIT_RATIO = 0.2;
export const MAX_EXPLORER_FILES_SPLIT_RATIO = 0.8;
interface PanelState {
export interface PanelVisibilityState {
isAgentListOpen: boolean;
isFileExplorerOpen: boolean;
}
export interface PanelLayoutInput {
isCompact: boolean;
}
export interface ExplorerPanelIntent extends PanelLayoutInput {
checkout: ExplorerCheckoutContext;
}
export interface PanelState {
// Mobile: which panel is currently shown
mobileView: MobilePanelView;
@@ -76,16 +86,16 @@ interface PanelState {
toggleFocusMode: () => void;
showMobileAgent: () => void;
showMobileAgentList: () => void;
showMobileFileExplorer: () => void;
toggleMobileAgentList: () => void;
toggleMobileFileExplorer: () => void;
openDesktopAgentList: () => void;
closeDesktopAgentList: () => void;
toggleDesktopAgentList: () => void;
openDesktopFileExplorer: () => void;
closeDesktopFileExplorer: () => void;
toggleDesktopFileExplorer: () => void;
toggleDesktopSidebars: () => void;
openAgentListForLayout: (input: PanelLayoutInput) => void;
closeAgentListForLayout: (input: PanelLayoutInput) => void;
toggleAgentListForLayout: (input: PanelLayoutInput) => void;
openFileExplorerForCheckout: (input: ExplorerPanelIntent) => void;
toggleFileExplorerForCheckout: (input: ExplorerPanelIntent) => void;
// File explorer settings actions
setExplorerTab: (tab: ExplorerTab) => void;
@@ -118,13 +128,7 @@ function clampExplorerFilesSplitRatio(ratio: number): number {
return clampNumber(ratio, MIN_EXPLORER_FILES_SPLIT_RATIO, MAX_EXPLORER_FILES_SPLIT_RATIO);
}
function resolveExplorerTabFromCheckout(
state: PanelState,
checkout: ExplorerCheckoutContext | null,
): ExplorerTab | null {
if (!checkout) {
return null;
}
function resolveExplorerTabFromCheckout(state: PanelState, checkout: ExplorerCheckoutContext) {
return resolveExplorerTabForCheckout({
serverId: checkout.serverId,
cwd: checkout.cwd,
@@ -133,6 +137,47 @@ function resolveExplorerTabFromCheckout(
});
}
function buildOpenFileExplorerPatch(
state: PanelState,
input: ExplorerPanelIntent,
): Partial<PanelState> {
const resolvedTab = resolveExplorerTabFromCheckout(state, input.checkout);
if (input.isCompact) {
return {
mobileView: "file-explorer",
explorerTab: resolvedTab,
};
}
return {
desktop: { ...state.desktop, fileExplorerOpen: true },
explorerTab: resolvedTab,
};
}
export function selectPanelVisibility(
state: PanelState,
input: PanelLayoutInput,
): PanelVisibilityState {
if (input.isCompact) {
return {
isAgentListOpen: state.mobileView === "agent-list",
isFileExplorerOpen: state.mobileView === "file-explorer",
};
}
return {
isAgentListOpen: state.desktop.agentListOpen,
isFileExplorerOpen: state.desktop.fileExplorerOpen,
};
}
export function selectIsAgentListOpen(state: PanelState, input: PanelLayoutInput): boolean {
return selectPanelVisibility(state, input).isAgentListOpen;
}
export function selectIsFileExplorerOpen(state: PanelState, input: PanelLayoutInput): boolean {
return selectPanelVisibility(state, input).isFileExplorerOpen;
}
const DEFAULT_DESKTOP_OPEN = isWeb;
export const usePanelStore = create<PanelState>()(
@@ -179,32 +224,11 @@ export const usePanelStore = create<PanelState>()(
return { mobileView: "agent-list" as const };
}),
showMobileFileExplorer: () =>
set((state) => {
const resolvedTab = resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout());
return {
mobileView: "file-explorer" as const,
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
toggleMobileAgentList: () =>
set((state) => ({
mobileView: state.mobileView === "agent-list" ? "agent" : "agent-list",
})),
toggleMobileFileExplorer: () =>
set((state) => {
if (state.mobileView === "file-explorer") {
return { mobileView: "agent" as const };
}
const resolvedTab = resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout());
return {
mobileView: "file-explorer" as const,
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
openDesktopAgentList: () =>
set((state) => {
if (state.desktop.agentListOpen) {
@@ -226,15 +250,6 @@ export const usePanelStore = create<PanelState>()(
desktop: { ...state.desktop, agentListOpen: !state.desktop.agentListOpen },
})),
openDesktopFileExplorer: () =>
set((state) => {
const resolvedTab = resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout());
return {
desktop: { ...state.desktop, fileExplorerOpen: true },
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
closeDesktopFileExplorer: () =>
set((state) => {
if (!state.desktop.fileExplorerOpen) {
@@ -243,31 +258,51 @@ export const usePanelStore = create<PanelState>()(
return { desktop: { ...state.desktop, fileExplorerOpen: false } };
}),
toggleDesktopFileExplorer: () =>
openAgentListForLayout: ({ isCompact }) =>
set((state) => {
const willOpen = !state.desktop.fileExplorerOpen;
const resolvedTab = willOpen
? resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout())
: null;
if (isCompact) {
return state.mobileView === "agent-list"
? state
: { mobileView: "agent-list" as const };
}
return state.desktop.agentListOpen
? state
: { desktop: { ...state.desktop, agentListOpen: true } };
}),
closeAgentListForLayout: ({ isCompact }) =>
set((state) => {
if (isCompact) {
return state.mobileView === "agent" ? state : { mobileView: "agent" as const };
}
return state.desktop.agentListOpen
? { desktop: { ...state.desktop, agentListOpen: false } }
: state;
}),
toggleAgentListForLayout: ({ isCompact }) =>
set((state) => {
if (isCompact) {
return { mobileView: state.mobileView === "agent-list" ? "agent" : "agent-list" };
}
return {
desktop: { ...state.desktop, fileExplorerOpen: willOpen },
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
desktop: { ...state.desktop, agentListOpen: !state.desktop.agentListOpen },
};
}),
toggleDesktopSidebars: () =>
openFileExplorerForCheckout: (input) =>
set((state) => buildOpenFileExplorerPatch(state, input)),
toggleFileExplorerForCheckout: (input) =>
set((state) => {
const anyOpen = state.desktop.agentListOpen || state.desktop.fileExplorerOpen;
if (anyOpen) {
return {
desktop: { ...state.desktop, agentListOpen: false, fileExplorerOpen: false },
};
const isOpen = selectIsFileExplorerOpen(state, input);
if (!isOpen) {
return buildOpenFileExplorerPatch(state, input);
}
const resolvedTab = resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout());
return {
desktop: { ...state.desktop, agentListOpen: true, fileExplorerOpen: true },
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
if (input.isCompact) {
return { mobileView: "agent" as const };
}
return { desktop: { ...state.desktop, fileExplorerOpen: false } };
}),
setExplorerTab: (tab) => set({ explorerTab: tab }),
@@ -434,54 +469,48 @@ export const usePanelStore = create<PanelState>()(
* @param isMobile - Whether the current breakpoint is mobile
*/
export function usePanelState(isMobile: boolean) {
const store = usePanelStore();
const isAgentListOpen = usePanelStore((state) =>
selectIsAgentListOpen(state, { isCompact: isMobile }),
);
const isFileExplorerOpen = usePanelStore((state) =>
selectIsFileExplorerOpen(state, { isCompact: isMobile }),
);
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
const openAgentListForLayout = usePanelStore((state) => state.openAgentListForLayout);
const closeAgentListForLayout = usePanelStore((state) => state.closeAgentListForLayout);
const toggleAgentListForLayout = usePanelStore((state) => state.toggleAgentListForLayout);
const closeDesktopFileExplorer = usePanelStore((state) => state.closeDesktopFileExplorer);
const explorerTab = usePanelStore((state) => state.explorerTab);
const explorerTabByCheckout = usePanelStore((state) => state.explorerTabByCheckout);
const explorerWidth = usePanelStore((state) => state.explorerWidth);
const explorerSortOption = usePanelStore((state) => state.explorerSortOption);
const explorerFilesSplitRatio = usePanelStore((state) => state.explorerFilesSplitRatio);
const setExplorerTab = usePanelStore((state) => state.setExplorerTab);
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
const activateExplorerTabForCheckout = usePanelStore(
(state) => state.activateExplorerTabForCheckout,
);
const setExplorerWidth = usePanelStore((state) => state.setExplorerWidth);
const setExplorerSortOption = usePanelStore((state) => state.setExplorerSortOption);
const setExplorerFilesSplitRatio = usePanelStore((state) => state.setExplorerFilesSplitRatio);
if (isMobile) {
return {
isAgentListOpen: store.mobileView === "agent-list",
isFileExplorerOpen: store.mobileView === "file-explorer",
openAgentList: store.showMobileAgentList,
openFileExplorer: store.showMobileFileExplorer,
closeAgentList: store.showMobileAgent,
closeFileExplorer: store.showMobileAgent,
toggleAgentList: store.toggleMobileAgentList,
toggleFileExplorer: store.toggleMobileFileExplorer,
// Explorer settings
explorerTab: store.explorerTab,
explorerTabByCheckout: store.explorerTabByCheckout,
explorerWidth: store.explorerWidth,
explorerSortOption: store.explorerSortOption,
explorerFilesSplitRatio: store.explorerFilesSplitRatio,
setExplorerTab: store.setExplorerTab,
setExplorerTabForCheckout: store.setExplorerTabForCheckout,
activateExplorerTabForCheckout: store.activateExplorerTabForCheckout,
setExplorerWidth: store.setExplorerWidth,
setExplorerSortOption: store.setExplorerSortOption,
setExplorerFilesSplitRatio: store.setExplorerFilesSplitRatio,
};
}
// Desktop: independent toggles
return {
isAgentListOpen: store.desktop.agentListOpen,
isFileExplorerOpen: store.desktop.fileExplorerOpen,
openAgentList: store.openDesktopAgentList,
openFileExplorer: store.openDesktopFileExplorer,
closeAgentList: store.closeDesktopAgentList,
closeFileExplorer: store.closeDesktopFileExplorer,
toggleAgentList: store.toggleDesktopAgentList,
toggleFileExplorer: store.toggleDesktopFileExplorer,
// Explorer settings
explorerTab: store.explorerTab,
explorerTabByCheckout: store.explorerTabByCheckout,
explorerWidth: store.explorerWidth,
explorerSortOption: store.explorerSortOption,
explorerFilesSplitRatio: store.explorerFilesSplitRatio,
setExplorerTab: store.setExplorerTab,
setExplorerTabForCheckout: store.setExplorerTabForCheckout,
activateExplorerTabForCheckout: store.activateExplorerTabForCheckout,
setExplorerWidth: store.setExplorerWidth,
setExplorerSortOption: store.setExplorerSortOption,
setExplorerFilesSplitRatio: store.setExplorerFilesSplitRatio,
isAgentListOpen,
isFileExplorerOpen,
openAgentList: () => openAgentListForLayout({ isCompact: isMobile }),
closeAgentList: () => closeAgentListForLayout({ isCompact: isMobile }),
closeFileExplorer: isMobile ? showMobileAgent : closeDesktopFileExplorer,
toggleAgentList: () => toggleAgentListForLayout({ isCompact: isMobile }),
explorerTab,
explorerTabByCheckout,
explorerWidth,
explorerSortOption,
explorerFilesSplitRatio,
setExplorerTab,
setExplorerTabForCheckout,
activateExplorerTabForCheckout,
setExplorerWidth,
setExplorerSortOption,
setExplorerFilesSplitRatio,
};
}