diff --git a/src/renderer/components/team/dialogs/ProjectPathSelector.tsx b/src/renderer/components/team/dialogs/ProjectPathSelector.tsx index 7d899f9c..071b5fbc 100644 --- a/src/renderer/components/team/dialogs/ProjectPathSelector.tsx +++ b/src/renderer/components/team/dialogs/ProjectPathSelector.tsx @@ -8,6 +8,8 @@ import { Label } from '@renderer/components/ui/label'; import { cn } from '@renderer/lib/utils'; import { Check, FolderOpen } from 'lucide-react'; +import { buildProjectPathOptions } from './projectPathOptions'; + import type { Project } from '@shared/types'; function escapeRegExp(value: string): string { @@ -69,131 +71,134 @@ export const ProjectPathSelector = ({ projectsLoading, projectsError, fieldError, -}: ProjectPathSelectorProps): React.JSX.Element => ( -
- -
-
-
- - -
+}: ProjectPathSelectorProps): React.JSX.Element => { + const projectOptions = React.useMemo( + () => buildProjectPathOptions(projects, selectedProjectPath), + [projects, selectedProjectPath] + ); -
- {cwdMode === 'project' ? ( -
-
- -
- ({ - value: project.path, - label: project.name, - description: project.path, - }))} - value={selectedProjectPath} - onValueChange={onSelectedProjectPathChange} - placeholder={projectsLoading ? 'Loading projects...' : 'Select a project...'} - searchPlaceholder="Search project by name or path" - emptyMessage="Nothing found" - disabled={projectsLoading || projects.length === 0} - renderOption={(option, isSelected, query) => ( - <> - -
-

- {renderHighlightedText(option.label, query)} -

-

- {renderHighlightedText(option.description ?? '', query)} -

-
- - )} - /> + return ( +
+ +
+
+
+ + +
+ +
+ {cwdMode === 'project' ? ( +
+
+ +
+ ( + <> + +
+

+ {renderHighlightedText(option.label, query)} +

+

+ {renderHighlightedText(option.description ?? '', query)} +

+
+ + )} + /> +
+ {!selectedProjectPath ? ( +

+ Select a project from the list +

+ ) : null} + {projectsError ?

{projectsError}

: null} + {!projectsLoading && projectOptions.length === 0 ? ( +

+ No projects found, switch to custom path. +

+ ) : null}
- {!selectedProjectPath ? ( -

- Select a project from the list -

- ) : null} - {projectsError ?

{projectsError}

: null} - {!projectsLoading && projects.length === 0 ? ( -

- No projects found, switch to custom path. -

- ) : null} -
- ) : ( -
-
- - onCustomCwdChange(event.target.value)} - placeholder="/absolute/path/to/project" - /> - + })(); + }} + > + Browse + +
+

+ If the directory does not exist, it will be created automatically. +

-

- If the directory does not exist, it will be created automatically. -

-
- )} + )} +
+ {fieldError ? ( +

+ {fieldError} +

+ ) : null}
- {fieldError ? ( -

- {fieldError} -

- ) : null} -
-); + ); +}; diff --git a/src/renderer/components/team/dialogs/projectPathOptions.ts b/src/renderer/components/team/dialogs/projectPathOptions.ts new file mode 100644 index 00000000..579f17e8 --- /dev/null +++ b/src/renderer/components/team/dialogs/projectPathOptions.ts @@ -0,0 +1,45 @@ +import { normalizePath } from '@renderer/utils/pathNormalize'; + +import type { ComboboxOption } from '@renderer/components/ui/combobox'; +import type { Project } from '@shared/types'; + +function toProjectOption(project: Project): ComboboxOption { + return { + value: project.path, + label: project.name, + description: project.path, + }; +} + +/** + * 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: Project[], + preferredPath?: string +): ComboboxOption[] { + const options: ComboboxOption[] = []; + const optionIndexByNormalizedPath = new Map(); + const normalizedPreferredPath = preferredPath ? normalizePath(preferredPath) : null; + + for (const project of projects) { + 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; +} diff --git a/test/renderer/components/team/dialogs/projectPathOptions.test.ts b/test/renderer/components/team/dialogs/projectPathOptions.test.ts new file mode 100644 index 00000000..f59e99dd --- /dev/null +++ b/test/renderer/components/team/dialogs/projectPathOptions.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; + +import { buildProjectPathOptions } from '@renderer/components/team/dialogs/projectPathOptions'; + +import type { Project } from '@shared/types'; + +function createProject(overrides: Partial): Project { + return { + id: 'project-id', + name: 'project', + path: '/Users/test/project', + sessions: [], + totalSessions: 0, + createdAt: 1, + ...overrides, + }; +} + +describe('buildProjectPathOptions', () => { + it('removes duplicate projects that point to the same path', () => { + const options = buildProjectPathOptions([ + createProject({ + id: 'project-1', + name: 'lintai', + path: '/Users/belief/dev/projects/lintai', + }), + createProject({ + id: 'project-2', + name: 'lintai duplicate', + path: '/Users/belief/dev/projects/lintai', + }), + ]); + + expect(options).toEqual([ + { + value: '/Users/belief/dev/projects/lintai', + label: 'lintai', + description: '/Users/belief/dev/projects/lintai', + }, + ]); + }); + + it('prefers the currently selected variant when duplicate paths normalize equally', () => { + const options = buildProjectPathOptions( + [ + createProject({ + id: 'project-1', + name: 'LintAI', + path: '/Users/Belief/dev/projects/lintai', + }), + createProject({ + id: 'project-2', + name: 'lintai', + path: '/Users/belief/dev/projects/lintai/', + }), + ], + '/Users/belief/dev/projects/lintai/' + ); + + expect(options).toEqual([ + { + value: '/Users/belief/dev/projects/lintai/', + label: 'lintai', + description: '/Users/belief/dev/projects/lintai/', + }, + ]); + }); +});