import AsyncStorage from "@react-native-async-storage/async-storage"; import { useEffect, useState } from "react"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import type { WorkspaceTab, WorkspaceTabTarget } from "@/workspace-tabs/model"; import { defaultWorkspaceLayoutIds, type WorkspaceLayoutIdSource, } from "@/stores/workspace-layout-ids"; import { clampNormalizedSizes, closeTabInLayout, collectAllPanes, collectAllTabs, convertDraftToAgentInLayout, createDefaultLayout, findPaneById, findPaneContainingTab, focusPaneInLayout, focusTabInLayout, getFocusedBrowserId, getTreeDepth, insertSplit, moveTabToPaneInLayout, normalizeLayout, openTabInLayoutBackground, openTabInLayoutFocused, reconcileWorkspaceTabs, removePaneFromTree, removeTabFromTree, reorderFocusedPaneTabsInLayout, reorderPaneTabsInLayout, retargetTabInLayout, splitPaneEmptyInLayout, splitPaneInLayout, stripEphemeralTabsFromLayout, type SplitGroup, type SplitNode, type SplitPane, type WorkspaceTabReconcileState, type WorkspaceTabSnapshot, type WorkspaceLayout, } from "@/stores/workspace-layout-actions"; import { normalizeWorkspaceTabTarget } from "@/workspace-tabs/identity"; export { collectAllPanes, collectAllTabs, createDefaultLayout, findPaneById, findPaneContainingTab, getFocusedBrowserId, getTreeDepth, insertSplit, normalizeLayout, removePaneFromTree, removeTabFromTree, stripEphemeralTabsFromLayout, }; export type { SplitGroup, SplitNode, SplitPane, WorkspaceLayout, WorkspaceTabReconcileState, WorkspaceTabSnapshot, }; interface WorkspaceLayoutStore { layoutByWorkspace: Record; splitSizesByWorkspace: Record>; pinnedAgentIdsByWorkspace: Record>; pendingAgentIdsByWorkspace: Record>; hiddenAgentIdsByWorkspace: Record>; focusRestorationByWorkspace: Record; openTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null; openChildTabFocused: ( workspaceKey: string, target: WorkspaceTabTarget, parentTabId: string, ) => string | null; openTabInBackground: (workspaceKey: string, target: WorkspaceTabTarget) => string | null; closeTab: (workspaceKey: string, tabId: string) => void; focusTab: (workspaceKey: string, tabId: string) => void; retargetTab: (workspaceKey: string, tabId: string, target: WorkspaceTabTarget) => string | null; convertDraftToAgent: (workspaceKey: string, tabId: string, agentId: string) => string | null; reconcileTabs: (workspaceKey: string, snapshot: WorkspaceTabSnapshot) => void; resolvePendingAgent: (workspaceKey: string, agentId: string) => void; reorderTabs: (workspaceKey: string, tabIds: string[]) => void; getWorkspaceTabs: (workspaceKey: string) => WorkspaceTab[]; splitPane: ( workspaceKey: string, input: { tabId: string; targetPaneId: string; position: "left" | "right" | "top" | "bottom"; }, ) => string | null; splitPaneEmpty: ( workspaceKey: string, input: { targetPaneId: string; position: "left" | "right" | "top" | "bottom"; }, ) => string | null; moveTabToPane: (workspaceKey: string, tabId: string, toPaneId: string) => void; focusPane: (workspaceKey: string, paneId: string) => void; unfocusPane: (workspaceKey: string) => string | null; restorePaneFocus: (workspaceKey: string, token: string) => void; resizeSplit: (workspaceKey: string, groupId: string, sizes: number[]) => void; reorderTabsInPane: (workspaceKey: string, paneId: string, tabIds: string[]) => void; pinAgent: (workspaceKey: string, agentId: string) => void; unpinAgent: (workspaceKey: string, agentId: string) => void; hideAgent: (workspaceKey: string, agentId: string) => void; unhideAgent: (workspaceKey: string, agentId: string) => void; purgeWorkspace: (workspaceKey: string) => void; } interface WorkspaceFocusRestorationState { restorePaneId: string | null; tokens: string[]; } const MAX_TREE_DEPTH = 4; function trimNonEmpty(value: string | null | undefined): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function addAgentIdToWorkspaceSet( state: Record>, workspaceKey: string, agentId: string, ): Record> { const currentAgentIds = state[workspaceKey] ?? null; if (currentAgentIds?.has(agentId)) { return state; } const nextAgentIds = new Set(currentAgentIds ?? []); nextAgentIds.add(agentId); return { ...state, [workspaceKey]: nextAgentIds, }; } function removeAgentIdFromWorkspaceSet( state: Record>, workspaceKey: string, agentId: string, ): Record> { const currentAgentIds = state[workspaceKey] ?? null; if (!currentAgentIds?.has(agentId)) { return state; } if (currentAgentIds.size === 1) { const nextState = { ...state }; delete nextState[workspaceKey]; return nextState; } const nextAgentIds = new Set(currentAgentIds); nextAgentIds.delete(agentId); return { ...state, [workspaceKey]: nextAgentIds, }; } function getWorkspaceLayout( state: Record, workspaceKey: string, ): WorkspaceLayout { return normalizeLayout(state[workspaceKey] ?? createDefaultLayout()); } function withoutFocusRestoration( state: WorkspaceLayoutStore, workspaceKey: string, ): Pick | null { if (!(workspaceKey in state.focusRestorationByWorkspace)) { return null; } const { [workspaceKey]: _removed, ...focusRestorationByWorkspace } = state.focusRestorationByWorkspace; return { focusRestorationByWorkspace }; } function attachParentTab(input: { layout: WorkspaceLayout; childTabId: string | null; parentTabId: string | null; }): WorkspaceLayout { const childTabId = trimNonEmpty(input.childTabId); const parentTabId = trimNonEmpty(input.parentTabId); if (!childTabId || !parentTabId || childTabId === parentTabId) { return normalizeLayout(input.layout); } const openTabIds = new Set(collectAllTabs(input.layout.root).map((tab) => tab.tabId)); if (!openTabIds.has(childTabId) || !openTabIds.has(parentTabId)) { return normalizeLayout(input.layout); } return normalizeLayout({ ...input.layout, parentTabIdByTabId: { ...input.layout.parentTabIdByTabId, [childTabId]: parentTabId, }, }); } export function createWorkspaceLayoutStore( ids: WorkspaceLayoutIdSource = defaultWorkspaceLayoutIds, ) { return create()( persist( (set, get) => ({ layoutByWorkspace: {}, splitSizesByWorkspace: {}, pinnedAgentIdsByWorkspace: {}, pendingAgentIdsByWorkspace: {}, hiddenAgentIdsByWorkspace: {}, focusRestorationByWorkspace: {}, openTabFocused: (workspaceKey, target) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTarget = normalizeWorkspaceTabTarget(target); if (!normalizedWorkspaceKey || !normalizedTarget) { return null; } const result = openTabInLayoutFocused({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), target: normalizedTarget, now: Date.now(), }); set((state) => ({ ...withoutFocusRestoration(state, normalizedWorkspaceKey), hiddenAgentIdsByWorkspace: normalizedTarget.kind !== "agent" ? state.hiddenAgentIdsByWorkspace : removeAgentIdFromWorkspaceSet( state.hiddenAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedTarget.agentId, ), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.tabId; }, openChildTabFocused: (workspaceKey, target, parentTabId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedParentTabId = trimNonEmpty(parentTabId); const normalizedTarget = normalizeWorkspaceTabTarget(target); if (!normalizedWorkspaceKey || !normalizedParentTabId || !normalizedTarget) { return null; } const result = openTabInLayoutFocused({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), target: normalizedTarget, now: Date.now(), }); set((state) => { const layout = attachParentTab({ layout: result.layout, childTabId: result.tabId, parentTabId: normalizedParentTabId, }); return { ...withoutFocusRestoration(state, normalizedWorkspaceKey), hiddenAgentIdsByWorkspace: normalizedTarget.kind !== "agent" ? state.hiddenAgentIdsByWorkspace : removeAgentIdFromWorkspaceSet( state.hiddenAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedTarget.agentId, ), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: layout, }, }; }); return result.tabId; }, openTabInBackground: (workspaceKey, target) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTarget = normalizeWorkspaceTabTarget(target); if (!normalizedWorkspaceKey || !normalizedTarget) { return null; } const result = openTabInLayoutBackground({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), target: normalizedTarget, now: Date.now(), }); set((state) => ({ hiddenAgentIdsByWorkspace: normalizedTarget.kind !== "agent" ? state.hiddenAgentIdsByWorkspace : removeAgentIdFromWorkspaceSet( state.hiddenAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedTarget.agentId, ), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.tabId; }, closeTab: (workspaceKey, tabId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(tabId); if (!normalizedWorkspaceKey || !normalizedTabId) { return; } set((state) => { const nextLayout = closeTabInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, }); if (!nextLayout) { return state; } return { ...withoutFocusRestoration(state, normalizedWorkspaceKey), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, focusTab: (workspaceKey, tabId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(tabId); if (!normalizedWorkspaceKey || !normalizedTabId) { return; } set((state) => { const nextLayout = focusTabInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, }); if (!nextLayout) { return state; } return { ...withoutFocusRestoration(state, normalizedWorkspaceKey), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, retargetTab: (workspaceKey, tabId, target) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(tabId); const normalizedTarget = normalizeWorkspaceTabTarget(target); if (!normalizedWorkspaceKey || !normalizedTabId || !normalizedTarget) { return null; } const result = retargetTabInLayout({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, target: normalizedTarget, }); if (!result) { return null; } set((state) => ({ ...(result.layout.focusedPaneId !== null ? (withoutFocusRestoration(state, normalizedWorkspaceKey) ?? {}) : {}), hiddenAgentIdsByWorkspace: normalizedTarget.kind !== "agent" ? state.hiddenAgentIdsByWorkspace : removeAgentIdFromWorkspaceSet( state.hiddenAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedTarget.agentId, ), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.tabId; }, convertDraftToAgent: (workspaceKey, tabId, agentId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(tabId); const normalizedAgentId = trimNonEmpty(agentId); if (!normalizedWorkspaceKey || !normalizedTabId || !normalizedAgentId) { return null; } const result = convertDraftToAgentInLayout({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, agentId: normalizedAgentId, }); if (!result) { return null; } set((state) => ({ ...(result.layout.focusedPaneId !== null ? (withoutFocusRestoration(state, normalizedWorkspaceKey) ?? {}) : {}), hiddenAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet( state.hiddenAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedAgentId, ), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.tabId; }, reconcileTabs: (workspaceKey, snapshot) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); if (!normalizedWorkspaceKey) { return; } set((state) => { const currentLayout = getWorkspaceLayout( state.layoutByWorkspace, normalizedWorkspaceKey, ); const nextState = reconcileWorkspaceTabs( { layout: currentLayout, pinnedAgentIds: state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null, pendingAgentIds: state.pendingAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null, hiddenAgentIds: state.hiddenAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null, }, snapshot, ); if (nextState.layout === currentLayout) { return state; } return { layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextState.layout, }, }; }); }, resolvePendingAgent: (workspaceKey, agentId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedAgentId = trimNonEmpty(agentId); if (!normalizedWorkspaceKey || !normalizedAgentId) { return; } set((state) => { const pendingAgentIdsByWorkspace = removeAgentIdFromWorkspaceSet( state.pendingAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedAgentId, ); if (pendingAgentIdsByWorkspace === state.pendingAgentIdsByWorkspace) { return state; } return { pendingAgentIdsByWorkspace }; }); }, reorderTabs: (workspaceKey, tabIds) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); if (!normalizedWorkspaceKey) { return; } set((state) => { const nextLayout = reorderFocusedPaneTabsInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), tabIds, }); if (!nextLayout) { return state; } return { layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, getWorkspaceTabs: (workspaceKey) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); if (!normalizedWorkspaceKey) { return []; } return collectAllTabs( getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey).root, ); }, splitPane: (workspaceKey, input) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(input.tabId); const normalizedTargetPaneId = trimNonEmpty(input.targetPaneId); if (!normalizedWorkspaceKey || !normalizedTabId || !normalizedTargetPaneId) { return null; } const result = splitPaneInLayout({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, targetPaneId: normalizedTargetPaneId, position: input.position, maxTreeDepth: MAX_TREE_DEPTH, createNodeId: ids.createNodeId, }); if (!result) { return null; } set((state) => ({ ...withoutFocusRestoration(state, normalizedWorkspaceKey), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.paneId; }, splitPaneEmpty: (workspaceKey, input) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTargetPaneId = trimNonEmpty(input.targetPaneId); if (!normalizedWorkspaceKey || !normalizedTargetPaneId) { return null; } const result = splitPaneEmptyInLayout({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), targetPaneId: normalizedTargetPaneId, position: input.position, maxTreeDepth: MAX_TREE_DEPTH, createNodeId: ids.createNodeId, }); if (!result) { return null; } set((state) => ({ ...withoutFocusRestoration(state, normalizedWorkspaceKey), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.paneId; }, moveTabToPane: (workspaceKey, tabId, toPaneId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(tabId); const normalizedToPaneId = trimNonEmpty(toPaneId); if (!normalizedWorkspaceKey || !normalizedTabId || !normalizedToPaneId) { return; } set((state) => { const nextLayout = moveTabToPaneInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, toPaneId: normalizedToPaneId, }); if (!nextLayout) { return state; } return { ...withoutFocusRestoration(state, normalizedWorkspaceKey), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, focusPane: (workspaceKey, paneId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedPaneId = trimNonEmpty(paneId); if (!normalizedWorkspaceKey || !normalizedPaneId) { return; } set((state) => { const nextLayout = focusPaneInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), paneId: normalizedPaneId, }); if (!nextLayout) { return state; } return { ...withoutFocusRestoration(state, normalizedWorkspaceKey), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, unfocusPane: (workspaceKey) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); if (!normalizedWorkspaceKey) { return null; } const token = ids.createFocusRestorationToken(); set((state) => { const layout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey); const currentRestoration = state.focusRestorationByWorkspace[normalizedWorkspaceKey]; const restorePaneId = currentRestoration?.restorePaneId ?? layout.focusedPaneId; return { focusRestorationByWorkspace: { ...state.focusRestorationByWorkspace, [normalizedWorkspaceKey]: { restorePaneId, tokens: [...(currentRestoration?.tokens ?? []), token], }, }, layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: layout.focusedPaneId === null ? layout : { ...layout, focusedPaneId: null }, }, }; }); return token; }, restorePaneFocus: (workspaceKey, token) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedToken = trimNonEmpty(token); if (!normalizedWorkspaceKey || !normalizedToken) { return; } set((state) => { const restoration = state.focusRestorationByWorkspace[normalizedWorkspaceKey]; if (!restoration?.tokens.includes(normalizedToken)) { return state; } const nextTokens = restoration.tokens.filter((entry) => entry !== normalizedToken); const { [normalizedWorkspaceKey]: _removed, ...remainingRestorations } = state.focusRestorationByWorkspace; const layout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey); if (layout.focusedPaneId !== null) { return { focusRestorationByWorkspace: remainingRestorations, }; } if (nextTokens.length > 0) { return { focusRestorationByWorkspace: { ...remainingRestorations, [normalizedWorkspaceKey]: { restorePaneId: restoration.restorePaneId, tokens: nextTokens, }, }, }; } const restorePaneId = findPaneById(layout.root, restoration.restorePaneId)?.id ?? null; if (!restorePaneId) { return { focusRestorationByWorkspace: remainingRestorations, }; } return { focusRestorationByWorkspace: remainingRestorations, layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: { ...layout, focusedPaneId: restorePaneId, }, }, }; }); }, resizeSplit: (workspaceKey, groupId, sizes) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedGroupId = trimNonEmpty(groupId); if (!normalizedWorkspaceKey || !normalizedGroupId) { return; } set((state) => ({ splitSizesByWorkspace: { ...state.splitSizesByWorkspace, [normalizedWorkspaceKey]: { ...state.splitSizesByWorkspace[normalizedWorkspaceKey], [normalizedGroupId]: clampNormalizedSizes(sizes), }, }, })); }, reorderTabsInPane: (workspaceKey, paneId, tabIds) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedPaneId = trimNonEmpty(paneId); if (!normalizedWorkspaceKey || !normalizedPaneId) { return; } set((state) => { const nextLayout = reorderPaneTabsInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), paneId: normalizedPaneId, tabIds, }); if (!nextLayout) { return state; } return { ...withoutFocusRestoration(state, normalizedWorkspaceKey), layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, pinAgent: (workspaceKey, agentId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedAgentId = trimNonEmpty(agentId); if (!normalizedWorkspaceKey || !normalizedAgentId) { return; } set((state) => { const currentPinnedAgentIds = state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null; const currentPendingAgentIds = state.pendingAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null; if ( currentPinnedAgentIds?.has(normalizedAgentId) && currentPendingAgentIds?.has(normalizedAgentId) ) { return state; } const nextPinnedAgentIds = new Set(currentPinnedAgentIds ?? []); nextPinnedAgentIds.add(normalizedAgentId); return { hiddenAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet( state.hiddenAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedAgentId, ), pinnedAgentIdsByWorkspace: { ...state.pinnedAgentIdsByWorkspace, [normalizedWorkspaceKey]: nextPinnedAgentIds, }, pendingAgentIdsByWorkspace: addAgentIdToWorkspaceSet( state.pendingAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedAgentId, ), }; }); }, unpinAgent: (workspaceKey, agentId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedAgentId = trimNonEmpty(agentId); if (!normalizedWorkspaceKey || !normalizedAgentId) { return; } set((state) => { const currentPinnedAgentIds = state.pinnedAgentIdsByWorkspace[normalizedWorkspaceKey] ?? null; if (!currentPinnedAgentIds?.has(normalizedAgentId)) { return state; } if (currentPinnedAgentIds.size === 1) { const nextPinnedAgentIdsByWorkspace = { ...state.pinnedAgentIdsByWorkspace, }; delete nextPinnedAgentIdsByWorkspace[normalizedWorkspaceKey]; return { pinnedAgentIdsByWorkspace: nextPinnedAgentIdsByWorkspace, pendingAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet( state.pendingAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedAgentId, ), }; } const nextPinnedAgentIds = new Set(currentPinnedAgentIds); nextPinnedAgentIds.delete(normalizedAgentId); return { pinnedAgentIdsByWorkspace: { ...state.pinnedAgentIdsByWorkspace, [normalizedWorkspaceKey]: nextPinnedAgentIds, }, pendingAgentIdsByWorkspace: removeAgentIdFromWorkspaceSet( state.pendingAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedAgentId, ), }; }); }, hideAgent: (workspaceKey, agentId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedAgentId = trimNonEmpty(agentId); if (!normalizedWorkspaceKey || !normalizedAgentId) { return; } set((state) => { const nextHiddenAgentIdsByWorkspace = addAgentIdToWorkspaceSet( state.hiddenAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedAgentId, ); if (nextHiddenAgentIdsByWorkspace === state.hiddenAgentIdsByWorkspace) { return state; } return { hiddenAgentIdsByWorkspace: nextHiddenAgentIdsByWorkspace, }; }); }, unhideAgent: (workspaceKey, agentId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedAgentId = trimNonEmpty(agentId); if (!normalizedWorkspaceKey || !normalizedAgentId) { return; } set((state) => { const nextHiddenAgentIdsByWorkspace = removeAgentIdFromWorkspaceSet( state.hiddenAgentIdsByWorkspace, normalizedWorkspaceKey, normalizedAgentId, ); if (nextHiddenAgentIdsByWorkspace === state.hiddenAgentIdsByWorkspace) { return state; } return { hiddenAgentIdsByWorkspace: nextHiddenAgentIdsByWorkspace, }; }); }, purgeWorkspace: (workspaceKey) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); if (!normalizedWorkspaceKey) { return; } set((state) => { const hasAny = normalizedWorkspaceKey in state.layoutByWorkspace || normalizedWorkspaceKey in state.splitSizesByWorkspace || normalizedWorkspaceKey in state.pinnedAgentIdsByWorkspace || normalizedWorkspaceKey in state.pendingAgentIdsByWorkspace || normalizedWorkspaceKey in state.hiddenAgentIdsByWorkspace || normalizedWorkspaceKey in state.focusRestorationByWorkspace; if (!hasAny) { return state; } const { [normalizedWorkspaceKey]: _layout, ...layoutByWorkspace } = state.layoutByWorkspace; const { [normalizedWorkspaceKey]: _splits, ...splitSizesByWorkspace } = state.splitSizesByWorkspace; const { [normalizedWorkspaceKey]: _pinned, ...pinnedAgentIdsByWorkspace } = state.pinnedAgentIdsByWorkspace; const { [normalizedWorkspaceKey]: _pending, ...pendingAgentIdsByWorkspace } = state.pendingAgentIdsByWorkspace; const { [normalizedWorkspaceKey]: _hidden, ...hiddenAgentIdsByWorkspace } = state.hiddenAgentIdsByWorkspace; const { [normalizedWorkspaceKey]: _restoration, ...focusRestorationByWorkspace } = state.focusRestorationByWorkspace; return { layoutByWorkspace, splitSizesByWorkspace, pinnedAgentIdsByWorkspace, pendingAgentIdsByWorkspace, hiddenAgentIdsByWorkspace, focusRestorationByWorkspace, }; }); }, }), { name: "workspace-layout-state", version: 1, storage: createJSONStorage(() => AsyncStorage), partialize: (state) => { const layoutByWorkspace: Record = {}; for (const key in state.layoutByWorkspace) { // Strip ephemeral (commit diff) tabs before persisting so they are // dropped on reload rather than restored pointing at a rebased SHA. layoutByWorkspace[key] = stripEphemeralTabsFromLayout( normalizeLayout(state.layoutByWorkspace[key]), ); } return { layoutByWorkspace, splitSizesByWorkspace: state.splitSizesByWorkspace, }; }, }, ), ); } export const useWorkspaceLayoutStore = createWorkspaceLayoutStore(); export function useWorkspaceLayoutStoreHydrated(): boolean { const [hasHydrated, setHasHydrated] = useState(() => useWorkspaceLayoutStore.persist.hasHydrated(), ); useEffect(() => { if (useWorkspaceLayoutStore.persist.hasHydrated()) { setHasHydrated(true); return; } return useWorkspaceLayoutStore.persist.onFinishHydration(() => { setHasHydrated(true); }); }, []); return hasHydrated; }