feat(projects): add multiple projects at once in the directory picker (#2877)
feat(projects): add multiple projects at once in the directory picker
This commit is contained in:
@@ -151,6 +151,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
const addProject = useProjectsStore((s) => s.addProject);
|
||||
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
|
||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||
const addProjects = useProjectsStore((s) => s.addProjects);
|
||||
const gitIdentityProfiles = useGitIdentitiesStore((s) => s.profiles);
|
||||
const globalGitIdentity = useGitIdentitiesStore((s) => s.globalIdentity);
|
||||
const defaultGitIdentityId = useGitIdentitiesStore((s) => s.defaultGitIdentityId);
|
||||
@@ -177,6 +178,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
const [cloneRemoteUrl, setCloneRemoteUrl] = React.useState('');
|
||||
const [selectedGitIdentityId, setSelectedGitIdentityId] = React.useState<string | null>(null);
|
||||
const [showHidden, setShowHidden] = React.useState(false);
|
||||
const [selectedPaths, setSelectedPaths] = React.useState<string[]>([]);
|
||||
|
||||
const explorerRootDirectory = dialogHomeDirectory || homeDirectory;
|
||||
|
||||
@@ -197,6 +199,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
setCloneRemoteUrl('');
|
||||
setSelectedGitIdentityId(null);
|
||||
setShowHidden(false);
|
||||
setSelectedPaths([]);
|
||||
requestAnimationFrame(() => focusPathInput(inputRef.current));
|
||||
|
||||
let cancelled = false;
|
||||
@@ -330,6 +333,27 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
setHighlightedIndex(0);
|
||||
}, [query, rows.length]);
|
||||
|
||||
// Selections apply to the currently browsed directory: navigating into
|
||||
// another folder clears the pending batch so the primary action always
|
||||
// reflects the visible picker state.
|
||||
React.useEffect(() => {
|
||||
setSelectedPaths([]);
|
||||
}, [browseDirectoryAbsolutePath]);
|
||||
|
||||
const selectionPaths = React.useMemo(
|
||||
() => selectedPaths.filter((path) => {
|
||||
const normalized = normalizeDirectoryPath(path);
|
||||
return Boolean(normalized && !addedProjectPaths.has(normalized));
|
||||
}),
|
||||
[addedProjectPaths, selectedPaths]
|
||||
);
|
||||
|
||||
const togglePathSelection = React.useCallback((path: string) => {
|
||||
setSelectedPaths((prev) => (
|
||||
prev.includes(path) ? prev.filter((entry) => entry !== path) : [...prev, path]
|
||||
));
|
||||
}, []);
|
||||
|
||||
const targetPath = React.useMemo(() => {
|
||||
if (!explorerRootDirectory) return '';
|
||||
return trimTrailingSeparators(displayPathToAbsolutePath(query, explorerRootDirectory));
|
||||
@@ -351,28 +375,29 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
);
|
||||
const canAddProject = !isConfirming
|
||||
&& !isOpeningFinder
|
||||
&& !isAlreadyAdded
|
||||
&& browseErrorReason !== 'os-permission'
|
||||
&& browseErrorReason !== 'invalid-response'
|
||||
&& browseErrorReason !== 'unknown'
|
||||
&& Boolean(targetPath);
|
||||
&& ((!isCloneMode && selectionPaths.length > 0) || (!isAlreadyAdded && Boolean(targetPath)));
|
||||
const canSubmitClone = canAddProject && cloneRemoteUrl.trim().length > 0;
|
||||
const highlightedRow = rows[highlightedIndex] ?? null;
|
||||
const hasHighlightedBrowseItem = Boolean(
|
||||
highlightedRow && (highlightedRow.type === 'up' || highlightedRow.type === 'directory')
|
||||
);
|
||||
const submitModifierLabel = formatShortcutForDisplay('mod');
|
||||
const submitActionLabel = isAlreadyAdded
|
||||
? t('directoryExplorerDialog.actions.alreadyAdded')
|
||||
: isCloneMode
|
||||
? isConfirming
|
||||
? t('directoryExplorerDialog.actions.cloning')
|
||||
: t('directoryExplorerDialog.actions.cloneAndAdd')
|
||||
: isConfirming
|
||||
? t('directoryExplorerDialog.actions.adding')
|
||||
: shouldCreateTarget
|
||||
? t('directoryExplorerDialog.actions.createAndAdd')
|
||||
: t('directoryExplorerDialog.actions.addProject');
|
||||
const submitActionLabel = !isCloneMode && selectionPaths.length > 0
|
||||
? t('directoryExplorerDialog.actions.addSelected')
|
||||
: isAlreadyAdded
|
||||
? t('directoryExplorerDialog.actions.alreadyAdded')
|
||||
: isCloneMode
|
||||
? isConfirming
|
||||
? t('directoryExplorerDialog.actions.cloning')
|
||||
: t('directoryExplorerDialog.actions.cloneAndAdd')
|
||||
: isConfirming
|
||||
? t('directoryExplorerDialog.actions.adding')
|
||||
: shouldCreateTarget
|
||||
? t('directoryExplorerDialog.actions.createAndAdd')
|
||||
: t('directoryExplorerDialog.actions.addProject');
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const button = addButtonRef.current;
|
||||
@@ -429,9 +454,17 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
}, [addProject, addedProjectPaths, openProjectDraft, t]);
|
||||
|
||||
const finalizeSelection = React.useCallback(async (target: string) => {
|
||||
if (!target || isConfirming) return;
|
||||
if (isConfirming) return;
|
||||
const normalized = normalizeDirectoryPath(target);
|
||||
if (normalized && addedProjectPaths.has(normalized)) return;
|
||||
// Batch selections supersede the single-target flow. Only the single-target
|
||||
// flow is blocked by an already-added (or missing) directory.
|
||||
const selectionToAdd = isCloneMode
|
||||
? []
|
||||
: selectedPaths.filter((path) => {
|
||||
const selectionNormalized = normalizeDirectoryPath(path);
|
||||
return Boolean(selectionNormalized && !addedProjectPaths.has(selectionNormalized));
|
||||
});
|
||||
if (selectionToAdd.length === 0 && (!target || (normalized && addedProjectPaths.has(normalized)))) return;
|
||||
let selectedTarget = target;
|
||||
|
||||
setIsConfirming(true);
|
||||
@@ -449,6 +482,21 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
gitIdentityId: selectedGitIdentity?.id ?? null,
|
||||
});
|
||||
selectedTarget = result.path;
|
||||
} else if (selectionToAdd.length > 0) {
|
||||
// Batch path wins over single-target create: with checkboxes ticked,
|
||||
// the user wants the selections added, not a fresh directory created
|
||||
// for whatever happens to be typed in the filter.
|
||||
const added = await addProjects(selectionToAdd);
|
||||
if (added.length === 0) {
|
||||
toast.error(t('directoryExplorerDialog.toast.failedToAddProject'), {
|
||||
description: t('directoryExplorerDialog.toast.selectValidDirectoryPath'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast.success(t('directoryExplorerDialog.toast.addedProjects', { count: added.length }));
|
||||
setSelectedPaths([]);
|
||||
handleClose();
|
||||
return;
|
||||
} else if (shouldCreateSelection) {
|
||||
await opencodeClient.createDirectory(target, { asProject: true });
|
||||
}
|
||||
@@ -467,7 +515,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
} finally {
|
||||
setIsConfirming(false);
|
||||
}
|
||||
}, [addProject, addedProjectPaths, cloneRemoteUrl, isCloneMode, isConfirming, openProjectDraft, selectedGitIdentity?.id, shouldCreateTarget, targetPath, t]);
|
||||
}, [addProject, addProjects, addedProjectPaths, cloneRemoteUrl, handleClose, isCloneMode, isConfirming, openProjectDraft, selectedGitIdentity?.id, selectedPaths, shouldCreateTarget, targetPath, t]);
|
||||
|
||||
const browseToDisplayPath = React.useCallback((displayPath: string) => {
|
||||
setQuery(ensureBrowseDirectoryPath(displayPath));
|
||||
@@ -508,6 +556,9 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear pending selections so the Finder-sourced target is honored
|
||||
// instead of silently being absorbed by the batch branch.
|
||||
setSelectedPaths([]);
|
||||
await finalizeSelection(result.path);
|
||||
} catch (error) {
|
||||
toast.error(t('directoryExplorerDialog.toast.failedToSelectDirectory'), {
|
||||
@@ -529,6 +580,20 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
setHighlightedIndex((index) => Math.max(0, index - 1));
|
||||
return;
|
||||
}
|
||||
if (event.key === ' ') {
|
||||
// Only treat Space as a selection toggle when the user is actively
|
||||
// browsing a directory (trailing slash or no filter typing). When
|
||||
// the input is in path-entry mode, Space is a literal character
|
||||
// and must reach the input value.
|
||||
if (hasTrailingPathSeparator(query)) {
|
||||
event.preventDefault();
|
||||
if (highlightedRow && highlightedRow.type === 'directory' && !highlightedRow.disabled) {
|
||||
togglePathSelection(highlightedRow.path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (isPrimaryModifierPressed(event)) {
|
||||
@@ -544,7 +609,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
event.preventDefault();
|
||||
handleClose();
|
||||
}
|
||||
}, [executeRow, finalizeSelection, handleClose, hasHighlightedBrowseItem, highlightedRow, query, rows.length, targetPath]);
|
||||
}, [executeRow, finalizeSelection, handleClose, hasHighlightedBrowseItem, highlightedRow, query, rows.length, targetPath, togglePathSelection]);
|
||||
|
||||
const showHiddenToggle = (
|
||||
<button
|
||||
@@ -684,15 +749,31 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
{t('directoryExplorerDialog.browse.addedBadge')}
|
||||
</span>
|
||||
) : row.type === 'directory' ? (
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => handleQuickAdd(event, row.path)}
|
||||
className="flex-shrink-0 rounded-full p-1 text-muted-foreground transition-colors hover:bg-interactive-hover/60 hover:text-foreground"
|
||||
title={t('directoryExplorerDialog.browse.quickAdd')}
|
||||
>
|
||||
<Icon name="add" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => { event.stopPropagation(); togglePathSelection(row.path); }}
|
||||
title={t('directoryExplorerDialog.browse.selectForAdd')}
|
||||
aria-label={t('directoryExplorerDialog.browse.selectForAdd')}
|
||||
aria-pressed={selectedPaths.includes(row.path)}
|
||||
className="flex-shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-interactive-hover/60 hover:text-foreground"
|
||||
>
|
||||
<Icon
|
||||
name={selectedPaths.includes(row.path) ? 'checkbox' : 'checkbox-blank'}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => handleQuickAdd(event, row.path)}
|
||||
className="flex-shrink-0 rounded-full p-1 text-muted-foreground transition-colors hover:bg-interactive-hover/60 hover:text-foreground"
|
||||
title={t('directoryExplorerDialog.browse.quickAdd')}
|
||||
>
|
||||
<Icon name="add" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
@@ -738,7 +819,16 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
{isOpeningFinder ? t('directoryExplorerDialog.actions.openingFinder') : t('directoryExplorerDialog.actions.openInFinder')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button variant="ghost" size="xs" onClick={() => setIsCloneMode((value) => !value)} disabled={isConfirming || isOpeningFinder} className={cn(isMobile && 'flex-1')}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setIsCloneMode((value) => !value);
|
||||
setSelectedPaths([]);
|
||||
}}
|
||||
disabled={isConfirming || isOpeningFinder}
|
||||
className={cn(isMobile && 'flex-1')}
|
||||
>
|
||||
{isCloneMode ? t('directoryExplorerDialog.actions.addLocalProject') : t('directoryExplorerDialog.actions.cloneRepository')}
|
||||
</Button>
|
||||
{isMobile ? (
|
||||
|
||||
@@ -1624,6 +1624,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.actions.openInFinder': 'Im Finder öffnen',
|
||||
'directoryExplorerDialog.actions.adding': 'Füge hinzu...',
|
||||
'directoryExplorerDialog.actions.addProject': 'Projekt hinzufügen',
|
||||
'directoryExplorerDialog.actions.addSelected': 'Ausgewählte hinzufügen',
|
||||
'directoryExplorerDialog.actions.addLocalProject': 'Lokales Projekt hinzufügen',
|
||||
'directoryExplorerDialog.actions.cloneRepository': 'Repository klonen',
|
||||
'directoryExplorerDialog.actions.cloneAndAdd': 'Klonen & hinzufügen',
|
||||
@@ -1641,6 +1642,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.browse.parentDirectory': 'Übergeordnetes Verzeichnis',
|
||||
'directoryExplorerDialog.browse.addedBadge': 'Hinzugefügt',
|
||||
'directoryExplorerDialog.browse.quickAdd': 'Hinzufügen',
|
||||
'directoryExplorerDialog.browse.selectForAdd': 'Zum Hinzufügen auswählen',
|
||||
'directoryExplorerDialog.footer.navigate': 'Navigieren',
|
||||
'directoryExplorerDialog.footer.select': 'Auswählen',
|
||||
'directoryExplorerDialog.footer.add': 'Hinzufügen',
|
||||
@@ -1649,6 +1651,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Desktop hat den Zugriff auf das Verzeichnis verweigert.',
|
||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Fehler beim Öffnen des Verzeichnisses',
|
||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop konnte keinen Dateizugriff gewähren.',
|
||||
'directoryExplorerDialog.toast.addedProjects': '{count} Projekt(e) hinzugefügt',
|
||||
'directoryExplorerDialog.toast.failedToAddProject': 'Fehler beim Hinzufügen des Projekts',
|
||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Geben Sie eine Repository-URL ein, bevor Sie klonen.',
|
||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Bitte wählen Sie einen gültigen Verzeichnispfad aus.',
|
||||
|
||||
@@ -1825,6 +1825,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.actions.openInFinder': 'Open in Finder',
|
||||
'directoryExplorerDialog.actions.adding': 'Adding...',
|
||||
'directoryExplorerDialog.actions.addProject': 'Add project',
|
||||
'directoryExplorerDialog.actions.addSelected': 'Add selected',
|
||||
'directoryExplorerDialog.actions.addLocalProject': 'Add local project',
|
||||
'directoryExplorerDialog.actions.cloneRepository': 'Clone repository',
|
||||
'directoryExplorerDialog.actions.cloneAndAdd': 'Clone & add',
|
||||
@@ -1842,6 +1843,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.browse.parentDirectory': 'Parent directory',
|
||||
'directoryExplorerDialog.browse.addedBadge': 'Added',
|
||||
'directoryExplorerDialog.browse.quickAdd': 'Add',
|
||||
'directoryExplorerDialog.browse.selectForAdd': 'Select for add',
|
||||
'directoryExplorerDialog.footer.navigate': 'Navigate',
|
||||
'directoryExplorerDialog.footer.select': 'Select',
|
||||
'directoryExplorerDialog.footer.add': 'Add',
|
||||
@@ -1850,6 +1852,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Desktop denied directory access.',
|
||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Failed to open directory',
|
||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop could not grant file access.',
|
||||
'directoryExplorerDialog.toast.addedProjects': 'Added {count} project(s)',
|
||||
'directoryExplorerDialog.toast.failedToAddProject': 'Failed to add project',
|
||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Enter a repository URL before cloning.',
|
||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Please select a valid directory path.',
|
||||
|
||||
@@ -1803,6 +1803,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"directoryExplorerDialog.actions.openInFinder": "Abrir en Finder",
|
||||
"directoryExplorerDialog.actions.adding": "Añadiendo...",
|
||||
"directoryExplorerDialog.actions.addProject": "Añadir proyecto",
|
||||
"directoryExplorerDialog.actions.addSelected": "Añadir seleccionados",
|
||||
"directoryExplorerDialog.actions.addLocalProject": "Añadir proyecto local",
|
||||
"directoryExplorerDialog.actions.cloneRepository": "Clonar repositorio",
|
||||
"directoryExplorerDialog.actions.cloneAndAdd": "Clonar y añadir",
|
||||
@@ -1820,6 +1821,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"directoryExplorerDialog.browse.parentDirectory": "Directorio padre",
|
||||
"directoryExplorerDialog.browse.addedBadge": "Añadido",
|
||||
"directoryExplorerDialog.browse.quickAdd": "Añadir",
|
||||
"directoryExplorerDialog.browse.selectForAdd": "Seleccionar para añadir",
|
||||
"directoryExplorerDialog.footer.navigate": "Navegar",
|
||||
"directoryExplorerDialog.footer.select": "Seleccionar",
|
||||
"directoryExplorerDialog.footer.add": "Añadir",
|
||||
@@ -1828,6 +1830,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"directoryExplorerDialog.toast.desktopDeniedAccess": "El escritorio denegó el acceso al directorio.",
|
||||
"directoryExplorerDialog.toast.failedToOpenDirectory": "No se pudo abrir el directorio",
|
||||
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "El escritorio no pudo otorgar acceso al archivo.",
|
||||
"directoryExplorerDialog.toast.addedProjects": "Se añadieron {count} proyecto(s)",
|
||||
"directoryExplorerDialog.toast.failedToAddProject": "No se pudo añadir el proyecto",
|
||||
"directoryExplorerDialog.toast.cloneUrlRequired": "Introduce una URL de repositorio antes de clonar.",
|
||||
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Por favor selecciona una ruta de directorio válida.",
|
||||
|
||||
@@ -1582,6 +1582,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.actions.openInFinder': 'Ouvrir dans le Finder',
|
||||
'directoryExplorerDialog.actions.adding': 'Ajout...',
|
||||
'directoryExplorerDialog.actions.addProject': 'Ajouter un projet',
|
||||
'directoryExplorerDialog.actions.addSelected': 'Ajouter la sélection',
|
||||
'directoryExplorerDialog.actions.addLocalProject': 'Ajouter un projet local',
|
||||
'directoryExplorerDialog.actions.cloneRepository': 'Cloner le dépôt',
|
||||
'directoryExplorerDialog.actions.cloneAndAdd': 'Cloner et ajouter',
|
||||
@@ -1599,6 +1600,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.browse.parentDirectory': 'Annuaire parent',
|
||||
'directoryExplorerDialog.browse.addedBadge': 'Ajouté',
|
||||
'directoryExplorerDialog.browse.quickAdd': 'Ajouter',
|
||||
'directoryExplorerDialog.browse.selectForAdd': 'Sélectionner pour ajouter',
|
||||
'directoryExplorerDialog.footer.navigate': 'Naviguer',
|
||||
'directoryExplorerDialog.footer.select': 'Sélectionner',
|
||||
'directoryExplorerDialog.footer.add': 'Ajouter',
|
||||
@@ -1607,6 +1609,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Le bureau a refusé l\'accès au répertoire.',
|
||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Échec de l\'ouverture du répertoire',
|
||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop n\'a pas pu accorder l\'accès aux fichiers.',
|
||||
'directoryExplorerDialog.toast.addedProjects': '{count} projet(s) ajouté(s)',
|
||||
'directoryExplorerDialog.toast.failedToAddProject': 'Échec de l\'ajout du projet',
|
||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Entrez dans un dépôt URL avant le clonage.',
|
||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Veuillez sélectionner un chemin de répertoire valide.',
|
||||
|
||||
@@ -1821,6 +1821,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.actions.openInFinder': 'Finderで開く',
|
||||
'directoryExplorerDialog.actions.adding': '追加中...',
|
||||
'directoryExplorerDialog.actions.addProject': 'プロジェクトを追加',
|
||||
'directoryExplorerDialog.actions.addSelected': '選択したものを追加',
|
||||
'directoryExplorerDialog.actions.addLocalProject': 'ローカルプロジェクトを追加',
|
||||
'directoryExplorerDialog.actions.cloneRepository': 'リポジトリをクローン',
|
||||
'directoryExplorerDialog.actions.cloneAndAdd': 'クローンして追加',
|
||||
@@ -1838,6 +1839,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.browse.parentDirectory': '親ディレクトリ',
|
||||
'directoryExplorerDialog.browse.addedBadge': '追加済み',
|
||||
'directoryExplorerDialog.browse.quickAdd': '追加',
|
||||
'directoryExplorerDialog.browse.selectForAdd': '追加するものを選択',
|
||||
'directoryExplorerDialog.footer.navigate': '移動',
|
||||
'directoryExplorerDialog.footer.select': '選択',
|
||||
'directoryExplorerDialog.footer.add': '追加',
|
||||
@@ -1846,6 +1848,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'デスクトップがディレクトリアクセスを拒否しました。',
|
||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'ディレクトリを開けませんでした',
|
||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'デスクトップがファイルアクセスを許可できませんでした。',
|
||||
'directoryExplorerDialog.toast.addedProjects': '{count}件のプロジェクトを追加しました',
|
||||
'directoryExplorerDialog.toast.failedToAddProject': 'プロジェクトの追加に失敗しました',
|
||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'クローンする前にリポジトリURLを入力してください。',
|
||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': '有効なディレクトリパスを選択してください。',
|
||||
|
||||
@@ -1827,6 +1827,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.actions.openInFinder': 'Finder에서 열기',
|
||||
'directoryExplorerDialog.actions.adding': '추가 중…',
|
||||
'directoryExplorerDialog.actions.addProject': '프로젝트 추가',
|
||||
'directoryExplorerDialog.actions.addSelected': '선택 항목 추가',
|
||||
'directoryExplorerDialog.actions.addLocalProject': '로컬 프로젝트 추가',
|
||||
'directoryExplorerDialog.actions.cloneRepository': '저장소 복제',
|
||||
'directoryExplorerDialog.actions.cloneAndAdd': '복제하고 추가',
|
||||
@@ -1844,6 +1845,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.browse.parentDirectory': '상위 디렉터리',
|
||||
'directoryExplorerDialog.browse.addedBadge': '추가됨',
|
||||
'directoryExplorerDialog.browse.quickAdd': '추가',
|
||||
'directoryExplorerDialog.browse.selectForAdd': '추가할 항목 선택',
|
||||
'directoryExplorerDialog.footer.navigate': '탐색',
|
||||
'directoryExplorerDialog.footer.select': '선택',
|
||||
'directoryExplorerDialog.footer.add': '추가',
|
||||
@@ -1852,6 +1854,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.toast.desktopDeniedAccess': '데스크톱에서 디렉터리 접근이 거부되었습니다.',
|
||||
'directoryExplorerDialog.toast.failedToOpenDirectory': '디렉터리를 열지 못했습니다',
|
||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '데스크톱에서 파일 접근 권한을 부여하지 못했습니다.',
|
||||
'directoryExplorerDialog.toast.addedProjects': '프로젝트 {count}개 추가됨',
|
||||
'directoryExplorerDialog.toast.failedToAddProject': '프로젝트 추가 실패',
|
||||
'directoryExplorerDialog.toast.cloneUrlRequired': '복제하기 전에 저장소 URL을 입력하세요.',
|
||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': '유효한 디렉터리 경로를 선택하세요.',
|
||||
|
||||
@@ -1890,6 +1890,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"diffView.scope.selectorAria": "Wybierz tryb zmian",
|
||||
'diffView.summary.changedFilesSingle': 'Zmieniono {count} plik',
|
||||
'directoryExplorerDialog.actions.addProject': 'Dodaj projekt',
|
||||
'directoryExplorerDialog.actions.addSelected': 'Dodaj zaznaczone',
|
||||
'directoryExplorerDialog.actions.addLocalProject': 'Dodaj projekt lokalny',
|
||||
'directoryExplorerDialog.actions.adding': 'Dodawanie...',
|
||||
'directoryExplorerDialog.actions.alreadyAdded': 'Już dodano',
|
||||
@@ -1901,6 +1902,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.actions.openingFinder': 'Otwieranie...',
|
||||
'directoryExplorerDialog.browse.addedBadge': 'Dodano',
|
||||
'directoryExplorerDialog.browse.quickAdd': 'Dodaj',
|
||||
'directoryExplorerDialog.browse.selectForAdd': 'Zaznacz do dodania',
|
||||
'directoryExplorerDialog.browse.directories': 'Katalogi',
|
||||
'directoryExplorerDialog.browse.empty': 'Brak pasujących katalogów.',
|
||||
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber potrzebuje dostępu do tego folderu.',
|
||||
@@ -1919,6 +1921,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.title': 'Dodaj katalog projektu',
|
||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Aplikacja desktopowa nie mogła przyznać dostępu do pliku.',
|
||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Aplikacja desktopowa odmówiła dostępu do katalogu.',
|
||||
'directoryExplorerDialog.toast.addedProjects': 'Dodano {count} projektów',
|
||||
'directoryExplorerDialog.toast.failedToAddProject': 'Nie udało się dodać projektu',
|
||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Wpisz URL repozytorium przed klonowaniem.',
|
||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Nie udało się otworzyć katalogu',
|
||||
|
||||
@@ -1803,6 +1803,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"directoryExplorerDialog.actions.openInFinder": "Abrir no Finder",
|
||||
"directoryExplorerDialog.actions.adding": "Adicionando...",
|
||||
"directoryExplorerDialog.actions.addProject": "Adicionar projeto",
|
||||
"directoryExplorerDialog.actions.addSelected": "Adicionar selecionados",
|
||||
"directoryExplorerDialog.actions.addLocalProject": "Adicionar projeto local",
|
||||
"directoryExplorerDialog.actions.cloneRepository": "Clonar repositório",
|
||||
"directoryExplorerDialog.actions.cloneAndAdd": "Clonar e adicionar",
|
||||
@@ -1820,6 +1821,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"directoryExplorerDialog.browse.parentDirectory": "Diretório pai",
|
||||
"directoryExplorerDialog.browse.addedBadge": "Adicionado",
|
||||
"directoryExplorerDialog.browse.quickAdd": "Adicionar",
|
||||
"directoryExplorerDialog.browse.selectForAdd": "Selecionar para adicionar",
|
||||
"directoryExplorerDialog.footer.navigate": "Navegar",
|
||||
"directoryExplorerDialog.footer.select": "Selecionar",
|
||||
"directoryExplorerDialog.footer.add": "Adicionar",
|
||||
@@ -1828,6 +1830,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"directoryExplorerDialog.toast.desktopDeniedAccess": "O desktop negou o acesso ao diretório.",
|
||||
"directoryExplorerDialog.toast.failedToOpenDirectory": "Não foi possível abrir o diretório",
|
||||
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "O desktop não pôde conceder acesso ao arquivo.",
|
||||
"directoryExplorerDialog.toast.addedProjects": "Foram adicionados {count} projeto(s)",
|
||||
"directoryExplorerDialog.toast.failedToAddProject": "Não foi possível adicionar o projeto",
|
||||
"directoryExplorerDialog.toast.cloneUrlRequired": "Insira uma URL de repositório antes de clonar.",
|
||||
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Selecione um caminho de diretório válido.",
|
||||
|
||||
@@ -1787,6 +1787,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.actions.openInFinder': 'Finder\'da aç',
|
||||
'directoryExplorerDialog.actions.adding': 'Ekleniyor...',
|
||||
'directoryExplorerDialog.actions.addProject': 'Proje ekle',
|
||||
'directoryExplorerDialog.actions.addSelected': 'Seçilenleri ekle',
|
||||
'directoryExplorerDialog.actions.addLocalProject': 'Yerel proje ekle',
|
||||
'directoryExplorerDialog.actions.cloneRepository': 'Depoyu klonla',
|
||||
'directoryExplorerDialog.actions.cloneAndAdd': 'Klonla ve ekle',
|
||||
@@ -1803,6 +1804,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.browse.retry': 'Yeniden dene',
|
||||
'directoryExplorerDialog.browse.parentDirectory': 'Üst dizin',
|
||||
'directoryExplorerDialog.browse.addedBadge': 'Eklendi',
|
||||
'directoryExplorerDialog.browse.selectForAdd': 'Eklemek için seç',
|
||||
'directoryExplorerDialog.browse.quickAdd': 'Ekle',
|
||||
'directoryExplorerDialog.footer.navigate': 'Gezin',
|
||||
'directoryExplorerDialog.footer.select': 'Seç',
|
||||
@@ -1812,6 +1814,7 @@ export const dict = {
|
||||
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Masaüstü uygulaması dizin erişimini reddetti.',
|
||||
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Dizin açılamadı',
|
||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Masaüstü uygulaması dosya erişimi veremedi.',
|
||||
'directoryExplorerDialog.toast.addedProjects': '{count} proje eklendi',
|
||||
'directoryExplorerDialog.toast.failedToAddProject': 'Proje eklenemedi',
|
||||
'directoryExplorerDialog.toast.cloneUrlRequired': 'Klonlamadan önce bir depo URL\'si girin.',
|
||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Geçerli bir dizin yolu seçin.',
|
||||
|
||||
@@ -1803,6 +1803,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"directoryExplorerDialog.actions.openInFinder": "Відкрити у Finder",
|
||||
"directoryExplorerDialog.actions.adding": "Додавання...",
|
||||
"directoryExplorerDialog.actions.addProject": "Додати проєкт",
|
||||
"directoryExplorerDialog.actions.addSelected": "Додати вибрані",
|
||||
"directoryExplorerDialog.actions.addLocalProject": "Додати локальний проєкт",
|
||||
"directoryExplorerDialog.actions.cloneRepository": "Клонувати репозиторій",
|
||||
"directoryExplorerDialog.actions.cloneAndAdd": "Клонувати й додати",
|
||||
@@ -1820,6 +1821,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"directoryExplorerDialog.browse.parentDirectory": "Батьківський каталог",
|
||||
"directoryExplorerDialog.browse.addedBadge": "Додано",
|
||||
"directoryExplorerDialog.browse.quickAdd": "Додати",
|
||||
"directoryExplorerDialog.browse.selectForAdd": "Вибрати для додавання",
|
||||
"directoryExplorerDialog.footer.navigate": "Навігація",
|
||||
"directoryExplorerDialog.footer.select": "Вибрати",
|
||||
"directoryExplorerDialog.footer.add": "Додати",
|
||||
@@ -1828,6 +1830,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"directoryExplorerDialog.toast.desktopDeniedAccess": "Десктопному застосунку заборонено доступ до каталогу.",
|
||||
"directoryExplorerDialog.toast.failedToOpenDirectory": "Не вдалося відкрити каталог",
|
||||
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "Десктопний застосунок не зміг надати доступ до файлу.",
|
||||
"directoryExplorerDialog.toast.addedProjects": "Додано {count} проєкт(и)",
|
||||
"directoryExplorerDialog.toast.failedToAddProject": "Не вдалося додати проєкт",
|
||||
"directoryExplorerDialog.toast.cloneUrlRequired": "Введіть URL репозиторію перед клонуванням.",
|
||||
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Виберіть правильний шлях до каталогу.",
|
||||
|
||||
@@ -1791,6 +1791,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.actions.openInFinder': '在 Finder 中打开',
|
||||
'directoryExplorerDialog.actions.adding': '添加中...',
|
||||
'directoryExplorerDialog.actions.addProject': '添加项目',
|
||||
'directoryExplorerDialog.actions.addSelected': '添加所选项目',
|
||||
'directoryExplorerDialog.actions.addLocalProject': '添加本地项目',
|
||||
'directoryExplorerDialog.actions.cloneRepository': '克隆仓库',
|
||||
'directoryExplorerDialog.actions.cloneAndAdd': '克隆并添加',
|
||||
@@ -1808,6 +1809,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.browse.parentDirectory': '上级目录',
|
||||
'directoryExplorerDialog.browse.addedBadge': '已添加',
|
||||
'directoryExplorerDialog.browse.quickAdd': '添加',
|
||||
'directoryExplorerDialog.browse.selectForAdd': '选择以添加',
|
||||
'directoryExplorerDialog.footer.navigate': '导航',
|
||||
'directoryExplorerDialog.footer.select': '选择',
|
||||
'directoryExplorerDialog.footer.add': '添加',
|
||||
@@ -1816,6 +1818,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.toast.desktopDeniedAccess': '桌面端拒绝了目录访问。',
|
||||
'directoryExplorerDialog.toast.failedToOpenDirectory': '打开目录失败',
|
||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '桌面端无法授予文件访问权限。',
|
||||
'directoryExplorerDialog.toast.addedProjects': '已添加 {count} 个项目',
|
||||
'directoryExplorerDialog.toast.failedToAddProject': '添加项目失败',
|
||||
'directoryExplorerDialog.toast.cloneUrlRequired': '克隆前请输入仓库 URL。',
|
||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': '请选择有效的目录路径。',
|
||||
|
||||
@@ -1795,6 +1795,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.actions.openInFinder': '在 Finder 中開啟',
|
||||
'directoryExplorerDialog.actions.adding': '新增中...',
|
||||
'directoryExplorerDialog.actions.addProject': '新增專案',
|
||||
'directoryExplorerDialog.actions.addSelected': '新增所選項目',
|
||||
'directoryExplorerDialog.actions.addLocalProject': '新增本地專案',
|
||||
'directoryExplorerDialog.actions.cloneRepository': '複製儲存庫',
|
||||
'directoryExplorerDialog.actions.cloneAndAdd': '複製並新增',
|
||||
@@ -1812,6 +1813,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.browse.parentDirectory': '上層目錄',
|
||||
'directoryExplorerDialog.browse.addedBadge': '已新增',
|
||||
'directoryExplorerDialog.browse.quickAdd': '添加',
|
||||
'directoryExplorerDialog.browse.selectForAdd': '選取以新增',
|
||||
'directoryExplorerDialog.footer.navigate': '導覽',
|
||||
'directoryExplorerDialog.footer.select': '選擇',
|
||||
'directoryExplorerDialog.footer.add': '新增',
|
||||
@@ -1820,6 +1822,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'directoryExplorerDialog.toast.desktopDeniedAccess': '桌面端拒絕了目錄存取。',
|
||||
'directoryExplorerDialog.toast.failedToOpenDirectory': '開啟目錄失敗',
|
||||
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '桌面端無法授予檔案存取權限。',
|
||||
'directoryExplorerDialog.toast.addedProjects': '已新增 {count} 個專案',
|
||||
'directoryExplorerDialog.toast.failedToAddProject': '新增專案失敗',
|
||||
'directoryExplorerDialog.toast.cloneUrlRequired': '複製前請輸入儲存庫 URL。',
|
||||
'directoryExplorerDialog.toast.selectValidDirectoryPath': '請選擇有效的目錄路徑。',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
import type { ProjectEntry } from "@/lib/api/types"
|
||||
import type { DesktopSettings } from "@/lib/desktop"
|
||||
import { useProjectsStore } from "./useProjectsStore"
|
||||
import { useDirectoryStore } from "./useDirectoryStore"
|
||||
|
||||
describe("useProjectsStore settings synchronization", () => {
|
||||
test("treats a successful empty project snapshot as authoritative", () => {
|
||||
@@ -119,3 +120,53 @@ describe("useProjectsStore default model and thinking level", () => {
|
||||
expect(project?.defaultVariant).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe("useProjectsStore.addProjects", () => {
|
||||
const resetProjects = () => {
|
||||
useProjectsStore.setState({
|
||||
projects: [],
|
||||
activeProjectId: null,
|
||||
manualProjectOrder: [],
|
||||
})
|
||||
}
|
||||
|
||||
test("adds multiple new projects in one update and activates the first", async () => {
|
||||
resetProjects()
|
||||
|
||||
const added = await useProjectsStore.getState().addProjects(["/one", "/two", "/three"])
|
||||
|
||||
expect(added).toHaveLength(3)
|
||||
expect(useProjectsStore.getState().projects.map((p) => p.path)).toEqual(["/one", "/two", "/three"])
|
||||
expect(useProjectsStore.getState().activeProjectId).toBe(added[0].id)
|
||||
expect(added[0].addedAt).toBe(added[1].addedAt)
|
||||
})
|
||||
|
||||
test("skips already-added paths and duplicates within the batch", async () => {
|
||||
resetProjects()
|
||||
await useProjectsStore.getState().addProjects(["/one"])
|
||||
|
||||
const added = await useProjectsStore.getState().addProjects(["/one", "/two", "/two", "/one"])
|
||||
|
||||
expect(added).toHaveLength(1)
|
||||
expect(added[0].path).toBe("/two")
|
||||
expect(useProjectsStore.getState().projects.map((p) => p.path)).toEqual(["/one", "/two"])
|
||||
})
|
||||
|
||||
test("skips invalid paths and returns an empty array when nothing is addable", async () => {
|
||||
resetProjects()
|
||||
|
||||
const added = await useProjectsStore.getState().addProjects(["", " ", 42 as unknown as string])
|
||||
|
||||
expect(added).toEqual([])
|
||||
expect(useProjectsStore.getState().projects).toEqual([])
|
||||
})
|
||||
|
||||
test("normalizes paths (trailing separators, backslashes, tilde expansion)", async () => {
|
||||
resetProjects()
|
||||
|
||||
const added = await useProjectsStore.getState().addProjects(["/repo/", "C:\\repo", "~/project"])
|
||||
|
||||
const home = useDirectoryStore.getState().homeDirectory;
|
||||
expect(added.map((p) => p.path)).toEqual(["/repo", "C:/repo", home ? `${home}/project` : "~/project"])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -50,6 +50,7 @@ interface ProjectsStore {
|
||||
manualProjectOrder: string[];
|
||||
|
||||
addProject: (path: string, options?: { label?: string; id?: string }) => Promise<ProjectEntry | null>;
|
||||
addProjects: (paths: string[]) => Promise<ProjectEntry[]>;
|
||||
removeProject: (id: string) => void;
|
||||
setActiveProject: (id: string) => void;
|
||||
setActiveProjectIdOnly: (id: string) => void;
|
||||
@@ -653,6 +654,69 @@ export const useProjectsStore = create<ProjectsStore>()(
|
||||
return entry;
|
||||
},
|
||||
|
||||
addProjects: async (paths: string[]) => {
|
||||
if (isVSCodeProjectsRuntime) {
|
||||
// VS Code paths are added via runtimeApis.vscode.addWorkspaceFolder,
|
||||
// which is reached only by addProject. Iterate so valid selections
|
||||
// succeed instead of silently returning []. Dedupe by path so the
|
||||
// returned array mirrors the non-VS Code contract.
|
||||
const added: ProjectEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const path of paths) {
|
||||
if (seen.has(path)) continue;
|
||||
seen.add(path);
|
||||
const project = await get().addProject(path);
|
||||
if (project) {
|
||||
added.push(project);
|
||||
}
|
||||
}
|
||||
return added;
|
||||
}
|
||||
const current = get();
|
||||
const existingPaths = new Set(current.projects.map((project) => project.path));
|
||||
const now = Date.now();
|
||||
const entries: ProjectEntry[] = [];
|
||||
const seenPaths = new Set<string>();
|
||||
|
||||
for (const rawPath of paths) {
|
||||
const validation = get().validateProjectPath(rawPath);
|
||||
if (!validation.ok || !validation.normalizedPath) {
|
||||
continue;
|
||||
}
|
||||
const normalizedPath = validation.normalizedPath;
|
||||
if (existingPaths.has(normalizedPath) || seenPaths.has(normalizedPath)) {
|
||||
continue;
|
||||
}
|
||||
seenPaths.add(normalizedPath);
|
||||
entries.push({
|
||||
id: createProjectIdFromPath(normalizedPath),
|
||||
path: normalizedPath,
|
||||
label: deriveProjectLabel(normalizedPath),
|
||||
color: pickAutoColor([...current.projects, ...entries]),
|
||||
addedAt: now,
|
||||
lastOpenedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nextProjects = [...current.projects, ...entries];
|
||||
set({ projects: nextProjects });
|
||||
|
||||
if (streamDebugEnabled()) {
|
||||
console.info('[ProjectsStore] Added projects', entries);
|
||||
}
|
||||
|
||||
// Mirror addProject: the first newly added project becomes active.
|
||||
get().setActiveProject(entries[0].id);
|
||||
for (const entry of entries) {
|
||||
void get().discoverProjectIcon(entry.id);
|
||||
}
|
||||
return entries;
|
||||
},
|
||||
|
||||
removeProject: (id: string) => {
|
||||
if (isVSCodeProjectsRuntime) {
|
||||
return;
|
||||
|
||||
@@ -133,4 +133,44 @@ describe('issue #2582: addProject in the VS Code runtime', () => {
|
||||
expect(added).toBeNull();
|
||||
expect(useProjectsStore.getState().projects.find((p) => p.path === '/other/path')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('addProjects iterates addProject in the VS Code runtime so valid selections succeed', async () => {
|
||||
// Regression: addProjects used to return [] unconditionally for the
|
||||
// VS Code runtime, which made the batch-add path toast "Failed to
|
||||
// add project" even for valid selections. The fix calls
|
||||
// addWorkspaceFolder per path; we assert the host is invoked once
|
||||
// per selection (not skipped) and that any successful add returns
|
||||
// a non-null entry.
|
||||
const added = await useProjectsStore.getState().addProjects([
|
||||
'/home/user/project-a',
|
||||
'/home/user/project-b',
|
||||
]);
|
||||
|
||||
expect(addWorkspaceFolderCalls).toEqual([
|
||||
'/home/user/project-a',
|
||||
'/home/user/project-b',
|
||||
]);
|
||||
// The mock's addWorkspaceFolder returns the second entry keyed by
|
||||
// `path`, so project-a lands; project-b is not reflected in
|
||||
// projects because the mock's hardcoded return array doesn't
|
||||
// include it. The point of the test is the call sequence, not the
|
||||
// final projects state (covered by the dedicated addProject tests).
|
||||
expect(added.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('addProjects dedupes paths within a single batch in the VS Code runtime', async () => {
|
||||
// A path repeated within one batch must hit the extension host once,
|
||||
// not twice — mirrors the non-VS Code contract (seenPaths Set).
|
||||
addWorkspaceFolderCalls.length = 0;
|
||||
await useProjectsStore.getState().addProjects([
|
||||
'/home/user/project-a',
|
||||
'/home/user/project-a',
|
||||
'/home/user/project-b',
|
||||
]);
|
||||
|
||||
expect(addWorkspaceFolderCalls).toEqual([
|
||||
'/home/user/project-a',
|
||||
'/home/user/project-b',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user