import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import { buildWorkspaceTabPersistenceKey, type WorkspaceTab, type WorkspaceTabTarget, } from "@/stores/workspace-tabs-store"; import { clampNormalizedSizes, closeTabInLayout, collectAllPanes, collectAllTabs, createDefaultLayout, findPaneById, findPaneContainingTab, focusPaneInLayout, focusTabInLayout, getTreeDepth, insertSplit, moveTabToPaneInLayout, normalizeLayout, openTabInLayout, removePaneFromTree, removeTabFromTree, reorderFocusedPaneTabsInLayout, reorderPaneTabsInLayout, retargetTabInLayout, splitPaneEmptyInLayout, splitPaneInLayout, type SplitGroup, type SplitNode, type SplitPane, type WorkspaceLayout, } from "@/stores/workspace-layout-actions"; import { normalizeWorkspaceTabTarget } from "@/utils/workspace-tab-identity"; export { buildWorkspaceTabPersistenceKey }; export { collectAllPanes, collectAllTabs, createDefaultLayout, findPaneById, findPaneContainingTab, getTreeDepth, insertSplit, normalizeLayout, removePaneFromTree, removeTabFromTree, }; export type { SplitGroup, SplitNode, SplitPane, WorkspaceLayout }; interface WorkspaceLayoutStore { layoutByWorkspace: Record; splitSizesByWorkspace: Record>; pinnedAgentIdsByWorkspace: Record>; openTab: (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; 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; 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; } 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 getWorkspaceLayout( state: Record, workspaceKey: string, ): WorkspaceLayout { return normalizeLayout(state[workspaceKey] ?? createDefaultLayout()); } export const useWorkspaceLayoutStore = create()( persist( (set, get) => ({ layoutByWorkspace: {}, splitSizesByWorkspace: {}, pinnedAgentIdsByWorkspace: {}, openTab: (workspaceKey, target) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTarget = normalizeWorkspaceTabTarget(target); if (!normalizedWorkspaceKey || !normalizedTarget) { return null; } const result = openTabInLayout({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), target: normalizedTarget, now: Date.now(), }); set((state) => ({ 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 { 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 { 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) => ({ layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.tabId; }, 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: (prefix) => { const randomValue = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `${prefix}_${randomValue}`; }, }); if (!result) { return null; } set((state) => ({ 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: (prefix) => { const randomValue = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `${prefix}_${randomValue}`; }, }); if (!result) { return null; } set((state) => ({ 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 { 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 { layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, 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 { 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; if (currentPinnedAgentIds?.has(normalizedAgentId)) { return state; } const nextPinnedAgentIds = new Set(currentPinnedAgentIds ?? []); nextPinnedAgentIds.add(normalizedAgentId); return { pinnedAgentIdsByWorkspace: { ...state.pinnedAgentIdsByWorkspace, [normalizedWorkspaceKey]: nextPinnedAgentIds, }, }; }); }, 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, }; } const nextPinnedAgentIds = new Set(currentPinnedAgentIds); nextPinnedAgentIds.delete(normalizedAgentId); return { pinnedAgentIdsByWorkspace: { ...state.pinnedAgentIdsByWorkspace, [normalizedWorkspaceKey]: nextPinnedAgentIds, }, }; }); }, }), { name: "workspace-layout-state", version: 1, storage: createJSONStorage(() => AsyncStorage), partialize: (state) => { const layoutByWorkspace: Record = {}; for (const key in state.layoutByWorkspace) { layoutByWorkspace[key] = normalizeLayout(state.layoutByWorkspace[key]); } return { layoutByWorkspace, splitSizesByWorkspace: state.splitSizesByWorkspace, }; }, }, ), );