import type { ProjectDescriptor, WorkspaceDescriptor } from "@/stores/session-store"; import { projectDisplayNameFromProjectId } from "@/utils/project-display-name"; export interface WorkspaceStructureHostPlacement { serverId: string; projectId: string; iconWorkingDir: string; worktreeSupport: "supported" | "unsupported" | "unknown"; customIconRevision?: string | null; } export interface WorkspaceStructureProject { viewKey: string; projectKey: string | null; projectName: string; projectKind: WorkspaceDescriptor["projectKind"] | "unknown"; iconWorkingDir: string; hosts: WorkspaceStructureHostPlacement[]; workspaceKeys: string[]; } export interface WorkspaceStructure { projects: WorkspaceStructureProject[]; } interface WorkspaceStructureSession { serverId: string; projects: Iterable; workspaces: Iterable; } interface ProjectDraft { viewKey: string; projectKey: string | null; projectName: string; hasCustomName: boolean; projectKind: WorkspaceDescriptor["projectKind"]; iconWorkingDir: string; hosts: Map; workspaces: Array<{ workspaceId: string; workspaceName: string; workspaceKey: string }>; } /** The single app boundary that turns host-local projects into grouped display projects. */ export function buildWorkspaceStructureProjects(input: { sessions: WorkspaceStructureSession[]; }): WorkspaceStructureProject[] { const byProject = new Map(); const projectEntries: Array<{ serverId: string; project: ProjectDescriptor }> = []; const keyCountsByServer = new Map>(); const viewKeyByServerProjectId = new Map>(); for (const session of input.sessions) { for (const project of session.projects) { projectEntries.push({ serverId: session.serverId, project }); const sharedKey = project.projectKey ?? null; if (sharedKey) { const counts = getOrCreate(keyCountsByServer, session.serverId, () => new Map()); counts.set(sharedKey, (counts.get(sharedKey) ?? 0) + 1); } } } const allocatedViewKeys = new Set( projectEntries.flatMap(({ project }) => (project.projectKey ? [project.projectKey] : [])), ); for (const { serverId, project } of projectEntries) { const viewKey = addProjectToView({ byProject, keyCountsByServer, allocatedViewKeys, serverId, project, }); getOrCreate(viewKeyByServerProjectId, serverId, () => new Map()).set( project.projectId, viewKey, ); } for (const session of input.sessions) { for (const workspace of session.workspaces) { const viewKey = viewKeyByServerProjectId.get(session.serverId)?.get(workspace.projectId); if (!viewKey) continue; byProject.get(viewKey)?.workspaces.push({ workspaceId: workspace.id, workspaceName: workspace.name, workspaceKey: `${session.serverId}:${workspace.id}`, }); } } return Array.from(byProject.values()) .map((draft) => ({ viewKey: draft.viewKey, projectKey: draft.projectKey, projectName: draft.projectName, projectKind: draft.projectKind, iconWorkingDir: draft.iconWorkingDir, hosts: Array.from(draft.hosts.values()), workspaceKeys: draft.workspaces .sort(compareWorkspaceStructureItems) .map((workspace) => workspace.workspaceKey), })) .sort( (left, right) => left.projectName.localeCompare(right.projectName, undefined, { numeric: true, sensitivity: "base", }) || left.viewKey.localeCompare(right.viewKey), ); } export function createProjectViewKey( identity: | { kind: "equivalence"; projectKey: string } | { kind: "placement"; serverId: string; projectId: string }, ): string { return identity.kind === "equivalence" ? identity.projectKey : JSON.stringify([identity.serverId, identity.projectId]); } function allocatePlacementViewKey( allocatedViewKeys: Set, serverId: string, projectId: string, ): string { const legacyKey = createProjectViewKey({ kind: "placement", serverId, projectId }); if (!allocatedViewKeys.has(legacyKey)) { allocatedViewKeys.add(legacyKey); return legacyKey; } for (let suffix = 0; ; suffix += 1) { const collisionKey = JSON.stringify(["placement", serverId, projectId, suffix]); if (allocatedViewKeys.has(collisionKey)) continue; allocatedViewKeys.add(collisionKey); return collisionKey; } } function addProjectToView(input: { byProject: Map; keyCountsByServer: Map>; allocatedViewKeys: Set; serverId: string; project: ProjectDescriptor; }): string { const { byProject, keyCountsByServer, serverId, project } = input; const sharedKey = project.projectKey ?? null; const canUseSharedKey = sharedKey !== null && keyCountsByServer.get(serverId)?.get(sharedKey) === 1; const viewKey = canUseSharedKey ? createProjectViewKey({ kind: "equivalence", projectKey: sharedKey }) : allocatePlacementViewKey(input.allocatedViewKeys, serverId, project.projectId); const placement: WorkspaceStructureHostPlacement = { serverId, projectId: project.projectId, iconWorkingDir: project.projectRootPath, worktreeSupport: project.projectKind === "git" ? "supported" : "unsupported", customIconRevision: project.projectCustomIconRevision, }; const draft = byProject.get(viewKey); if (!draft) { byProject.set(viewKey, { viewKey, projectKey: sharedKey, projectName: project.projectCustomName ?? project.projectDisplayName ?? projectDisplayNameFromProjectId(project.projectId), hasCustomName: Boolean(project.projectCustomName), projectKind: project.projectKind, iconWorkingDir: project.projectRootPath, hosts: new Map([[serverId, placement]]), workspaces: [], }); } else { if (project.projectCustomName && !draft.hasCustomName) { draft.projectName = project.projectCustomName; draft.hasCustomName = true; } draft.hosts.set(serverId, placement); } return viewKey; } function getOrCreate(map: Map, key: K, create: () => V): V { const existing = map.get(key); if (existing !== undefined) return existing; const value = create(); map.set(key, value); return value; } function compareWorkspaceStructureItems( left: { workspaceId: string; workspaceName: string }, right: { workspaceId: string; workspaceName: string }, ): number { return ( left.workspaceName.localeCompare(right.workspaceName, undefined, { numeric: true, sensitivity: "base", }) || left.workspaceId.localeCompare(right.workspaceId, undefined, { sensitivity: "base" }) ); }