mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 23:44:41 +00:00
Order ordinary sidebar workspaces by Created or Last prompt, preserve project and pin ordering, and keep later active or unacknowledged work visible beyond the first seven rows. Preserve reveal state through live updates and verify sorting, visibility, compatibility, and rendering behavior.
232 lines
7.4 KiB
TypeScript
232 lines
7.4 KiB
TypeScript
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;
|
|
iconRevision?: string;
|
|
}
|
|
|
|
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<ProjectDescriptor>;
|
|
workspaces: Iterable<WorkspaceDescriptor>;
|
|
}
|
|
|
|
export type WorkspaceSort = "created" | "lastPrompt";
|
|
|
|
interface WorkspaceSortItem {
|
|
workspaceName: string;
|
|
workspaceKey: string;
|
|
sortTime: number;
|
|
}
|
|
|
|
interface ProjectDraft {
|
|
viewKey: string;
|
|
projectKey: string | null;
|
|
projectName: string;
|
|
hasCustomName: boolean;
|
|
projectKind: WorkspaceDescriptor["projectKind"];
|
|
iconWorkingDir: string;
|
|
hosts: Map<string, WorkspaceStructureHostPlacement>;
|
|
workspaces: WorkspaceSortItem[];
|
|
}
|
|
|
|
/** The single app boundary that turns host-local projects into grouped display projects. */
|
|
export function buildWorkspaceStructureProjects(input: {
|
|
sessions: WorkspaceStructureSession[];
|
|
workspaceSort?: WorkspaceSort;
|
|
}): WorkspaceStructureProject[] {
|
|
const byProject = new Map<string, ProjectDraft>();
|
|
const projectEntries: Array<{ serverId: string; project: ProjectDescriptor }> = [];
|
|
const keyCountsByServer = new Map<string, Map<string, number>>();
|
|
const viewKeyByServerProjectId = new Map<string, Map<string, string>>();
|
|
|
|
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({
|
|
sortTime: workspaceSortTime(workspace, input.workspaceSort),
|
|
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(input.workspaceSort ? compareWorkspaceTimes : 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<string>,
|
|
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<string, ProjectDraft>;
|
|
keyCountsByServer: Map<string, Map<string, number>>;
|
|
allocatedViewKeys: Set<string>;
|
|
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,
|
|
iconRevision: project.projectIconRevision,
|
|
};
|
|
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<K, V>(map: Map<K, V>, 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: WorkspaceSortItem, right: WorkspaceSortItem): number {
|
|
return (
|
|
left.workspaceName.localeCompare(right.workspaceName, undefined, {
|
|
numeric: true,
|
|
sensitivity: "base",
|
|
}) || left.workspaceKey.localeCompare(right.workspaceKey)
|
|
);
|
|
}
|
|
|
|
function workspaceSortTime(
|
|
workspace: WorkspaceDescriptor,
|
|
sort: WorkspaceSort | undefined,
|
|
): number {
|
|
if (!sort) return 0;
|
|
const time =
|
|
sort === "lastPrompt"
|
|
? (workspace.lastUserMessageAt ?? workspace.createdAt)
|
|
: workspace.createdAt;
|
|
return time?.getTime() ?? 0;
|
|
}
|
|
|
|
function compareWorkspaceTimes(left: WorkspaceSortItem, right: WorkspaceSortItem): number {
|
|
return right.sortTime - left.sortTime || left.workspaceKey.localeCompare(right.workspaceKey);
|
|
}
|