import { useMemo, useRef } from "react"; import { shallow } from "zustand/shallow"; import { useStoreWithEqualityFn } from "zustand/traditional"; import type { SidebarProjectEntry, SidebarWorkspacePlacement, } from "@/hooks/use-sidebar-workspaces-list"; import { applyStoredOrdering } from "@/hooks/sidebar-workspaces-view-model"; import { useSessionStore } from "@/stores/session-store"; export interface PinnedSidebarKeys { pinnedWorkspaceKeys: string[]; // workspaceKey -> pinnedAt ISO string, used to order by recency. pinnedAtByKey: Record; } export interface PinnedSidebarGroups { // Individually pinned chats, hoisted into the Pinned section and removed from their // project below. Most recently pinned first. pinnedChats: SidebarWorkspacePlacement[]; // Everything else, with pinned chats removed. Feeds the draggable project list. unpinnedProjects: SidebarProjectEntry[]; } function buildPinnedSidebarKeys( projects: SidebarProjectEntry[], workspaceMaps: ReadonlyMap>, ): PinnedSidebarKeys { const pinnedWorkspaceKeys: string[] = []; const pinnedAtByKey: Record = {}; for (const project of projects) { for (const placement of project.workspaces) { const workspace = workspaceMaps.get(placement.serverId)?.get(placement.workspaceId); if (workspace?.pinnedAt) { pinnedWorkspaceKeys.push(placement.workspaceKey); pinnedAtByKey[placement.workspaceKey] = workspace.pinnedAt; } } } return { pinnedWorkspaceKeys, pinnedAtByKey }; } function arePinnedSidebarKeysEqual(left: PinnedSidebarKeys, right: PinnedSidebarKeys): boolean { if (left.pinnedWorkspaceKeys.length !== right.pinnedWorkspaceKeys.length) { return false; } for (let index = 0; index < left.pinnedWorkspaceKeys.length; index += 1) { const workspaceKey = left.pinnedWorkspaceKeys[index]; if ( workspaceKey !== right.pinnedWorkspaceKeys[index] || (workspaceKey && left.pinnedAtByKey[workspaceKey] !== right.pinnedAtByKey[workspaceKey]) ) { return false; } } return true; } export function usePinnedSidebarKeys(projects: SidebarProjectEntry[]): PinnedSidebarKeys { const previousKeysRef = useRef({ pinnedWorkspaceKeys: [], pinnedAtByKey: {}, }); const serverIds = useMemo( () => Array.from( new Set( projects.flatMap((project) => project.workspaces.map((workspace) => workspace.serverId)), ), ), [projects], ); const workspaceMaps = useStoreWithEqualityFn( useSessionStore, (state) => serverIds.map((serverId) => state.sessions[serverId]?.workspaces ?? null), shallow, ); return useMemo(() => { const workspaceMapByServerId = new Map< string, ReadonlyMap >(); for (let index = 0; index < serverIds.length; index += 1) { const serverId = serverIds[index]; const workspaceMap = workspaceMaps[index]; if (serverId && workspaceMap) { workspaceMapByServerId.set(serverId, workspaceMap); } } const nextKeys = buildPinnedSidebarKeys(projects, workspaceMapByServerId); if (arePinnedSidebarKeysEqual(previousKeysRef.current, nextKeys)) { return previousKeysRef.current; } previousKeysRef.current = nextKeys; return nextKeys; }, [projects, serverIds, workspaceMaps]); } // Splits the sidebar into a dedicated Pinned section (chats) and the regular list below. // Pinned chats are ordered most-recently-pinned first. export function splitPinnedSidebarGroups(input: { projects: SidebarProjectEntry[]; keys: PinnedSidebarKeys; pinnedWorkspaceOrder: string[]; }): PinnedSidebarGroups { const { projects, keys, pinnedWorkspaceOrder } = input; if (keys.pinnedWorkspaceKeys.length === 0) { return { pinnedChats: [], unpinnedProjects: projects }; } const pinnedWorkspaceKeySet = new Set(keys.pinnedWorkspaceKeys); const pinnedChats: SidebarWorkspacePlacement[] = []; const unpinnedProjects: SidebarProjectEntry[] = []; for (const project of projects) { const remainingWorkspaces: SidebarWorkspacePlacement[] = []; for (const workspace of project.workspaces) { if (pinnedWorkspaceKeySet.has(workspace.workspaceKey)) { pinnedChats.push(workspace); } else { remainingWorkspaces.push(workspace); } } // Every chat got hoisted into the Pinned section: drop the empty shell instead of // leaving a duplicate project header below. A genuinely empty project (no chats to // begin with) is kept so its "new workspace" row stays reachable. if (remainingWorkspaces.length === 0 && project.workspaces.length > 0) { continue; } unpinnedProjects.push( remainingWorkspaces.length === project.workspaces.length ? project : { ...project, workspaces: remainingWorkspaces }, ); } pinnedChats.sort((a, b) => (keys.pinnedAtByKey[b.workspaceKey] ?? "").localeCompare( keys.pinnedAtByKey[a.workspaceKey] ?? "", ), ); return { pinnedChats: applyStoredOrdering({ items: pinnedChats, storedOrder: pinnedWorkspaceOrder, getKey: (workspace) => workspace.workspaceKey, }), unpinnedProjects, }; }