feat: add repository cloning to project setup

Add project can clone repositories into a chosen folder
Command palette now opens the add project flow
Clone flow supports selecting Git identities
This commit is contained in:
Bohdan Triapitsyn
2026-05-07 01:10:17 +03:00
parent fd3557e5c3
commit d764ad6872
13 changed files with 419 additions and 126 deletions
@@ -11,9 +11,11 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
import { useFileSystemAccess } from '@/hooks/useFileSystemAccess';
import { cn } from '@/lib/utils';
import { toast } from '@/components/ui';
import { IdentityDropdown } from '@/components/views/git/GitHeader';
import {
RiArrowDownSLine,
RiArrowLeftSLine,
@@ -152,6 +154,12 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
const homeDirectory = useDirectoryStore((s) => s.homeDirectory);
const projects = useProjectsStore((s) => s.projects);
const addProject = useProjectsStore((s) => s.addProject);
const gitIdentityProfiles = useGitIdentitiesStore((s) => s.profiles);
const globalGitIdentity = useGitIdentitiesStore((s) => s.globalIdentity);
const defaultGitIdentityId = useGitIdentitiesStore((s) => s.defaultGitIdentityId);
const loadGitIdentityProfiles = useGitIdentitiesStore((s) => s.loadProfiles);
const loadGlobalGitIdentity = useGitIdentitiesStore((s) => s.loadGlobalIdentity);
const loadDefaultGitIdentityId = useGitIdentitiesStore((s) => s.loadDefaultGitIdentityId);
const showHidden = useDirectoryShowHidden();
const { isDesktop, requestAccess, startAccessing } = useFileSystemAccess();
const { isMobile } = useDeviceInfo();
@@ -167,6 +175,9 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
const [isConfirming, setIsConfirming] = React.useState(false);
const [isOpeningFinder, setIsOpeningFinder] = React.useState(false);
const [addButtonWidth, setAddButtonWidth] = React.useState(0);
const [isCloneMode, setIsCloneMode] = React.useState(false);
const [cloneRemoteUrl, setCloneRemoteUrl] = React.useState('');
const [selectedGitIdentityId, setSelectedGitIdentityId] = React.useState<string | null>(null);
const explorerRootDirectory = dialogHomeDirectory || homeDirectory;
@@ -183,6 +194,9 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
setHighlightedIndex(0);
setIsConfirming(false);
setIsOpeningFinder(false);
setIsCloneMode(false);
setCloneRemoteUrl('');
setSelectedGitIdentityId(null);
requestAnimationFrame(() => focusPathInput(inputRef.current));
let cancelled = false;
@@ -198,6 +212,42 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
};
}, [homeDirectory, open]);
React.useEffect(() => {
if (!open) return;
void loadGitIdentityProfiles();
void loadGlobalGitIdentity();
void loadDefaultGitIdentityId();
}, [loadDefaultGitIdentityId, loadGitIdentityProfiles, loadGlobalGitIdentity, open]);
const availableGitIdentities = React.useMemo(() => {
const unique = new Map<string, NonNullable<typeof globalGitIdentity>>();
if (globalGitIdentity) {
unique.set(globalGitIdentity.id, globalGitIdentity);
}
for (const profile of gitIdentityProfiles) {
unique.set(profile.id, profile);
}
return Array.from(unique.values());
}, [gitIdentityProfiles, globalGitIdentity]);
React.useEffect(() => {
if (!open || !isCloneMode || selectedGitIdentityId !== null) return;
const defaultId = typeof defaultGitIdentityId === 'string' ? defaultGitIdentityId.trim() : '';
if (defaultId && availableGitIdentities.some((identity) => identity.id === defaultId)) {
setSelectedGitIdentityId(defaultId);
return;
}
const firstSshIdentity = availableGitIdentities.find((identity) => identity.authType === 'ssh' || identity.sshKey);
if (firstSshIdentity) {
setSelectedGitIdentityId(firstSshIdentity.id);
}
}, [availableGitIdentities, defaultGitIdentityId, isCloneMode, open, selectedGitIdentityId]);
const selectedGitIdentity = React.useMemo(
() => availableGitIdentities.find((identity) => identity.id === selectedGitIdentityId) ?? null,
[availableGitIdentities, selectedGitIdentityId]
);
const browseDirectoryDisplayPath = React.useMemo(() => getBrowseDirectoryPath(query), [query]);
const browseFilterQuery = React.useMemo(
() => (hasTrailingPathSeparator(query) ? '' : getBrowseLeafPathSegment(query)),
@@ -294,6 +344,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
)
);
const canAddProject = !isConfirming && !isOpeningFinder && !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' && !highlightedRow.disabled))
@@ -303,6 +354,12 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
: 'Ctrl';
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');
@@ -345,14 +402,27 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
if (!target || isConfirming) return;
const normalized = normalizeDirectoryPath(target);
if (normalized && addedProjectPaths.has(normalized)) return;
let selectedTarget = target;
setIsConfirming(true);
try {
const shouldCreateSelection = shouldCreateTarget && normalizeDirectoryPath(target) === normalizeDirectoryPath(targetPath);
if (shouldCreateSelection) {
const shouldCreateSelection = !isCloneMode && shouldCreateTarget && normalizeDirectoryPath(target) === normalizeDirectoryPath(targetPath);
if (isCloneMode) {
const remoteUrl = cloneRemoteUrl.trim();
if (!remoteUrl) {
toast.error(t('directoryExplorerDialog.toast.cloneUrlRequired'));
return;
}
const result = await opencodeClient.cloneRepository({
remoteUrl,
destinationPath: target,
gitIdentityId: selectedGitIdentity?.id ?? null,
});
selectedTarget = result.path;
} else if (shouldCreateSelection) {
await opencodeClient.createDirectory(target, { allowOutsideWorkspace: true });
}
const added = addProject(target);
const added = addProject(selectedTarget);
if (!added) {
toast.error(t('directoryExplorerDialog.toast.failedToAddProject'), {
description: t('directoryExplorerDialog.toast.selectValidDirectoryPath'),
@@ -367,7 +437,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
} finally {
setIsConfirming(false);
}
}, [addProject, addedProjectPaths, handleClose, isConfirming, shouldCreateTarget, targetPath, t]);
}, [addProject, addedProjectPaths, cloneRemoteUrl, handleClose, isCloneMode, isConfirming, selectedGitIdentity?.id, shouldCreateTarget, targetPath, t]);
const browseToDisplayPath = React.useCallback((displayPath: string) => {
setQuery(ensureBrowseDirectoryPath(displayPath));
@@ -459,36 +529,59 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
);
const inputSection = (
<div className="relative px-2.5 py-1.5">
<RiFolderAddLine className="pointer-events-none absolute left-5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/80" />
<Input
ref={inputRef}
value={query}
onChange={(event) => setQuery(normalizeSeparators(event.target.value))}
onKeyDown={handleKeyDown}
placeholder={t('directoryExplorerDialog.pathInput.placeholder')}
className="border-transparent bg-transparent pl-9 font-mono typography-ui-label shadow-none focus-visible:ring-0"
style={!isMobile && addButtonWidth > 0 ? { paddingRight: `${addButtonWidth + 24}px` } : undefined}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
/>
{!isMobile ? (
<Button
ref={addButtonRef}
variant="outline"
size="xs"
tabIndex={-1}
className="absolute right-4 top-1/2 h-7 -translate-y-1/2 gap-1 px-2 typography-meta"
disabled={!canAddProject}
onMouseDown={(event) => event.preventDefault()}
onClick={() => void finalizeSelection(targetPath)}
title={submitActionLabel}
>
{submitActionLabel}
</Button>
<div className="px-2.5 py-1.5">
{isCloneMode ? (
<div className="mb-1.5 flex items-center gap-1.5">
<Input
value={cloneRemoteUrl}
onChange={(event) => setCloneRemoteUrl(event.target.value)}
placeholder={t('directoryExplorerDialog.clone.remoteUrlPlaceholder')}
className="min-w-0 flex-1 border-border/60 bg-[var(--surface-elevated)] font-mono typography-ui-label shadow-none"
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
/>
<IdentityDropdown
activeProfile={selectedGitIdentity}
identities={availableGitIdentities}
onSelect={(profile) => setSelectedGitIdentityId(profile.id)}
isApplying={isConfirming}
iconOnly
/>
</div>
) : null}
<div className="relative">
<RiFolderAddLine className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/80" />
<Input
ref={inputRef}
value={query}
onChange={(event) => setQuery(normalizeSeparators(event.target.value))}
onKeyDown={handleKeyDown}
placeholder={t('directoryExplorerDialog.pathInput.placeholder')}
className="border-transparent bg-transparent pl-9 font-mono typography-ui-label shadow-none focus-visible:ring-0"
style={!isMobile && addButtonWidth > 0 ? { paddingRight: `${addButtonWidth + 24}px` } : undefined}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
/>
{!isMobile ? (
<Button
ref={addButtonRef}
variant="outline"
size="xs"
tabIndex={-1}
className="absolute right-1.5 top-1/2 h-7 -translate-y-1/2 gap-1 px-2 typography-meta"
disabled={isCloneMode ? !canSubmitClone : !canAddProject}
onMouseDown={(event) => event.preventDefault()}
onClick={() => void finalizeSelection(targetPath)}
title={submitActionLabel}
>
{submitActionLabel}
</Button>
) : null}
</div>
</div>
);
@@ -577,10 +670,6 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
<RiCornerDownLeftLine className="h-3.5 w-3.5" />
{t('directoryExplorerDialog.footer.add')}
</span>
<span className="inline-flex items-center gap-1">
<span>Esc</span>
{t('directoryExplorerDialog.footer.close')}
</span>
</div>
);
@@ -589,15 +678,15 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
{!isMobile ? footerHints : null}
<div className={cn('flex w-full flex-row justify-end gap-2 sm:w-auto', isMobile && 'justify-stretch')}>
{isDesktop ? (
<Button variant="ghost" size="xs" onClick={handleOpenInFinder} disabled={isConfirming || isOpeningFinder}>
<Button variant="ghost" size="xs" onClick={handleOpenInFinder} disabled={isConfirming || isOpeningFinder || isCloneMode}>
{isOpeningFinder ? t('directoryExplorerDialog.actions.openingFinder') : t('directoryExplorerDialog.actions.openInFinder')}
</Button>
) : null}
<Button variant="ghost" size="xs" onClick={handleClose} disabled={isConfirming || isOpeningFinder} className={cn(isMobile && 'flex-1')}>
{t('directoryExplorerDialog.actions.cancel')}
<Button variant="ghost" size="xs" onClick={() => setIsCloneMode((value) => !value)} disabled={isConfirming || isOpeningFinder} className={cn(isMobile && 'flex-1')}>
{isCloneMode ? t('directoryExplorerDialog.actions.addLocalProject') : t('directoryExplorerDialog.actions.cloneRepository')}
</Button>
{isMobile ? (
<Button size="xs" onClick={() => void finalizeSelection(targetPath)} disabled={!canAddProject} className="flex-1">
<Button size="xs" onClick={() => void finalizeSelection(targetPath)} disabled={isCloneMode ? !canSubmitClone : !canAddProject} className="flex-1">
{submitActionLabel}
</Button>
) : null}
@@ -125,10 +125,6 @@ export const AboutDialog: React.FC<AboutDialogProps> = ({
)}
</div>
<p className="typography-meta text-muted-foreground">
{t('aboutDialog.tagline')}
</p>
<div className="flex flex-col items-center gap-2 pt-2">
<button
onClick={handleCopyDiagnostics}
@@ -31,6 +31,7 @@ import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import {
RiAddLine,
RiChatAi3Line,
RiFolderAddLine,
RiGitBranchLine,
RiLayoutLeftLine,
RiLayoutRightLine,
@@ -47,6 +48,7 @@ import { getSettingsNavIcon } from '@/components/views/SettingsView';
import { scoreByFuzzyQuery } from '@/lib/search/fuzzySearch';
import { truncatePathMiddle } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import { sessionEvents } from '@/lib/sessionEvents';
type CommandEntry = {
id: string;
@@ -168,6 +170,15 @@ export const CommandPalette: React.FC = () => {
void createWorktreeSession();
}),
},
{
id: 'add-project',
title: t('commandPalette.item.addProject'),
icon: <RiFolderAddLine className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.addProject'),
onSelect: run(() => {
sessionEvents.requestDirectoryDialog();
}),
},
{
id: 'toggle-sidebar',
title: isMobile
@@ -109,7 +109,7 @@ interface IdentityDropdownProps {
iconOnly?: boolean;
}
const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
export const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
activeProfile,
identities,
onSelect,
+8 -4
View File
@@ -422,7 +422,7 @@ export const dict = {
'gitView.commit.addGitmoji': 'Add gitmoji',
'gitView.commit.aiHighlights.insertAria': 'Insert aria label',
'gitView.commit.aiHighlights.insertTooltip': 'Insert tooltip',
'gitView.commit.aiHighlights.title': 'Title',
'gitView.commit.aiHighlights.title': 'Highlights',
'gitView.commit.commit': 'Commit',
'gitView.commit.commitAria': 'Commit aria label',
'gitView.commit.committing': 'Committing...',
@@ -1103,13 +1103,17 @@ export const dict = {
'directoryExplorerDialog.description': 'Choose a folder to add as a project.',
'directoryExplorerDialog.toggle.showHidden': 'Show hidden',
'directoryExplorerDialog.pathInput.placeholder': 'Enter path or select from tree...',
'directoryExplorerDialog.actions.cancel': 'Cancel',
'directoryExplorerDialog.actions.openingFinder': 'Opening...',
'directoryExplorerDialog.actions.openInFinder': 'Open in Finder',
'directoryExplorerDialog.actions.adding': 'Adding...',
'directoryExplorerDialog.actions.addProject': 'Add project',
'directoryExplorerDialog.actions.addLocalProject': 'Add local project',
'directoryExplorerDialog.actions.cloneRepository': 'Clone repository',
'directoryExplorerDialog.actions.cloneAndAdd': 'Clone & add',
'directoryExplorerDialog.actions.cloning': 'Cloning...',
'directoryExplorerDialog.actions.createAndAdd': 'Create & add',
'directoryExplorerDialog.actions.alreadyAdded': 'Already added',
'directoryExplorerDialog.clone.remoteUrlPlaceholder': 'Repository URL (HTTPS or SSH)',
'directoryExplorerDialog.browse.directories': 'Directories',
'directoryExplorerDialog.browse.loading': 'Loading directories...',
'directoryExplorerDialog.browse.empty': 'No matching directories.',
@@ -1118,13 +1122,13 @@ export const dict = {
'directoryExplorerDialog.footer.navigate': 'Navigate',
'directoryExplorerDialog.footer.select': 'Select',
'directoryExplorerDialog.footer.add': 'Add',
'directoryExplorerDialog.footer.close': 'Close',
'directoryExplorerDialog.shortcut.enter': 'Enter',
'directoryExplorerDialog.toast.unableToAccessDirectory': 'Unable to access directory',
'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.failedToAddProject': 'Failed to add project',
'directoryExplorerDialog.toast.cloneUrlRequired': 'Enter a repository URL before cloning.',
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Please select a valid directory path.',
'directoryExplorerDialog.toast.failedToSelectDirectory': 'Failed to select directory',
'directoryExplorerDialog.toast.unknownError': 'Unknown error occurred.',
@@ -1140,7 +1144,6 @@ export const dict = {
'directoryTree.section.pinned': 'Pinned',
'directoryTree.section.browse': 'Browse',
'aboutDialog.versionLabel': 'Version {version}',
'aboutDialog.tagline': 'Coding with agents, in a workspace built with care.',
'aboutDialog.actions.copyDiagnostics': 'Copy diagnostics',
'aboutDialog.actions.preparingDiagnostics': 'Preparing diagnostics...',
'aboutDialog.actions.diagnosticsCopied': 'Diagnostics copied',
@@ -1649,6 +1652,7 @@ export const dict = {
'commandPalette.empty.searchingFiles': 'Searching files...',
'commandPalette.item.newSession': 'New Session',
'commandPalette.item.newWorktreeDraft': 'New Worktree Draft',
'commandPalette.item.addProject': 'Add Project',
'commandPalette.item.showSessionSwitcher': 'Show Session Switcher',
'commandPalette.item.toggleSidebar': 'Toggle Sidebar',
'commandPalette.item.toggleRightSidebar': 'Toggle Right Sidebar',
+8 -4
View File
@@ -423,7 +423,7 @@ export const dict: Record<I18nKey, string> = {
"gitView.commit.addGitmoji": "Añadir gitmoji",
"gitView.commit.aiHighlights.insertAria": "Insertar",
"gitView.commit.aiHighlights.insertTooltip": "Insertar",
"gitView.commit.aiHighlights.title": "Título",
"gitView.commit.aiHighlights.title": "Puntos destacados",
"gitView.commit.commit": "Commit",
"gitView.commit.commitAria": "Commit",
"gitView.commit.committing": "Creando commit...",
@@ -1069,13 +1069,17 @@ export const dict: Record<I18nKey, string> = {
"directoryExplorerDialog.description": "Elige una carpeta para añadir como proyecto.",
"directoryExplorerDialog.toggle.showHidden": "Mostrar ocultos",
"directoryExplorerDialog.pathInput.placeholder": "Escribe la ruta o selecciona desde el árbol...",
"directoryExplorerDialog.actions.cancel": "Cancelar",
"directoryExplorerDialog.actions.openingFinder": "Abriendo...",
"directoryExplorerDialog.actions.openInFinder": "Abrir en Finder",
"directoryExplorerDialog.actions.adding": "Añadiendo...",
"directoryExplorerDialog.actions.addProject": "Añadir proyecto",
"directoryExplorerDialog.actions.addLocalProject": "Añadir proyecto local",
"directoryExplorerDialog.actions.cloneRepository": "Clonar repositorio",
"directoryExplorerDialog.actions.cloneAndAdd": "Clonar y añadir",
"directoryExplorerDialog.actions.cloning": "Clonando...",
"directoryExplorerDialog.actions.createAndAdd": "Crear y añadir",
"directoryExplorerDialog.actions.alreadyAdded": "Ya añadido",
"directoryExplorerDialog.clone.remoteUrlPlaceholder": "URL del repositorio (HTTPS o SSH)",
"directoryExplorerDialog.browse.directories": "Directorios",
"directoryExplorerDialog.browse.loading": "Cargando directorios...",
"directoryExplorerDialog.browse.empty": "No hay directorios coincidentes.",
@@ -1084,13 +1088,13 @@ export const dict: Record<I18nKey, string> = {
"directoryExplorerDialog.footer.navigate": "Navegar",
"directoryExplorerDialog.footer.select": "Seleccionar",
"directoryExplorerDialog.footer.add": "Añadir",
"directoryExplorerDialog.footer.close": "Cerrar",
"directoryExplorerDialog.shortcut.enter": "Enter",
"directoryExplorerDialog.toast.unableToAccessDirectory": "No se puede acceder al directorio",
"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.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.",
"directoryExplorerDialog.toast.failedToSelectDirectory": "No se pudo seleccionar el directorio",
"directoryExplorerDialog.toast.unknownError": "Ocurrió un error desconocido.",
@@ -1106,7 +1110,6 @@ export const dict: Record<I18nKey, string> = {
"directoryTree.section.pinned": "Fijados",
"directoryTree.section.browse": "Explorar",
"aboutDialog.versionLabel": "Versión {version}",
"aboutDialog.tagline": "Programar con agentes en un espacio de trabajo cuidado.",
"aboutDialog.actions.copyDiagnostics": "Copiar diagnósticos",
"aboutDialog.actions.preparingDiagnostics": "Preparando diagnósticos...",
"aboutDialog.actions.diagnosticsCopied": "Diagnósticos copiados",
@@ -1615,6 +1618,7 @@ export const dict: Record<I18nKey, string> = {
"commandPalette.empty.searchingFiles": "Buscando archivos...",
"commandPalette.item.newSession": "Nueva sesión",
"commandPalette.item.newWorktreeDraft": "Nuevo borrador de worktree",
"commandPalette.item.addProject": "Añadir proyecto",
"commandPalette.item.showSessionSwitcher": "Mostrar cambiador de sesiones",
"commandPalette.item.toggleSidebar": "Mostrar u ocultar barra lateral",
"commandPalette.item.toggleRightSidebar": "Mostrar u ocultar barra lateral derecha",
+8 -4
View File
@@ -423,7 +423,7 @@ export const dict: Record<I18nKey, string> = {
'gitView.commit.addGitmoji': 'gitmoji 추가',
'gitView.commit.aiHighlights.insertAria': '커밋 메시지에 삽입',
'gitView.commit.aiHighlights.insertTooltip': '커밋 메시지에 삽입',
'gitView.commit.aiHighlights.title': '제목',
'gitView.commit.aiHighlights.title': '주요 내용',
'gitView.commit.commit': '커밋',
'gitView.commit.commitAria': '커밋 생성',
'gitView.commit.committing': '커밋 중…',
@@ -1105,13 +1105,17 @@ export const dict: Record<I18nKey, string> = {
'directoryExplorerDialog.description': '프로젝트로 추가할 폴더를 선택하세요.',
'directoryExplorerDialog.toggle.showHidden': '숨김 항목 표시',
'directoryExplorerDialog.pathInput.placeholder': '경로를 입력하거나 트리에서 선택…',
'directoryExplorerDialog.actions.cancel': '취소',
'directoryExplorerDialog.actions.openingFinder': '여는 중...',
'directoryExplorerDialog.actions.openInFinder': 'Finder에서 열기',
'directoryExplorerDialog.actions.adding': '추가 중…',
'directoryExplorerDialog.actions.addProject': '프로젝트 추가',
'directoryExplorerDialog.actions.addLocalProject': '로컬 프로젝트 추가',
'directoryExplorerDialog.actions.cloneRepository': '저장소 복제',
'directoryExplorerDialog.actions.cloneAndAdd': '복제하고 추가',
'directoryExplorerDialog.actions.cloning': '복제 중...',
'directoryExplorerDialog.actions.createAndAdd': '생성하고 추가',
'directoryExplorerDialog.actions.alreadyAdded': '이미 추가됨',
'directoryExplorerDialog.clone.remoteUrlPlaceholder': '저장소 URL (HTTPS 또는 SSH)',
'directoryExplorerDialog.browse.directories': '디렉터리',
'directoryExplorerDialog.browse.loading': '디렉터리 로드 중...',
'directoryExplorerDialog.browse.empty': '일치하는 디렉터리가 없습니다.',
@@ -1120,13 +1124,13 @@ export const dict: Record<I18nKey, string> = {
'directoryExplorerDialog.footer.navigate': '탐색',
'directoryExplorerDialog.footer.select': '선택',
'directoryExplorerDialog.footer.add': '추가',
'directoryExplorerDialog.footer.close': '닫기',
'directoryExplorerDialog.shortcut.enter': 'Enter',
'directoryExplorerDialog.toast.unableToAccessDirectory': '디렉터리에 접근할 수 없음',
'directoryExplorerDialog.toast.desktopDeniedAccess': '데스크톱에서 디렉터리 접근이 거부되었습니다.',
'directoryExplorerDialog.toast.failedToOpenDirectory': '디렉터리를 열지 못했습니다',
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '데스크톱에서 파일 접근 권한을 부여하지 못했습니다.',
'directoryExplorerDialog.toast.failedToAddProject': '프로젝트 추가 실패',
'directoryExplorerDialog.toast.cloneUrlRequired': '복제하기 전에 저장소 URL을 입력하세요.',
'directoryExplorerDialog.toast.selectValidDirectoryPath': '유효한 디렉터리 경로를 선택하세요.',
'directoryExplorerDialog.toast.failedToSelectDirectory': '디렉터리 선택 실패',
'directoryExplorerDialog.toast.unknownError': '알 수 없는 오류가 발생했습니다.',
@@ -1142,7 +1146,6 @@ export const dict: Record<I18nKey, string> = {
'directoryTree.section.pinned': '고정됨',
'directoryTree.section.browse': '찾아보기',
'aboutDialog.versionLabel': '버전 {version}',
'aboutDialog.tagline': '워크스페이스에서 에이전트와 함께 코딩하세요.',
'aboutDialog.actions.copyDiagnostics': '진단 정보 복사',
'aboutDialog.actions.preparingDiagnostics': '진단 정보 준비 중…',
'aboutDialog.actions.diagnosticsCopied': '진단 정보 복사됨',
@@ -1649,6 +1652,7 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.empty.searchingFiles': '파일 검색 중…',
'commandPalette.item.newSession': '새 세션',
'commandPalette.item.newWorktreeDraft': '새 워크트리 드래프트',
'commandPalette.item.addProject': '프로젝트 추가',
'commandPalette.item.showSessionSwitcher': '세션 전환기 표시',
'commandPalette.item.toggleSidebar': '토글 사이드바',
'commandPalette.item.toggleRightSidebar': '오른쪽 사이드바 전환',
+60 -56
View File
@@ -640,7 +640,6 @@ export const dict: Record<I18nKey, string> = {
'aboutDialog.actions.preparingDiagnostics': 'Przygotowywanie diagnostyki...',
'aboutDialog.diagnosticsDescription': 'Zawiera stan OpenChamber, kondycję OpenCode, katalogi i projekty.',
'aboutDialog.footerNote': 'Stworzone z sercem dla społeczności',
'aboutDialog.tagline': 'Kodowanie z agentami w starannie zaprojektowanej przestrzeni roboczej.',
'aboutDialog.toast.copyFailed': 'Nie udało się skopiować',
'aboutDialog.toast.diagnosticsCopied': 'Diagnostyka skopiowana',
'aboutDialog.toast.diagnosticsNotReady': 'Diagnostyka nie jest jeszcze gotowa. Poczekaj chwilę i spróbuj ponownie.',
@@ -963,6 +962,7 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.input.placeholder': 'Szukaj plików, sesji i poleceń...',
'commandPalette.item.newSession': 'Nowa sesja',
'commandPalette.item.newWorktreeDraft': 'Nowy szkic drzewa pracy',
'commandPalette.item.addProject': 'Dodaj projekt',
'commandPalette.item.openSettings': 'Otwórz ustawienia...',
'commandPalette.item.showContextUsage': 'Pokaż użycie kontekstu',
'commandPalette.item.showSessionSwitcher': 'Pokaż przełącznik sesji',
@@ -1142,67 +1142,71 @@ export const dict: Record<I18nKey, string> = {
'diffView.change.renamed': 'Plik o zmienionej nazwie',
'diffView.change.untracked': 'Nieśledzony plik',
'diffView.image.modified': 'Zmodyfikowany',
'diffView.image.modifiedAlt': 'Modified: {path}',
'diffView.image.modifiedAlt': 'Zmodyfikowano: {path}',
'diffView.image.new': 'Nowy',
'diffView.image.original': 'Original',
'diffView.image.originalAlt': 'Original: {path}',
'diffView.mode.single.description': 'Show one file at a time',
'diffView.mode.single.label': 'Single file',
'diffView.mode.stacked.description': 'Stack all modified files together',
'diffView.mode.stacked.label': 'All files',
'diffView.image.original': 'Oryginał',
'diffView.image.originalAlt': 'Oryginał: {path}',
'diffView.mode.single.description': 'Pokazuj jeden plik naraz',
'diffView.mode.single.label': 'Pojedynczy plik',
'diffView.mode.stacked.description': 'Pokaż wszystkie zmodyfikowane pliki razem',
'diffView.mode.stacked.label': 'Wszystkie pliki',
'diffView.section.files': 'Pliki',
'diffView.selector.selectFile': 'Select file',
'diffView.selector.viewMode': 'View mode',
'diffView.state.cleanWorkingTree': 'Working tree clean, no changes to display',
'diffView.state.failedToLoadDiff': 'Failed to load diff',
'diffView.state.loadingChanges': 'Loading changes...',
'diffView.state.loadingDiff': 'Loading diff...',
'diffView.state.loadingRepositoryStatus': 'Loading repository status...',
'diffView.state.notGitRepository': 'Not a git repository. Use the Git tab to initialize or change directories.',
'diffView.state.selectSessionDirectory': 'Select a session directory to view diffs',
'diffView.summary.changedFilesPlural': '{count} files changed',
'diffView.summary.changedFilesSingle': '{count} file changed',
'directoryExplorerDialog.actions.addProject': 'Add project',
'directoryExplorerDialog.actions.adding': 'Adding...',
'directoryExplorerDialog.actions.alreadyAdded': 'Already added',
'directoryExplorerDialog.actions.cancel': 'Anuluj',
'directoryExplorerDialog.actions.createAndAdd': 'Create & add',
'directoryExplorerDialog.actions.openInFinder': 'Open in Finder',
'directoryExplorerDialog.actions.openingFinder': 'Opening...',
'directoryExplorerDialog.browse.addedBadge': 'Added',
'directoryExplorerDialog.browse.directories': 'Directories',
'directoryExplorerDialog.browse.empty': 'No matching directories.',
'directoryExplorerDialog.browse.loading': 'Loading directories...',
'directoryExplorerDialog.browse.parentDirectory': 'Parent directory',
'directoryExplorerDialog.description': 'Choose a folder to add as a project.',
'diffView.selector.selectFile': 'Wybierz plik',
'diffView.selector.viewMode': 'Tryb widoku',
'diffView.state.cleanWorkingTree': 'Drzewo robocze jest czyste, brak zmian do wyświetlenia',
'diffView.state.failedToLoadDiff': 'Nie udało się wczytać diffu',
'diffView.state.loadingChanges': 'Ładowanie zmian...',
'diffView.state.loadingDiff': 'Ładowanie diffu...',
'diffView.state.loadingRepositoryStatus': 'Ładowanie stanu repozytorium...',
'diffView.state.notGitRepository': 'To nie jest repozytorium Git. Użyj karty Git, aby zainicjować lub zmienić katalog.',
'diffView.state.selectSessionDirectory': 'Wybierz katalog sesji, aby zobaczyć diffy',
'diffView.summary.changedFilesPlural': 'Zmieniono {count} plików',
'diffView.summary.changedFilesSingle': 'Zmieniono {count} plik',
'directoryExplorerDialog.actions.addProject': 'Dodaj projekt',
'directoryExplorerDialog.actions.addLocalProject': 'Dodaj projekt lokalny',
'directoryExplorerDialog.actions.adding': 'Dodawanie...',
'directoryExplorerDialog.actions.alreadyAdded': 'Już dodano',
'directoryExplorerDialog.actions.cloneAndAdd': 'Sklonuj i dodaj',
'directoryExplorerDialog.actions.cloneRepository': 'Sklonuj repozytorium',
'directoryExplorerDialog.actions.cloning': 'Klonowanie...',
'directoryExplorerDialog.actions.createAndAdd': 'Utwórz i dodaj',
'directoryExplorerDialog.actions.openInFinder': 'Otwórz w Finderze',
'directoryExplorerDialog.actions.openingFinder': 'Otwieranie...',
'directoryExplorerDialog.browse.addedBadge': 'Dodano',
'directoryExplorerDialog.browse.directories': 'Katalogi',
'directoryExplorerDialog.browse.empty': 'Brak pasujących katalogów.',
'directoryExplorerDialog.browse.loading': 'Ładowanie katalogów...',
'directoryExplorerDialog.browse.parentDirectory': 'Katalog nadrzędny',
'directoryExplorerDialog.description': 'Wybierz folder, który chcesz dodać jako projekt.',
'directoryExplorerDialog.clone.remoteUrlPlaceholder': 'URL repozytorium (HTTPS lub SSH)',
'directoryExplorerDialog.footer.add': 'Dodaj',
'directoryExplorerDialog.footer.close': 'Zamknij',
'directoryExplorerDialog.footer.navigate': 'Navigate',
'directoryExplorerDialog.footer.navigate': 'Nawiguj',
'directoryExplorerDialog.footer.select': 'Wybierz',
'directoryExplorerDialog.pathInput.placeholder': 'Enter path or select from tree...',
'directoryExplorerDialog.pathInput.placeholder': 'Wpisz ścieżkę lub wybierz z drzewa...',
'directoryExplorerDialog.shortcut.enter': 'Enter',
'directoryExplorerDialog.title': 'Add project directory',
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': 'Desktop could not grant file access.',
'directoryExplorerDialog.toast.desktopDeniedAccess': 'Desktop denied directory access.',
'directoryExplorerDialog.toast.failedToAddProject': 'Failed to add project',
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Failed to open directory',
'directoryExplorerDialog.toast.failedToSelectDirectory': 'Failed to select directory',
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Please select a valid directory path.',
'directoryExplorerDialog.toast.unableToAccessDirectory': 'Unable to access directory',
'directoryExplorerDialog.toast.unknownError': 'Unknown error occurred.',
'directoryExplorerDialog.toggle.showHidden': 'Show hidden',
'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.failedToAddProject': 'Nie udało się dodać projektu',
'directoryExplorerDialog.toast.cloneUrlRequired': 'Wpisz URL repozytorium przed klonowaniem.',
'directoryExplorerDialog.toast.failedToOpenDirectory': 'Nie udało się otworzyć katalogu',
'directoryExplorerDialog.toast.failedToSelectDirectory': 'Nie udało się wybrać katalogu',
'directoryExplorerDialog.toast.selectValidDirectoryPath': 'Wybierz prawidłową ścieżkę katalogu.',
'directoryExplorerDialog.toast.unableToAccessDirectory': 'Nie można uzyskać dostępu do katalogu',
'directoryExplorerDialog.toast.unknownError': 'Wystąpił nieznany błąd.',
'directoryExplorerDialog.toggle.showHidden': 'Pokaż ukryte',
'directoryTree.actions.cancel': 'Anuluj',
'directoryTree.actions.createDirectory': 'Create directory',
'directoryTree.actions.createNewDirectory': 'Create new directory',
'directoryTree.actions.pinDirectory': 'Pin directory',
'directoryTree.actions.selectWorkingDirectoryAria': 'Select working directory',
'directoryTree.actions.unpinDirectory': 'Unpin directory',
'directoryTree.actions.createDirectory': 'Utwórz katalog',
'directoryTree.actions.createNewDirectory': 'Utwórz nowy katalog',
'directoryTree.actions.pinDirectory': 'Przypnij katalog',
'directoryTree.actions.selectWorkingDirectoryAria': 'Wybierz katalog roboczy',
'directoryTree.actions.unpinDirectory': 'Odepnij katalog',
'directoryTree.field.newDirectoryPlaceholder': 'new_directory',
'directoryTree.section.browse': 'Browse',
'directoryTree.section.pinned': 'Pinned',
'directoryTree.state.loading': 'Loading...',
'directoryTree.state.locatingHomeDirectory': 'Locating home directory...',
'directoryTree.state.noDirectoriesFound': 'No directories found',
'directoryTree.section.browse': 'Przeglądaj',
'directoryTree.section.pinned': 'Przypięte',
'directoryTree.state.loading': 'Ładowanie...',
'directoryTree.state.locatingHomeDirectory': 'Wyszukiwanie katalogu domowego...',
'directoryTree.state.noDirectoriesFound': 'Nie znaleziono katalogów',
'filesView.dialog.cancel': 'Anuluj',
'filesView.dialog.confirm': 'Potwierdź',
'filesView.dialog.createFile.description': 'Utwórz nowy plik w {path}',
@@ -1319,7 +1323,7 @@ export const dict: Record<I18nKey, string> = {
'gitView.commit.addGitmoji': 'Dodaj gitmoji',
'gitView.commit.aiHighlights.insertAria': 'Wstaw',
'gitView.commit.aiHighlights.insertTooltip': 'Wstaw',
'gitView.commit.aiHighlights.title': 'Tytuł',
'gitView.commit.aiHighlights.title': 'Najważniejsze',
'gitView.commit.commit': 'Zatwierdź',
'gitView.commit.commitAria': 'Zatwierdź zmiany',
'gitView.commit.committing': 'Zatwierdzanie...',
+8 -4
View File
@@ -423,7 +423,7 @@ export const dict: Record<I18nKey, string> = {
"gitView.commit.addGitmoji": "Adicionar gitmoji",
"gitView.commit.aiHighlights.insertAria": "Insertar",
"gitView.commit.aiHighlights.insertTooltip": "Insertar",
"gitView.commit.aiHighlights.title": "Título",
"gitView.commit.aiHighlights.title": "Destaques",
"gitView.commit.commit": "Commit",
"gitView.commit.commitAria": "Commit",
"gitView.commit.committing": "Criando commit...",
@@ -1069,13 +1069,17 @@ export const dict: Record<I18nKey, string> = {
"directoryExplorerDialog.description": "Escolha uma pasta para adicionar como projeto.",
"directoryExplorerDialog.toggle.showHidden": "Mostrar ocultos",
"directoryExplorerDialog.pathInput.placeholder": "Digite o caminho ou selecione na árvore...",
"directoryExplorerDialog.actions.cancel": "Cancelar",
"directoryExplorerDialog.actions.openingFinder": "Abrindo...",
"directoryExplorerDialog.actions.openInFinder": "Abrir no Finder",
"directoryExplorerDialog.actions.adding": "Adicionando...",
"directoryExplorerDialog.actions.addProject": "Adicionar projeto",
"directoryExplorerDialog.actions.addLocalProject": "Adicionar projeto local",
"directoryExplorerDialog.actions.cloneRepository": "Clonar repositório",
"directoryExplorerDialog.actions.cloneAndAdd": "Clonar e adicionar",
"directoryExplorerDialog.actions.cloning": "Clonando...",
"directoryExplorerDialog.actions.createAndAdd": "Criar e adicionar",
"directoryExplorerDialog.actions.alreadyAdded": "Já adicionado",
"directoryExplorerDialog.clone.remoteUrlPlaceholder": "URL do repositório (HTTPS ou SSH)",
"directoryExplorerDialog.browse.directories": "Diretórios",
"directoryExplorerDialog.browse.loading": "Carregando diretórios...",
"directoryExplorerDialog.browse.empty": "Nenhum diretório correspondente.",
@@ -1084,13 +1088,13 @@ export const dict: Record<I18nKey, string> = {
"directoryExplorerDialog.footer.navigate": "Navegar",
"directoryExplorerDialog.footer.select": "Selecionar",
"directoryExplorerDialog.footer.add": "Adicionar",
"directoryExplorerDialog.footer.close": "Fechar",
"directoryExplorerDialog.shortcut.enter": "Enter",
"directoryExplorerDialog.toast.unableToAccessDirectory": "Não é possível acessar o diretório",
"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.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.",
"directoryExplorerDialog.toast.failedToSelectDirectory": "Não foi possível selecionar o diretório",
"directoryExplorerDialog.toast.unknownError": "Ocorreu um erro desconhecido.",
@@ -1106,7 +1110,6 @@ export const dict: Record<I18nKey, string> = {
"directoryTree.section.pinned": "Fixados",
"directoryTree.section.browse": "Explorar",
"aboutDialog.versionLabel": "Versão {version}",
"aboutDialog.tagline": "Programação com agentes em um espaço de trabalho feito com cuidado.",
"aboutDialog.actions.copyDiagnostics": "Copiar diagnósticos",
"aboutDialog.actions.preparingDiagnostics": "Preparando diagnósticos...",
"aboutDialog.actions.diagnosticsCopied": "Diagnósticos copiados",
@@ -1615,6 +1618,7 @@ export const dict: Record<I18nKey, string> = {
"commandPalette.empty.searchingFiles": "Buscando arquivos...",
"commandPalette.item.newSession": "Nova sessão",
"commandPalette.item.newWorktreeDraft": "Novo rascunho de worktree",
"commandPalette.item.addProject": "Adicionar projeto",
"commandPalette.item.showSessionSwitcher": "Mostrar seletor de sessões",
"commandPalette.item.toggleSidebar": "Mostrar ou ocultar barra lateral",
"commandPalette.item.toggleRightSidebar": "Mostrar ou ocultar barra lateral direita",
+8 -4
View File
@@ -423,7 +423,7 @@ export const dict: Record<I18nKey, string> = {
"gitView.commit.addGitmoji": "Додати gitmoji",
"gitView.commit.aiHighlights.insertAria": "Вставити підказку",
"gitView.commit.aiHighlights.insertTooltip": "Вставити підказку",
"gitView.commit.aiHighlights.title": "Назва",
"gitView.commit.aiHighlights.title": "Основне",
"gitView.commit.commit": "Коміт",
"gitView.commit.commitAria": "Створити коміт",
"gitView.commit.committing": "Створення коміту...",
@@ -1069,13 +1069,17 @@ export const dict: Record<I18nKey, string> = {
"directoryExplorerDialog.description": "Виберіть папку, щоб додати її як проєкт.",
"directoryExplorerDialog.toggle.showHidden": "Показати приховані",
"directoryExplorerDialog.pathInput.placeholder": "Введіть шлях або виберіть із дерева...",
"directoryExplorerDialog.actions.cancel": "Скасувати",
"directoryExplorerDialog.actions.openingFinder": "Відкриття...",
"directoryExplorerDialog.actions.openInFinder": "Відкрити у Finder",
"directoryExplorerDialog.actions.adding": "Додавання...",
"directoryExplorerDialog.actions.addProject": "Додати проєкт",
"directoryExplorerDialog.actions.addLocalProject": "Додати локальний проєкт",
"directoryExplorerDialog.actions.cloneRepository": "Клонувати репозиторій",
"directoryExplorerDialog.actions.cloneAndAdd": "Клонувати й додати",
"directoryExplorerDialog.actions.cloning": "Клонування...",
"directoryExplorerDialog.actions.createAndAdd": "Створити й додати",
"directoryExplorerDialog.actions.alreadyAdded": "Уже додано",
"directoryExplorerDialog.clone.remoteUrlPlaceholder": "URL репозиторію (HTTPS або SSH)",
"directoryExplorerDialog.browse.directories": "Каталоги",
"directoryExplorerDialog.browse.loading": "Завантаження каталогів...",
"directoryExplorerDialog.browse.empty": "Немає відповідних каталогів.",
@@ -1084,13 +1088,13 @@ export const dict: Record<I18nKey, string> = {
"directoryExplorerDialog.footer.navigate": "Навігація",
"directoryExplorerDialog.footer.select": "Вибрати",
"directoryExplorerDialog.footer.add": "Додати",
"directoryExplorerDialog.footer.close": "Закрити",
"directoryExplorerDialog.shortcut.enter": "Enter",
"directoryExplorerDialog.toast.unableToAccessDirectory": "Неможливо отримати доступ до каталогу",
"directoryExplorerDialog.toast.desktopDeniedAccess": "Десктопному застосунку заборонено доступ до каталогу.",
"directoryExplorerDialog.toast.failedToOpenDirectory": "Не вдалося відкрити каталог",
"directoryExplorerDialog.toast.desktopCouldNotGrantAccess": "Десктопний застосунок не зміг надати доступ до файлу.",
"directoryExplorerDialog.toast.failedToAddProject": "Не вдалося додати проєкт",
"directoryExplorerDialog.toast.cloneUrlRequired": "Введіть URL репозиторію перед клонуванням.",
"directoryExplorerDialog.toast.selectValidDirectoryPath": "Виберіть правильний шлях до каталогу.",
"directoryExplorerDialog.toast.failedToSelectDirectory": "Не вдалося вибрати каталог",
"directoryExplorerDialog.toast.unknownError": "Сталася невідома помилка.",
@@ -1106,7 +1110,6 @@ export const dict: Record<I18nKey, string> = {
"directoryTree.section.pinned": "Закріплено",
"directoryTree.section.browse": "Огляд",
"aboutDialog.versionLabel": "Версія {version}",
"aboutDialog.tagline": "Кодування з агентами в робочому просторі, створеному з турботою.",
"aboutDialog.actions.copyDiagnostics": "Скопіювати діагностику",
"aboutDialog.actions.preparingDiagnostics": "Підготовка діагностики...",
"aboutDialog.actions.diagnosticsCopied": "Діагностику скопійовано",
@@ -1615,6 +1618,7 @@ export const dict: Record<I18nKey, string> = {
"commandPalette.empty.searchingFiles": "Пошук файлів...",
"commandPalette.item.newSession": "Нова сесія",
"commandPalette.item.newWorktreeDraft": "Чернетка нового worktree",
"commandPalette.item.addProject": "Додати проєкт",
"commandPalette.item.showSessionSwitcher": "Показати перемикач сесій",
"commandPalette.item.toggleSidebar": "Перемкнути бічну панель",
"commandPalette.item.toggleRightSidebar": "Перемкнути праву бічну панель",
+8 -4
View File
@@ -423,7 +423,7 @@ export const dict: Record<I18nKey, string> = {
'gitView.commit.addGitmoji': '添加 gitmoji',
'gitView.commit.aiHighlights.insertAria': '将高亮插入提交信息',
'gitView.commit.aiHighlights.insertTooltip': '将高亮追加到提交信息',
'gitView.commit.aiHighlights.title': 'AI 高亮',
'gitView.commit.aiHighlights.title': '重点',
'gitView.commit.commit': '提交',
'gitView.commit.commitAria': '提交',
'gitView.commit.committing': '提交中...',
@@ -1069,13 +1069,17 @@ export const dict: Record<I18nKey, string> = {
'directoryExplorerDialog.description': '选择一个文件夹添加为项目。',
'directoryExplorerDialog.toggle.showHidden': '显示隐藏项',
'directoryExplorerDialog.pathInput.placeholder': '输入路径或从树中选择...',
'directoryExplorerDialog.actions.cancel': '取消',
'directoryExplorerDialog.actions.openingFinder': '正在打开...',
'directoryExplorerDialog.actions.openInFinder': '在 Finder 中打开',
'directoryExplorerDialog.actions.adding': '添加中...',
'directoryExplorerDialog.actions.addProject': '添加项目',
'directoryExplorerDialog.actions.addLocalProject': '添加本地项目',
'directoryExplorerDialog.actions.cloneRepository': '克隆仓库',
'directoryExplorerDialog.actions.cloneAndAdd': '克隆并添加',
'directoryExplorerDialog.actions.cloning': '正在克隆...',
'directoryExplorerDialog.actions.createAndAdd': '创建并添加',
'directoryExplorerDialog.actions.alreadyAdded': '已添加',
'directoryExplorerDialog.clone.remoteUrlPlaceholder': '仓库 URLHTTPS 或 SSH',
'directoryExplorerDialog.browse.directories': '目录',
'directoryExplorerDialog.browse.loading': '正在加载目录...',
'directoryExplorerDialog.browse.empty': '没有匹配的目录。',
@@ -1084,13 +1088,13 @@ export const dict: Record<I18nKey, string> = {
'directoryExplorerDialog.footer.navigate': '导航',
'directoryExplorerDialog.footer.select': '选择',
'directoryExplorerDialog.footer.add': '添加',
'directoryExplorerDialog.footer.close': '关闭',
'directoryExplorerDialog.shortcut.enter': 'Enter',
'directoryExplorerDialog.toast.unableToAccessDirectory': '无法访问目录',
'directoryExplorerDialog.toast.desktopDeniedAccess': '桌面端拒绝了目录访问。',
'directoryExplorerDialog.toast.failedToOpenDirectory': '打开目录失败',
'directoryExplorerDialog.toast.desktopCouldNotGrantAccess': '桌面端无法授予文件访问权限。',
'directoryExplorerDialog.toast.failedToAddProject': '添加项目失败',
'directoryExplorerDialog.toast.cloneUrlRequired': '克隆前请输入仓库 URL。',
'directoryExplorerDialog.toast.selectValidDirectoryPath': '请选择有效的目录路径。',
'directoryExplorerDialog.toast.failedToSelectDirectory': '选择目录失败',
'directoryExplorerDialog.toast.unknownError': '发生未知错误。',
@@ -1106,7 +1110,6 @@ export const dict: Record<I18nKey, string> = {
'directoryTree.section.pinned': '已固定',
'directoryTree.section.browse': '浏览',
'aboutDialog.versionLabel': '版本 {version}',
'aboutDialog.tagline': '与智能体协作编码,在精心打造的工作区中完成。',
'aboutDialog.actions.copyDiagnostics': '复制诊断信息',
'aboutDialog.actions.preparingDiagnostics': '正在准备诊断信息...',
'aboutDialog.actions.diagnosticsCopied': '诊断信息已复制',
@@ -1615,6 +1618,7 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.empty.searchingFiles': '正在搜索文件...',
'commandPalette.item.newSession': '新建会话',
'commandPalette.item.newWorktreeDraft': '新建工作树草稿',
'commandPalette.item.addProject': '添加项目',
'commandPalette.item.showSessionSwitcher': '显示会话切换器',
'commandPalette.item.toggleSidebar': '切换侧边栏',
'commandPalette.item.toggleRightSidebar': '切换右侧边栏',
+18
View File
@@ -1415,6 +1415,24 @@ class OpencodeService {
return result;
}
async cloneRepository(input: { remoteUrl: string; destinationPath: string; gitIdentityId?: string | null }): Promise<{ success: boolean; path: string; output?: string }> {
const response = await fetch(`${this.baseUrl}/fs/clone`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Failed to clone repository' }));
throw new Error(error.error || 'Failed to clone repository');
}
return await response.json();
}
async listLocalDirectory(directoryPath: string | null | undefined, options?: { respectGitignore?: boolean }): Promise<FilesystemEntry[]> {
const normalizedDirectoryPath = typeof directoryPath === 'string' ? normalizeFsPath(directoryPath.trim()) : '';
const cacheKey = `${normalizedDirectoryPath}|${options?.respectGitignore ? '1' : '0'}`;
+151
View File
@@ -95,6 +95,48 @@ const resolveWorkspacePathFromContext = async ({ req, targetPath, resolveProject
});
};
const deriveCloneDirectoryName = (remoteUrl) => {
const remote = typeof remoteUrl === 'string' ? remoteUrl.trim() : '';
if (!remote) return '';
const withoutQuery = remote.split(/[?#]/, 1)[0] || remote;
const match = withoutQuery.match(/([^/:]+?)(?:\.git)?\/?$/);
return match?.[1]?.trim() || '';
};
const resolveCloneGitIdentity = async (gitIdentityId) => {
const id = typeof gitIdentityId === 'string' ? gitIdentityId.trim() : '';
if (!id) return null;
const { getProfile, getGlobalIdentity } = await import('../git/index.js');
if (id === 'global') {
const globalIdentity = await getGlobalIdentity();
if (!globalIdentity?.userName || !globalIdentity?.userEmail) return null;
return {
id: 'global',
name: 'Global Identity',
userName: globalIdentity.userName,
userEmail: globalIdentity.userEmail,
sshKey: globalIdentity.sshCommand ? globalIdentity.sshCommand.replace('ssh -i ', '') : null,
};
}
return getProfile(id) || null;
};
const escapeCloneSshKeyPath = (sshKeyPath) => {
const raw = String(sshKeyPath || '').trim();
if (!raw) return '';
const normalized = process.platform === 'win32' ? raw.replace(/\\/g, '/') : raw;
const dangerousChars = /[`$!"';&|<>(){}[\]*?#~]/;
if (dangerousChars.test(normalized)) {
throw new Error(`SSH key path contains invalid characters: ${raw}`);
}
if (process.platform === 'win32') {
const driveMatch = normalized.match(/^([A-Za-z]):\//);
const unixPath = driveMatch ? `/${driveMatch[1].toLowerCase()}${normalized.slice(2)}` : normalized;
return `'${unixPath}'`;
}
return `'${normalized.replace(/'/g, "'\\''")}'`;
};
const resolveReadPathFromContext = async ({ req, targetPath, resolveProjectDirectory, path, os, normalizeDirectoryPath, openchamberUserConfigRoot }) => {
if (req.query?.allowOutsideWorkspace === 'true') {
const normalized = normalizeDirectoryPath(targetPath);
@@ -304,6 +346,115 @@ export const registerFsRoutes = (app, dependencies) => {
}
});
app.post('/api/fs/clone', async (req, res) => {
try {
const { remoteUrl, destinationPath, gitIdentityId } = req.body ?? {};
const remote = typeof remoteUrl === 'string' ? remoteUrl.trim() : '';
const destination = typeof destinationPath === 'string' ? destinationPath.trim() : '';
if (!remote) {
return res.status(400).json({ error: 'Repository URL is required' });
}
if (!destination) {
return res.status(400).json({ error: 'Destination path is required' });
}
let resolvedDestination = path.resolve(normalizeDirectoryPath(destination));
let parentPath = path.dirname(resolvedDestination);
let directoryName = path.basename(resolvedDestination);
const cloneIntoDestinationDirectory = destination.endsWith('/') || destination.endsWith('\\');
if (cloneIntoDestinationDirectory) {
const inferredName = deriveCloneDirectoryName(remote);
if (!inferredName) {
return res.status(400).json({ error: 'Could not infer repository directory name from URL' });
}
parentPath = resolvedDestination;
directoryName = inferredName;
resolvedDestination = path.join(parentPath, directoryName);
} else {
try {
const stat = await fsPromises.stat(resolvedDestination);
if (stat.isDirectory()) {
const inferredName = deriveCloneDirectoryName(remote);
if (!inferredName) {
return res.status(400).json({ error: 'Could not infer repository directory name from URL' });
}
parentPath = resolvedDestination;
directoryName = inferredName;
resolvedDestination = path.join(parentPath, directoryName);
}
} catch (error) {
if (!error || error.code !== 'ENOENT') {
throw error;
}
}
}
if (!directoryName || directoryName === '.' || directoryName === '..') {
return res.status(400).json({ error: 'Destination path must include a directory name' });
}
const identity = await resolveCloneGitIdentity(gitIdentityId);
const gitArgs = ['clone', '--', remote, directoryName];
const sshKeyPath = typeof identity?.sshKey === 'string' ? identity.sshKey.trim() : '';
if (sshKeyPath) {
gitArgs.unshift(`core.sshCommand=ssh -i ${escapeCloneSshKeyPath(sshKeyPath)} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new`);
gitArgs.unshift('-c');
}
await fsPromises.mkdir(parentPath, { recursive: true });
try {
await fsPromises.access(resolvedDestination);
return res.status(409).json({ error: 'Destination path already exists' });
} catch (error) {
if (!error || error.code !== 'ENOENT') {
throw error;
}
}
const output = await new Promise((resolve, reject) => {
const child = spawn(resolveGitBinaryForSpawn(), gitArgs, {
cwd: parentPath,
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
PATH: buildAugmentedPath ? buildAugmentedPath(process.env.PATH || '') : process.env.PATH,
GIT_TERMINAL_PROMPT: '0',
},
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => { stdout += data.toString(); });
child.stderr.on('data', (data) => { stderr += data.toString(); });
child.on('error', reject);
child.on('close', (code) => {
const combined = `${stdout}\n${stderr}`.trim();
if (code === 0) {
resolve(combined);
return;
}
const message = combined || `git clone failed with exit code ${code}`;
reject(new Error(message));
});
});
if (identity?.userName && identity?.userEmail) {
try {
const { setLocalIdentity } = await import('../git/index.js');
await setLocalIdentity(resolvedDestination, identity);
} catch (error) {
console.warn('Failed to apply git identity after clone:', error);
}
}
return res.json({ success: true, path: resolvedDestination, output });
} catch (error) {
console.error('Failed to clone repository:', error);
return res.status(500).json({ error: error.message || 'Failed to clone repository' });
}
});
app.get('/api/fs/stat', async (req, res) => {
const filePath = typeof req.query.path === 'string' ? req.query.path.trim() : '';
if (!filePath) {