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:
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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': '오른쪽 사이드바 전환',
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Перемкнути праву бічну панель",
|
||||
|
||||
@@ -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': '仓库 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': '新建工作树草稿',
|
||||
'commandPalette.item.addProject': '添加项目',
|
||||
'commandPalette.item.showSessionSwitcher': '显示会话切换器',
|
||||
'commandPalette.item.toggleSidebar': '切换侧边栏',
|
||||
'commandPalette.item.toggleRightSidebar': '切换右侧边栏',
|
||||
|
||||
@@ -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'}`;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user