agent-ecosystem/src/renderer/components/team/dialogs/projectPathOptions.ts
2026-05-16 20:03:30 +03:00

102 lines
3.2 KiB
TypeScript

import { normalizePath } from '@renderer/utils/pathNormalize';
import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath';
import type {
DashboardRecentProjectFilesystemState,
DashboardRecentProjectSource,
} from '@features/recent-projects/contracts';
import type { ComboboxOption } from '@renderer/components/ui/combobox';
import type { Project } from '@shared/types';
export interface ProjectPathProject extends Project {
discoverySource?: DashboardRecentProjectSource;
filesystemState?: DashboardRecentProjectFilesystemState;
}
export interface ProjectPathOptionMeta extends Record<string, unknown> {
discoverySource?: DashboardRecentProjectSource;
filesystemState?: DashboardRecentProjectFilesystemState;
}
function toProjectOption(project: ProjectPathProject): ComboboxOption {
const option: ComboboxOption = {
value: project.path,
label: project.name,
description: project.path,
};
if (project.filesystemState === 'deleted') {
option.disabled = true;
}
if (project.discoverySource !== undefined || project.filesystemState !== undefined) {
const meta: ProjectPathOptionMeta = {};
if (project.discoverySource !== undefined) {
meta.discoverySource = project.discoverySource;
}
if (project.filesystemState !== undefined) {
meta.filesystemState = project.filesystemState;
}
option.meta = meta;
}
return option;
}
export function isSelectableProjectPathProject(
project: Pick<ProjectPathProject, 'path' | 'filesystemState'>
): boolean {
return !isEphemeralProjectPath(project.path) && project.filesystemState !== 'deleted';
}
export function findProjectPathProjectByPath(
projects: readonly ProjectPathProject[],
projectPath: string
): ProjectPathProject | undefined {
const normalizedPath = normalizePath(projectPath);
return projects.find((project) => normalizePath(project.path) === normalizedPath);
}
export function isDeletedProjectPathSelection(
projects: readonly ProjectPathProject[],
projectPath: string
): boolean {
return findProjectPathProjectByPath(projects, projectPath)?.filesystemState === 'deleted';
}
/**
* Collapse duplicate project entries that resolve to the same filesystem path.
* This keeps combobox item values unique even when scanner sources overlap.
*/
export function buildProjectPathOptions(
projects: ProjectPathProject[],
preferredPath?: string
): ComboboxOption[] {
const options: ComboboxOption[] = [];
const optionIndexByNormalizedPath = new Map<string, number>();
const normalizedPreferredPath = preferredPath ? normalizePath(preferredPath) : null;
for (const project of projects) {
if (isEphemeralProjectPath(project.path)) {
continue;
}
const normalizedProjectPath = normalizePath(project.path);
const existingIndex = optionIndexByNormalizedPath.get(normalizedProjectPath);
if (existingIndex === undefined) {
optionIndexByNormalizedPath.set(normalizedProjectPath, options.length);
options.push(toProjectOption(project));
continue;
}
const shouldPreferCurrentOption =
normalizedPreferredPath === normalizedProjectPath && project.path === preferredPath;
if (shouldPreferCurrentOption) {
options[existingIndex] = toProjectOption(project);
}
}
return options;
}