fix(worktrees): remove worktrees in the background (#3319)
* refactor(worktrees): fetch source once during creation * fix(worktrees): remove worktrees in background * fix(worktrees): show background removal progress * fix(worktrees): name the worktree in removal toasts
This commit is contained in:
@@ -7,7 +7,7 @@ import { toast } from '@/components/ui';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getWorktreeStatus } from '@/lib/worktrees/worktreeStatus';
|
||||
import { removeProjectWorktree, type ProjectRef } from '@/lib/worktrees/worktreeManager';
|
||||
import { getWorktreeDisplayName, removeProjectWorktree, type ProjectRef } from '@/lib/worktrees/worktreeManager';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
@@ -91,9 +91,24 @@ export const MobileDeleteWorktreeDialog: React.FC<MobileDeleteWorktreeDialogProp
|
||||
};
|
||||
}, [open, worktree?.path, worktree?.status?.isDirty]);
|
||||
|
||||
const removeWorktreeInBackground = React.useCallback((target: WorktreeMetadata) => {
|
||||
const removeWorktreeInBackground = React.useCallback((target: WorktreeMetadata, sessionIds: string[]) => {
|
||||
const name = getWorktreeDisplayName(target);
|
||||
const toastId = toast.loading(t('sessions.sidebar.sessionDialogs.worktree.removingTitle', { name }));
|
||||
void (async () => {
|
||||
try {
|
||||
if (sessionIds.length > 0) {
|
||||
const { failedIds } = await archiveSessions(sessionIds);
|
||||
if (failedIds.length > 0) {
|
||||
toast.error(
|
||||
failedIds.length === 1
|
||||
? t('sessions.sidebar.bulkActions.failedArchiveSingle', { count: failedIds.length })
|
||||
: t('sessions.sidebar.bulkActions.failedArchivePlural', { count: failedIds.length }),
|
||||
{ id: toastId, description: t('sessions.sidebar.dialogs.deleteResult.tryAgain') },
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await removeProjectWorktree(project, target, {
|
||||
deleteRemoteBranch: hasBranch && deleteRemoteBranch,
|
||||
deleteLocalBranch: hasBranch && deleteLocalBranch,
|
||||
@@ -104,7 +119,8 @@ export const MobileDeleteWorktreeDialog: React.FC<MobileDeleteWorktreeDialogProp
|
||||
useDirectoryStore.getState().setDirectory(normalizePath(project.path), { showOverlay: false });
|
||||
}
|
||||
|
||||
toast.success(t('sessions.sidebar.sessionDialogs.worktree.removedTitle'), {
|
||||
toast.success(t('sessions.sidebar.sessionDialogs.worktree.removedTitle', { name }), {
|
||||
id: toastId,
|
||||
description:
|
||||
hasBranch && deleteRemoteBranch
|
||||
? t('sessions.sidebar.sessionDialogs.worktree.removedWithRemote')
|
||||
@@ -112,49 +128,19 @@ export const MobileDeleteWorktreeDialog: React.FC<MobileDeleteWorktreeDialogProp
|
||||
});
|
||||
onDeleted?.();
|
||||
} catch (error) {
|
||||
toast.error(t('sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle'), {
|
||||
toast.error(t('sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle', { name }), {
|
||||
id: toastId,
|
||||
description: error instanceof Error ? error.message : t('sessions.sidebar.dialogs.deleteResult.tryAgain'),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [currentDirectory, deleteLocalBranch, deleteRemoteBranch, hasBranch, onDeleted, project, t, worktreePath]);
|
||||
}, [archiveSessions, currentDirectory, deleteLocalBranch, deleteRemoteBranch, hasBranch, onDeleted, project, t, worktreePath]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const handleConfirm = () => {
|
||||
if (!worktree || isProcessing) return;
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
if (linkedSessions.length > 0) {
|
||||
const { archivedIds, failedIds } = await archiveSessions(linkedSessions.map((session) => session.id));
|
||||
if (failedIds.length > 0) {
|
||||
if (archivedIds.length > 0) {
|
||||
toast.success(
|
||||
archivedIds.length === 1
|
||||
? t('sessions.sidebar.bulkActions.archivedSingle', { count: archivedIds.length })
|
||||
: t('sessions.sidebar.bulkActions.archivedPlural', { count: archivedIds.length }),
|
||||
);
|
||||
}
|
||||
toast.error(
|
||||
failedIds.length === 1
|
||||
? t('sessions.sidebar.bulkActions.failedArchiveSingle', { count: failedIds.length })
|
||||
: t('sessions.sidebar.bulkActions.failedArchivePlural', { count: failedIds.length }),
|
||||
{ description: t('sessions.sidebar.dialogs.deleteResult.tryAgain') },
|
||||
);
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
removeWorktreeInBackground(worktree);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast.error(t('sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle'), {
|
||||
description: error instanceof Error ? error.message : t('sessions.sidebar.dialogs.deleteResult.tryAgain'),
|
||||
});
|
||||
setIsProcessing(false);
|
||||
} finally {
|
||||
if (!open) {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
removeWorktreeInBackground(worktree, linkedSessions.map((session) => session.id));
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!worktree) return null;
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from '@/lib/responseStyle';
|
||||
import type { DesktopSettings } from '@/lib/desktop';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { noteDeferredRestartFromPayload, recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
|
||||
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
|
||||
import {
|
||||
@@ -47,7 +46,6 @@ type ResponseStyleValue = ResponseStylePreset | 'custom';
|
||||
type BehaviorSettingsState = {
|
||||
prompt: string;
|
||||
optimizeSystemPrompt: boolean;
|
||||
worktreeFetchSource: boolean;
|
||||
responseStyleEnabled: boolean;
|
||||
responseStylePreset: ResponseStyleValue;
|
||||
responseStyleCustomInstructions: string;
|
||||
@@ -56,7 +54,6 @@ type BehaviorSettingsState = {
|
||||
const DEFAULT_BEHAVIOR_SETTINGS: BehaviorSettingsState = {
|
||||
prompt: '',
|
||||
optimizeSystemPrompt: false,
|
||||
worktreeFetchSource: true,
|
||||
responseStyleEnabled: false,
|
||||
responseStylePreset: 'concise',
|
||||
responseStyleCustomInstructions: '',
|
||||
@@ -106,10 +103,8 @@ const saveBehaviorSetting = async (settings: Partial<DesktopSettings>, fallbackE
|
||||
export const BehaviorPage: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const isVSCode = useIsVSCodeRuntime();
|
||||
const setSettingsWorktreeFetchSource = useConfigStore((state) => state.setSettingsWorktreeFetchSource);
|
||||
const [prompt, setPrompt] = React.useState('');
|
||||
const [optimizeSystemPrompt, setOptimizeSystemPrompt] = React.useState(false);
|
||||
const [worktreeFetchSource, setWorktreeFetchSource] = React.useState(DEFAULT_BEHAVIOR_SETTINGS.worktreeFetchSource);
|
||||
const [responseStyleEnabled, setResponseStyleEnabled] = React.useState(DEFAULT_BEHAVIOR_SETTINGS.responseStyleEnabled);
|
||||
const [responseStylePreset, setResponseStylePreset] = React.useState<ResponseStyleValue>(DEFAULT_BEHAVIOR_SETTINGS.responseStylePreset);
|
||||
const [responseStyleCustomInstructions, setResponseStyleCustomInstructions] = React.useState(DEFAULT_BEHAVIOR_SETTINGS.responseStyleCustomInstructions);
|
||||
@@ -148,16 +143,12 @@ export const BehaviorPage: React.FC = () => {
|
||||
nextSettings = {
|
||||
...nextSettings,
|
||||
optimizeSystemPrompt: data.optimizeSystemPrompt === true,
|
||||
worktreeFetchSource: data.worktreeFetchSource !== false,
|
||||
responseStyleEnabled: data.responseStyleEnabled === true,
|
||||
responseStylePreset: sanitizeResponseStylePreset(data.responseStylePreset),
|
||||
responseStyleCustomInstructions: typeof data.responseStyleCustomInstructions === 'string'
|
||||
? data.responseStyleCustomInstructions
|
||||
: '',
|
||||
};
|
||||
if (typeof data.worktreeFetchSource === 'boolean') {
|
||||
setSettingsWorktreeFetchSource(data.worktreeFetchSource);
|
||||
}
|
||||
if (typeof data.globalBehaviorPrompt === 'string') {
|
||||
nextSettings = { ...nextSettings, prompt: data.globalBehaviorPrompt };
|
||||
}
|
||||
@@ -173,7 +164,6 @@ export const BehaviorPage: React.FC = () => {
|
||||
setPrompt(nextSettings.prompt);
|
||||
setOptimizeSystemPrompt(nextSettings.optimizeSystemPrompt);
|
||||
setInitialOptimizeSystemPrompt(nextSettings.optimizeSystemPrompt);
|
||||
setWorktreeFetchSource(nextSettings.worktreeFetchSource);
|
||||
setResponseStyleEnabled(nextSettings.responseStyleEnabled);
|
||||
setResponseStylePreset(nextSettings.responseStylePreset);
|
||||
setResponseStyleCustomInstructions(nextSettings.responseStyleCustomInstructions);
|
||||
@@ -194,7 +184,7 @@ export const BehaviorPage: React.FC = () => {
|
||||
|
||||
void load();
|
||||
return () => abort.abort();
|
||||
}, [setSettingsWorktreeFetchSource]);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isLoading) return;
|
||||
@@ -275,21 +265,6 @@ export const BehaviorPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorktreeFetchSourceChange = (enabled: boolean) => {
|
||||
const previous = worktreeFetchSource;
|
||||
setWorktreeFetchSource(enabled);
|
||||
setSettingsWorktreeFetchSource(enabled);
|
||||
void saveBehaviorSetting(
|
||||
{ worktreeFetchSource: enabled },
|
||||
t('settings.behavior.page.toast.saveFailed'),
|
||||
).catch((error) => {
|
||||
setWorktreeFetchSource(previous);
|
||||
setSettingsWorktreeFetchSource(previous);
|
||||
const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed');
|
||||
toast.error(message);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSavePromptOptimization = async () => {
|
||||
setIsApplyingPromptOptimization(true);
|
||||
try {
|
||||
@@ -343,21 +318,6 @@ export const BehaviorPage: React.FC = () => {
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
<SettingsSection
|
||||
title={t('settings.behavior.page.section.worktrees')}
|
||||
settingsItem="behavior.worktree-fetch-source"
|
||||
contentClassName="space-y-3"
|
||||
>
|
||||
<SettingsCheckboxRow
|
||||
checked={worktreeFetchSource}
|
||||
onChange={handleWorktreeFetchSourceChange}
|
||||
disabled={isLoading}
|
||||
label={t('settings.behavior.page.worktreeFetchSource.enable')}
|
||||
ariaLabel={t('settings.behavior.page.worktreeFetchSource.enableAria')}
|
||||
info={t('settings.behavior.page.worktreeFetchSource.info')}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t('settings.behavior.page.section.systemPrompt')}
|
||||
info={(
|
||||
|
||||
@@ -17,7 +17,7 @@ import { cn, formatPathForDisplay } from '@/lib/utils';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import type { WorktreeMetadata } from '@/types/worktree';
|
||||
import { getWorktreeStatus } from '@/lib/worktrees/worktreeStatus';
|
||||
import { removeProjectWorktree } from '@/lib/worktrees/worktreeManager';
|
||||
import { getWorktreeDisplayName, removeProjectWorktree } from '@/lib/worktrees/worktreeManager';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import * as sessionActions from '@/sync/session-actions';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
@@ -65,7 +65,6 @@ export const SessionDialogs: React.FC = () => {
|
||||
const setNewSessionDraftTarget = useSessionUIStore((s) => s.setNewSessionDraftTarget);
|
||||
const setDraftBootstrapPendingDirectory = useSessionUIStore((s) => s.setDraftBootstrapPendingDirectory);
|
||||
const deleteSession = sessionActions.deleteSession;
|
||||
const archiveSession = sessionActions.archiveSession;
|
||||
const deleteSessions = useSessionUIStore((s) => s.deleteSessions);
|
||||
const archiveSessions = useSessionUIStore((s) => s.archiveSessions);
|
||||
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
|
||||
@@ -333,7 +332,8 @@ export const SessionDialogs: React.FC = () => {
|
||||
|
||||
const removeSelectedWorktree = React.useCallback(async (
|
||||
worktree: WorktreeMetadata,
|
||||
deleteLocalBranch: boolean
|
||||
deleteLocalBranch: boolean,
|
||||
toastId?: string | number,
|
||||
): Promise<boolean> => {
|
||||
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
||||
const projectRef = getProjectRefForWorktree(worktree);
|
||||
@@ -366,7 +366,8 @@ export const SessionDialogs: React.FC = () => {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
toast.error(t('sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle'), {
|
||||
toast.error(t('sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle', { name: getWorktreeDisplayName(worktree) }), {
|
||||
id: toastId,
|
||||
description: renderToastDescription(error instanceof Error ? error.message : t('sessions.sidebar.dialogs.deleteResult.tryAgain')),
|
||||
});
|
||||
return false;
|
||||
@@ -375,22 +376,45 @@ export const SessionDialogs: React.FC = () => {
|
||||
|
||||
const removeSelectedWorktreeInBackground = React.useCallback((
|
||||
worktree: WorktreeMetadata,
|
||||
sessionIds: string[],
|
||||
deleteLocalBranch: boolean
|
||||
): void => {
|
||||
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
||||
const toastId = toast.loading(t('sessions.sidebar.sessionDialogs.worktree.removingTitle', { name: getWorktreeDisplayName(worktree) }));
|
||||
void (async () => {
|
||||
const removed = await removeSelectedWorktree(worktree, deleteLocalBranch);
|
||||
if (!removed) {
|
||||
return;
|
||||
try {
|
||||
if (sessionIds.length > 0) {
|
||||
const { failedIds } = await archiveSessions(sessionIds);
|
||||
if (failedIds.length > 0) {
|
||||
toast.error(failedIds.length === 1
|
||||
? t('sessions.sidebar.bulkActions.failedArchiveSingle', { count: failedIds.length })
|
||||
: t('sessions.sidebar.bulkActions.failedArchivePlural', { count: failedIds.length }), {
|
||||
id: toastId,
|
||||
description: renderToastDescription(t('sessions.sidebar.dialogs.deleteResult.tryAgain')),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const removed = await removeSelectedWorktree(worktree, deleteLocalBranch, toastId);
|
||||
if (!removed) {
|
||||
return;
|
||||
}
|
||||
const archiveNote = shouldRemoveRemote
|
||||
? t('sessions.sidebar.sessionDialogs.worktree.removedWithRemote')
|
||||
: t('sessions.sidebar.sessionDialogs.worktree.removed');
|
||||
toast.success(t('sessions.sidebar.sessionDialogs.worktree.removedTitle', { name: getWorktreeDisplayName(worktree) }), {
|
||||
id: toastId,
|
||||
description: renderToastDescription(archiveNote),
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(t('sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle'), {
|
||||
id: toastId,
|
||||
description: renderToastDescription(error instanceof Error ? error.message : t('sessions.sidebar.dialogs.deleteResult.tryAgain')),
|
||||
});
|
||||
}
|
||||
const archiveNote = shouldRemoveRemote
|
||||
? t('sessions.sidebar.sessionDialogs.worktree.removedWithRemote')
|
||||
: t('sessions.sidebar.sessionDialogs.worktree.removed');
|
||||
toast.success(t('sessions.sidebar.sessionDialogs.worktree.removedTitle'), {
|
||||
description: renderToastDescription(archiveNote),
|
||||
});
|
||||
})();
|
||||
}, [canRemoveRemoteBranches, deleteDialogShouldRemoveRemote, removeSelectedWorktree, t]);
|
||||
}, [archiveSessions, canRemoveRemoteBranches, deleteDialogShouldRemoveRemote, removeSelectedWorktree, t]);
|
||||
|
||||
const handleConfirmDelete = React.useCallback(async () => {
|
||||
if (!deleteDialog) {
|
||||
@@ -403,21 +427,21 @@ export const SessionDialogs: React.FC = () => {
|
||||
const removeRemoteBranch = shouldArchive && deleteDialogShouldRemoveRemote;
|
||||
const deleteLocalBranch = shouldArchive && deleteDialogShouldDeleteLocalBranch;
|
||||
|
||||
if (deleteDialog.sessions.length === 0 && isWorktreeDelete && deleteDialog.worktree) {
|
||||
removeSelectedWorktreeInBackground(deleteDialog.worktree, deleteLocalBranch);
|
||||
if (isWorktreeDelete && deleteDialog.worktree) {
|
||||
removeSelectedWorktreeInBackground(
|
||||
deleteDialog.worktree,
|
||||
deleteDialog.sessions.map((session) => session.id),
|
||||
deleteLocalBranch,
|
||||
);
|
||||
closeDeleteDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteDialog.sessions.length === 1) {
|
||||
const target = deleteDialog.sessions[0];
|
||||
const success = isWorktreeDelete
|
||||
? await archiveSession(target.id)
|
||||
: await deleteSession(target.id);
|
||||
const success = await deleteSession(target.id);
|
||||
if (!success) {
|
||||
toast.error(isWorktreeDelete
|
||||
? t('sessions.sidebar.session.archive.error')
|
||||
: t('sessions.sidebar.session.delete.error'));
|
||||
toast.error(t('sessions.sidebar.session.delete.error'));
|
||||
setIsProcessingDelete(false);
|
||||
return;
|
||||
}
|
||||
@@ -426,9 +450,7 @@ export const SessionDialogs: React.FC = () => {
|
||||
? t('sessions.sidebar.sessionDialogs.worktree.removedWithRemote')
|
||||
: t('sessions.sidebar.sessionDialogs.worktree.attachedArchived')
|
||||
: undefined;
|
||||
toast.success(isWorktreeDelete
|
||||
? t('sessions.sidebar.session.archive.success')
|
||||
: t('sessions.sidebar.session.delete.success'), {
|
||||
toast.success(t('sessions.sidebar.session.delete.success'), {
|
||||
description: renderToastDescription(archiveNote),
|
||||
action: {
|
||||
label: t('sessions.sidebar.sessionDialogs.ok'),
|
||||
@@ -439,22 +461,9 @@ export const SessionDialogs: React.FC = () => {
|
||||
const ids = deleteDialog.sessions.map((session) => session.id);
|
||||
let deletedIds: string[] = [];
|
||||
let failedIds: string[] = [];
|
||||
if (isWorktreeDelete) {
|
||||
const result = await archiveSessions(ids);
|
||||
deletedIds = result.archivedIds;
|
||||
failedIds = result.failedIds;
|
||||
} else {
|
||||
const result = await deleteSessions(ids);
|
||||
deletedIds = result.deletedIds;
|
||||
failedIds = result.failedIds;
|
||||
}
|
||||
|
||||
if (isWorktreeDelete && deleteDialog.worktree && failedIds.length === 0) {
|
||||
// Remove selected worktree even if per-session metadata is missing.
|
||||
// Use same projectRef logic as the no-sessions path.
|
||||
removeSelectedWorktreeInBackground(deleteDialog.worktree, deleteLocalBranch);
|
||||
// sync handles session refresh automatically
|
||||
}
|
||||
const result = await deleteSessions(ids);
|
||||
deletedIds = result.deletedIds;
|
||||
failedIds = result.failedIds;
|
||||
|
||||
if (deletedIds.length > 0) {
|
||||
const archiveNote = !isWorktreeDelete && shouldArchive
|
||||
@@ -464,24 +473,16 @@ export const SessionDialogs: React.FC = () => {
|
||||
: undefined;
|
||||
const successDescription =
|
||||
failedIds.length > 0
|
||||
? (isWorktreeDelete
|
||||
? (failedIds.length === 1
|
||||
? t('sessions.sidebar.dialogs.deleteResult.singleFailedToArchive', { count: failedIds.length })
|
||||
: t('sessions.sidebar.dialogs.deleteResult.manyFailedToArchive', { count: failedIds.length }))
|
||||
: (failedIds.length === 1
|
||||
? (failedIds.length === 1
|
||||
? t('sessions.sidebar.dialogs.deleteResult.singleFailedToDelete', { count: failedIds.length })
|
||||
: t('sessions.sidebar.dialogs.deleteResult.manyFailedToDelete', { count: failedIds.length })))
|
||||
: t('sessions.sidebar.dialogs.deleteResult.manyFailedToDelete', { count: failedIds.length }))
|
||||
: deleteDialog.dateLabel
|
||||
? t('sessions.sidebar.dialogs.deleteResult.removedFromDate', { dateLabel: deleteDialog.dateLabel })
|
||||
: undefined;
|
||||
const combinedDescription = [successDescription, archiveNote].filter(Boolean).join(' ');
|
||||
toast.success(isWorktreeDelete
|
||||
? (deletedIds.length === 1
|
||||
? t('sessions.sidebar.bulkActions.archivedSingle', { count: deletedIds.length })
|
||||
: t('sessions.sidebar.bulkActions.archivedPlural', { count: deletedIds.length }))
|
||||
: (deletedIds.length === 1
|
||||
toast.success(deletedIds.length === 1
|
||||
? t('sessions.sidebar.bulkActions.deletedSingle', { count: deletedIds.length })
|
||||
: t('sessions.sidebar.bulkActions.deletedPlural', { count: deletedIds.length })), {
|
||||
: t('sessions.sidebar.bulkActions.deletedPlural', { count: deletedIds.length }), {
|
||||
description: renderToastDescription(combinedDescription || undefined),
|
||||
action: {
|
||||
label: t('sessions.sidebar.sessionDialogs.ok'),
|
||||
@@ -491,13 +492,9 @@ export const SessionDialogs: React.FC = () => {
|
||||
}
|
||||
|
||||
if (failedIds.length > 0) {
|
||||
toast.error(isWorktreeDelete
|
||||
? (failedIds.length === 1
|
||||
? t('sessions.sidebar.bulkActions.failedArchiveSingle', { count: failedIds.length })
|
||||
: t('sessions.sidebar.bulkActions.failedArchivePlural', { count: failedIds.length }))
|
||||
: (failedIds.length === 1
|
||||
toast.error(failedIds.length === 1
|
||||
? t('sessions.sidebar.bulkActions.failedDeleteSingle', { count: failedIds.length })
|
||||
: t('sessions.sidebar.bulkActions.failedDeletePlural', { count: failedIds.length })), {
|
||||
: t('sessions.sidebar.bulkActions.failedDeletePlural', { count: failedIds.length }), {
|
||||
description: renderToastDescription(t('sessions.sidebar.dialogs.deleteResult.tryAgain')),
|
||||
});
|
||||
if (deletedIds.length === 0) {
|
||||
@@ -507,11 +504,6 @@ export const SessionDialogs: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isWorktreeDelete && deleteDialog.sessions.length === 1 && deleteDialog.worktree) {
|
||||
removeSelectedWorktreeInBackground(deleteDialog.worktree, deleteLocalBranch);
|
||||
// sync bootstrap refreshes sessions automatically
|
||||
}
|
||||
|
||||
closeDeleteDialog();
|
||||
} finally {
|
||||
setIsProcessingDelete(false);
|
||||
@@ -522,8 +514,6 @@ export const SessionDialogs: React.FC = () => {
|
||||
deleteDialogShouldDeleteLocalBranch,
|
||||
deleteSession,
|
||||
deleteSessions,
|
||||
archiveSession,
|
||||
archiveSessions,
|
||||
closeDeleteDialog,
|
||||
shouldArchiveWorktree,
|
||||
isWorktreeDelete,
|
||||
|
||||
@@ -438,6 +438,7 @@ export interface GitWorktreeCreateResult {
|
||||
path: string;
|
||||
directoryCreated?: true;
|
||||
bootstrapStatus?: GitWorktreeBootstrapStatus;
|
||||
sourceFetchFailed?: true;
|
||||
}
|
||||
|
||||
export interface RemoveGitWorktreePayload {
|
||||
|
||||
@@ -146,7 +146,6 @@ export type DesktopSettings = {
|
||||
defaultGitIdentityId?: string; // ''/undefined = unset, 'global' or profile id
|
||||
openInAppId?: string;
|
||||
autoCreateWorktree?: boolean;
|
||||
worktreeFetchSource?: boolean;
|
||||
followUpBehavior?: 'steer' | 'queue';
|
||||
queueModeEnabled?: boolean;
|
||||
gitmojiEnabled?: boolean;
|
||||
|
||||
@@ -2188,10 +2188,6 @@ export const settingsDict = {
|
||||
'settings.behavior.page.systemPromptOptimization.enable': 'Größe des System-Prompts optimieren',
|
||||
'settings.behavior.page.systemPromptOptimization.enableAria': 'Größe des OpenCode-System-Prompts optimieren',
|
||||
'settings.behavior.page.systemPromptOptimization.info': 'Reduziert den System-Prompt für die Build- und Plan-Agenten schätzungsweise um 40 %. Andere Agenten bleiben unverändert. Dadurch können benutzerdefinierte Definitionen entfernt werden, die Build oder Plan überschreiben, daher nicht für Workflows aktivieren, die diese Agenten anpassen. Ein Neustart von OpenCode wendet die Änderung an.',
|
||||
'settings.behavior.page.section.worktrees': 'Worktrees',
|
||||
'settings.behavior.page.worktreeFetchSource.enable': 'Remote vor dem Erstellen von Worktrees abrufen',
|
||||
'settings.behavior.page.worktreeFetchSource.enableAria': 'Änderungen vom Remote abrufen, bevor Worktrees erstellt werden',
|
||||
'settings.behavior.page.worktreeFetchSource.info': 'Neue Worktrees verzweigen vom abgerufenen Remote-Stand des aktuellen Branches statt vom lokalen Checkout und sind dadurch nicht veraltet. Beim Erstellen wird der lokale Stand verwendet, wenn das Remote nicht erreichbar ist.',
|
||||
'settings.behavior.page.systemPromptOptimization.restarting': 'OpenCode wird neu gestartet, um die System-Prompt-Optimierung anzuwenden…',
|
||||
'settings.openchamber.desktopNetwork.field.windowControls': 'Fenstersteuerungen',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Stil',
|
||||
|
||||
@@ -600,8 +600,9 @@ export const dict = {
|
||||
'sessions.sidebar.sessionDialogs.directory.errorOpenDescription': 'Desktop konnte keinen Dateizugriff gewähren.',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle': 'Fehler beim Hinzufügen des Projekts',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription': 'Bitte wählen Sie einen gültigen Verzeichnispfad aus.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'Fehler beim Entfernen des Worktrees',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'Worktree entfernt',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'Fehler beim Entfernen des Worktrees „{name}“',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removingTitle': 'Worktree „{name}“ wird entfernt…',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'Worktree „{name}“ entfernt',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removed': 'Worktree entfernt.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedWithRemote': 'Worktree und Remote-Branch entfernt.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.attachedArchived': 'Angefügter Worktree archiviert.',
|
||||
|
||||
@@ -607,10 +607,6 @@ export const settingsDict = {
|
||||
'settings.behavior.page.systemPromptOptimization.enable': 'Optimize system prompt size',
|
||||
'settings.behavior.page.systemPromptOptimization.enableAria': 'Optimize OpenCode system prompt size',
|
||||
'settings.behavior.page.systemPromptOptimization.info': 'Reduces the system prompt by an estimated 40% for the build and plan agents. Other agents are not changed. This can remove custom definitions that override build or plan, so do not enable it for workflows that customize those agents. Restarting OpenCode applies the change.',
|
||||
'settings.behavior.page.section.worktrees': 'Worktrees',
|
||||
'settings.behavior.page.worktreeFetchSource.enable': 'Fetch remote before creating worktrees',
|
||||
'settings.behavior.page.worktreeFetchSource.enableAria': 'Fetch changes from remote before creating worktrees',
|
||||
'settings.behavior.page.worktreeFetchSource.info': 'New worktrees branch from the fetched remote state of the current branch instead of the local checkout, so they are not stale. Creation falls back to the local state when the remote is unreachable.',
|
||||
'settings.behavior.page.systemPromptOptimization.restarting': 'Restarting OpenCode to apply system prompt optimization…',
|
||||
'settings.behavior.page.field.systemPromptPlaceholder': 'You are a helpful AI assistant...\n\nUse this space to define absolute rules for how the AI should behave across all sessions and providers.',
|
||||
'settings.behavior.page.section.responseStyle': 'Response style',
|
||||
|
||||
@@ -696,8 +696,9 @@ export const dict = {
|
||||
'sessions.sidebar.sessionDialogs.directory.errorOpenDescription': 'Desktop could not grant file access.',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle': 'Failed to add project',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription': 'Please select a valid directory path.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'Failed to remove worktree',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'Worktree removed',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'Failed to remove worktree “{name}”',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removingTitle': 'Removing worktree “{name}”…',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'Worktree “{name}” removed',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removed': 'Worktree removed.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedWithRemote': 'Worktree and remote branch removed.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.attachedArchived': 'Attached worktree archived.',
|
||||
|
||||
@@ -575,10 +575,6 @@ export const settingsDict = {
|
||||
"settings.behavior.page.systemPromptOptimization.enable": "Optimizar el tamaño del prompt del sistema",
|
||||
"settings.behavior.page.systemPromptOptimization.enableAria": "Optimizar el tamaño del prompt del sistema de OpenCode",
|
||||
"settings.behavior.page.systemPromptOptimization.info": "Reduce el prompt del sistema aproximadamente un 40 % para los agentes build y plan. Los demás agentes no cambian. Puede eliminar definiciones personalizadas que sobrescriban build o plan, así que no lo actives en esos flujos de trabajo. El cambio se aplica al reiniciar OpenCode.",
|
||||
"settings.behavior.page.section.worktrees": "Worktrees",
|
||||
"settings.behavior.page.worktreeFetchSource.enable": "Obtener cambios del remoto antes de crear worktrees",
|
||||
"settings.behavior.page.worktreeFetchSource.enableAria": "Obtener cambios del repositorio remoto antes de crear worktrees",
|
||||
"settings.behavior.page.worktreeFetchSource.info": "Los nuevos worktrees se crean a partir del estado remoto obtenido de la rama actual en lugar del checkout local, por lo que no quedan obsoletos. La creación usa el estado local si el remoto no está accesible.",
|
||||
"settings.behavior.page.systemPromptOptimization.restarting": "Reiniciando OpenCode para aplicar la optimización del prompt del sistema…",
|
||||
"settings.behavior.page.field.systemPromptPlaceholder": "Eres un asistente de IA útil...\n\nUsa este espacio para definir reglas absolutas sobre cómo la IA debe comportarse en todas las sesiones y proveedores.",
|
||||
"settings.behavior.page.section.responseStyle": "Estilo de respuesta",
|
||||
|
||||
@@ -697,8 +697,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.sessionDialogs.directory.errorOpenDescription": "Desktop no pudo otorgar acceso a archivos.",
|
||||
"sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle": "No se pudo añadir proyecto",
|
||||
"sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription": "Selecciona una ruta de directorio válida.",
|
||||
"sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle": "No se pudo eliminar worktree",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removedTitle": "Worktree eliminado",
|
||||
"sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle": "No se pudo eliminar el worktree «{name}»",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removingTitle": "Eliminando el worktree «{name}»…",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removedTitle": "Worktree «{name}» eliminado",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removed": "Worktree eliminado.",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removedWithRemote": "Worktree y rama remota eliminados.",
|
||||
"sessions.sidebar.sessionDialogs.worktree.attachedArchived": "Worktree adjunto archivado.",
|
||||
|
||||
@@ -493,10 +493,6 @@ export const settingsDict = {
|
||||
'settings.behavior.page.systemPromptOptimization.enable': 'Optimiser la taille du prompt système',
|
||||
'settings.behavior.page.systemPromptOptimization.enableAria': 'Optimiser la taille du prompt système d’OpenCode',
|
||||
'settings.behavior.page.systemPromptOptimization.info': 'Réduit le prompt système d’environ 40 % pour les agents build et plan. Les autres agents ne sont pas modifiés. Cela peut supprimer les définitions personnalisées qui remplacent build ou plan ; ne l’activez donc pas pour ces workflows. Le redémarrage d’OpenCode applique la modification.',
|
||||
'settings.behavior.page.section.worktrees': 'Worktrees',
|
||||
'settings.behavior.page.worktreeFetchSource.enable': 'Récupérer le dépôt distant avant de créer des worktrees',
|
||||
'settings.behavior.page.worktreeFetchSource.enableAria': 'Récupérer les changements du dépôt distant avant de créer des worktrees',
|
||||
'settings.behavior.page.worktreeFetchSource.info': 'Les nouveaux worktrees sont créés depuis l’état distant récupéré de la branche courante plutôt que depuis le checkout local, afin d’éviter qu’ils ne soient obsolètes. La création utilise l’état local si le dépôt distant est inaccessible.',
|
||||
'settings.behavior.page.systemPromptOptimization.restarting': 'Redémarrage d’OpenCode pour appliquer l’optimisation du prompt système…',
|
||||
'settings.behavior.page.field.systemPromptPlaceholder': 'Vous êtes un assistant IA utile...\n\nUtilisez cet espace pour définir des règles absolues sur la façon dont l\'IA doit se comporter dans toutes les sessions et tous les fournisseurs.',
|
||||
'settings.behavior.page.section.responseStyle': 'Style de réponse',
|
||||
|
||||
@@ -525,8 +525,9 @@ export const dict = {
|
||||
'sessions.sidebar.sessionDialogs.directory.errorOpenDescription': 'Desktop n\'a pas pu accorder l\'accès aux fichiers.',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle': 'Échec de l\'ajout du projet',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription': 'Veuillez sélectionner un chemin de répertoire valide.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'Échec de la suppression du worktree',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'Worktree supprimé',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'Échec de la suppression du worktree « {name} »',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removingTitle': 'Suppression du worktree « {name} »…',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'Worktree « {name} » supprimé',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removed': 'Worktree supprimé.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedWithRemote': 'Worktree et branche du dépôt distant supprimés.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.attachedArchived': 'Worktree ci-joint archivé.',
|
||||
|
||||
@@ -608,10 +608,6 @@ export const settingsDict = {
|
||||
'settings.behavior.page.systemPromptOptimization.enable': 'システムプロンプトのサイズを最適化',
|
||||
'settings.behavior.page.systemPromptOptimization.enableAria': 'OpenCode のシステムプロンプトサイズを最適化',
|
||||
'settings.behavior.page.systemPromptOptimization.info': 'build と plan エージェントのシステムプロンプトを約 40% 削減します。他のエージェントは変更されません。build または plan を上書きするカスタム定義が削除される可能性があるため、そのようなワークフローでは有効にしないでください。OpenCode の再起動後に変更が適用されます。',
|
||||
'settings.behavior.page.section.worktrees': 'ワークツリー',
|
||||
'settings.behavior.page.worktreeFetchSource.enable': 'ワークツリー作成前にリモートから取得',
|
||||
'settings.behavior.page.worktreeFetchSource.enableAria': 'ワークツリー作成前にリモートから変更を取得する',
|
||||
'settings.behavior.page.worktreeFetchSource.info': '新しいワークツリーは、ローカルのチェックアウトではなく現在のブランチのリモート状態を取得して作成されるため、最新の状態になります。リモートにアクセスできない場合はローカルの状態で作成されます。',
|
||||
'settings.behavior.page.systemPromptOptimization.restarting': 'システムプロンプトの最適化を適用するため OpenCode を再起動しています…',
|
||||
'settings.behavior.page.field.systemPromptPlaceholder': 'あなたは役立つ AI アシスタントです...\n\nこのスペースを使用して、すべての Session と Provider にわたって AI の動作方法に関する絶対的なルールを定義してください。',
|
||||
'settings.behavior.page.section.responseStyle': '応答スタイル',
|
||||
|
||||
@@ -697,8 +697,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.sessionDialogs.directory.errorOpenDescription': 'デスクトップがファイルアクセスを許可しませんでした。',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle': 'プロジェクトの追加に失敗しました',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription': '有効なディレクトリパスを選択してください。',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'ワークツリーの削除に失敗しました',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'ワークツリーを削除しました',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'ワークツリー「{name}」の削除に失敗しました',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removingTitle': 'ワークツリー「{name}」を削除しています…',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'ワークツリー「{name}」を削除しました',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removed': 'ワークツリーを削除しました。',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedWithRemote': 'ワークツリーとリモートブランチを削除しました。',
|
||||
'sessions.sidebar.sessionDialogs.worktree.attachedArchived': '添付のワークツリーをアーカイブしました。',
|
||||
|
||||
@@ -575,10 +575,6 @@ export const settingsDict = {
|
||||
'settings.behavior.page.systemPromptOptimization.enable': '시스템 프롬프트 크기 최적화',
|
||||
'settings.behavior.page.systemPromptOptimization.enableAria': 'OpenCode 시스템 프롬프트 크기 최적화',
|
||||
'settings.behavior.page.systemPromptOptimization.info': 'build 및 plan 에이전트의 시스템 프롬프트를 약 40% 줄입니다. 다른 에이전트는 변경되지 않습니다. build 또는 plan을 재정의하는 사용자 지정 정의가 제거될 수 있으므로 이러한 워크플로에서는 활성화하지 마세요. OpenCode를 다시 시작하면 변경 사항이 적용됩니다.',
|
||||
'settings.behavior.page.section.worktrees': '워크트리',
|
||||
'settings.behavior.page.worktreeFetchSource.enable': '워크트리를 만들기 전에 원격에서 가져오기',
|
||||
'settings.behavior.page.worktreeFetchSource.enableAria': '워크트리를 만들기 전에 원격에서 변경 사항 가져오기',
|
||||
'settings.behavior.page.worktreeFetchSource.info': '새 워크트리는 로컬 체크아웃 대신 현재 브랜치의 원격 상태를 가져와서 만들어지므로 최신 상태를 유지합니다. 원격에 접근할 수 없으면 로컬 상태로 만듭니다.',
|
||||
'settings.behavior.page.systemPromptOptimization.restarting': '시스템 프롬프트 최적화를 적용하기 위해 OpenCode를 다시 시작하는 중…',
|
||||
'settings.behavior.page.field.systemPromptPlaceholder': '당신은 유용한 AI 어시스턴트입니다...\n\n모든 세션과 공급자에서 AI가 어떻게 행동해야 하는지에 대한 절대적인 규칙을 정의할 수 있습니다.',
|
||||
'settings.behavior.page.section.responseStyle': '응답 스타일',
|
||||
|
||||
@@ -697,8 +697,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.sessionDialogs.directory.errorOpenDescription': '데스크톱에서 파일 접근 권한을 부여하지 못했습니다.',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle': '프로젝트 추가 실패',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription': '유효한 디렉터리 경로를 선택하세요.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': '워크트리 제거 실패',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': '워크트리 제거됨',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': '워크트리 “{name}” 제거 실패',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removingTitle': '워크트리 “{name}” 제거 중…',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': '워크트리 “{name}” 제거됨',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removed': '워크트리가 제거되었습니다.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedWithRemote': '워크트리와 리모트 브랜치가 제거되었습니다.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.attachedArchived': '첨부됨 워크트리 보관됨.',
|
||||
|
||||
@@ -160,10 +160,6 @@ export const settingsDict = {
|
||||
'settings.behavior.page.systemPromptOptimization.enable': 'Optymalizuj rozmiar promptu systemowego',
|
||||
'settings.behavior.page.systemPromptOptimization.enableAria': 'Optymalizuj rozmiar promptu systemowego OpenCode',
|
||||
'settings.behavior.page.systemPromptOptimization.info': 'Zmniejsza prompt systemowy o około 40% dla agentów build i plan. Inni agenci pozostają bez zmian. Może to usunąć niestandardowe definicje zastępujące build lub plan, dlatego nie włączaj tej opcji w takich przepływach pracy. Zmiana zostanie zastosowana po ponownym uruchomieniu OpenCode.',
|
||||
'settings.behavior.page.section.worktrees': 'Drzewa pracy',
|
||||
'settings.behavior.page.worktreeFetchSource.enable': 'Pobierz zmiany ze zdalnego przed utworzeniem drzew pracy',
|
||||
'settings.behavior.page.worktreeFetchSource.enableAria': 'Pobierz zmiany ze zdalnego repozytorium przed utworzeniem drzew pracy',
|
||||
'settings.behavior.page.worktreeFetchSource.info': 'Nowe drzewa pracy powstają z pobranego zdalnego stanu bieżącej gałęzi, a nie z lokalnej kopii roboczej, dzięki czemu nie są nieaktualne. Jeśli zdalne repozytorium jest niedostępne, używany jest stan lokalny.',
|
||||
'settings.behavior.page.systemPromptOptimization.restarting': 'Ponowne uruchamianie OpenCode w celu zastosowania optymalizacji promptu systemowego…',
|
||||
'settings.behavior.page.title': 'Zachowanie',
|
||||
'settings.behavior.page.toast.saveFailed': 'Nie udało się zapisać zachowania',
|
||||
|
||||
@@ -697,8 +697,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.sessionDialogs.directory.errorOpenDescription': 'Aplikacja desktopowa nie mogła nadać dostępu do plików.',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle': 'Nie udało się dodać projektu',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription': 'Wybierz prawidłową ścieżkę katalogu.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'Nie udało się usunąć drzewa pracy',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'Drzewo pracy usunięte',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'Nie udało się usunąć drzewa pracy „{name}”',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removingTitle': 'Usuwanie drzewa pracy „{name}”…',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'Drzewo pracy „{name}” usunięte',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removed': 'Drzewo pracy usunięte.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedWithRemote': 'Drzewo pracy i zdalna gałąź usunięte.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.attachedArchived': 'Dołączone drzewo pracy zarchiwizowane.',
|
||||
|
||||
@@ -575,10 +575,6 @@ export const settingsDict = {
|
||||
"settings.behavior.page.systemPromptOptimization.enable": "Otimizar o tamanho do prompt do sistema",
|
||||
"settings.behavior.page.systemPromptOptimization.enableAria": "Otimizar o tamanho do prompt do sistema do OpenCode",
|
||||
"settings.behavior.page.systemPromptOptimization.info": "Reduz o prompt do sistema em cerca de 40% para os agentes build e plan. Os outros agentes não são alterados. Isso pode remover definições personalizadas que substituem build ou plan, portanto não ative nesses fluxos de trabalho. A alteração é aplicada após reiniciar o OpenCode.",
|
||||
"settings.behavior.page.section.worktrees": "Worktrees",
|
||||
"settings.behavior.page.worktreeFetchSource.enable": "Buscar do remoto antes de criar worktrees",
|
||||
"settings.behavior.page.worktreeFetchSource.enableAria": "Buscar alterações do remoto antes de criar worktrees",
|
||||
"settings.behavior.page.worktreeFetchSource.info": "Novos worktrees são criados a partir do estado remoto buscado do branch atual, e não do checkout local, para que não fiquem desatualizados. A criação usa o estado local quando o remoto está inacessível.",
|
||||
"settings.behavior.page.systemPromptOptimization.restarting": "Reiniciando o OpenCode para aplicar a otimização do prompt do sistema…",
|
||||
"settings.behavior.page.field.systemPromptPlaceholder": "Você é um assistente de IA útil...\n\nUse este espaço para definir regras absolutas sobre como a IA deve se comportar em todas as sessões e provedores.",
|
||||
"settings.behavior.page.section.responseStyle": "Estilo de resposta",
|
||||
|
||||
@@ -697,8 +697,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.sessionDialogs.directory.errorOpenDescription": "O desktop não pôde conceder acesso aos arquivos.",
|
||||
"sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle": "Não foi possível adicionar projeto",
|
||||
"sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription": "Selecione uma caminho de diretório válida.",
|
||||
"sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle": "Não foi possível excluir worktree",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removedTitle": "Worktree excluído",
|
||||
"sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle": "Não foi possível excluir o worktree “{name}”",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removingTitle": "Excluindo o worktree “{name}”…",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removedTitle": "Worktree “{name}” excluído",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removed": "Worktree excluído.",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removedWithRemote": "Worktree e branch remota excluídos.",
|
||||
"sessions.sidebar.sessionDialogs.worktree.attachedArchived": "Worktree adjunto archivado.",
|
||||
|
||||
@@ -603,10 +603,6 @@ export const settingsDict = {
|
||||
'settings.behavior.page.systemPromptOptimization.enable': 'System prompt boyutunu optimize et',
|
||||
'settings.behavior.page.systemPromptOptimization.enableAria': 'OpenCode system prompt boyutunu optimize et',
|
||||
'settings.behavior.page.systemPromptOptimization.info': 'System prompt\'u, build ve plan agent\'ları için tahmini %40 küçültür. Diğer agent\'lar değişmez. Bu, build veya plan\'ı geçersiz kılan özel tanımları kaldırabileceği için bu agent\'ları özelleştiren iş akışlarında etkinleştirmeyin. Değişiklik, OpenCode yeniden başlatıldığında uygulanır.',
|
||||
'settings.behavior.page.section.worktrees': 'Worktree\'ler',
|
||||
'settings.behavior.page.worktreeFetchSource.enable': 'Worktree oluşturmadan önce uzak depodan fetch et',
|
||||
'settings.behavior.page.worktreeFetchSource.enableAria': 'Worktree oluşturmadan önce uzak depodan değişiklikleri fetch et',
|
||||
'settings.behavior.page.worktreeFetchSource.info': 'Yeni worktree\'ler yerel checkout yerine geçerli dalın uzak depodan fetch edilen durumuna dayanır, böylece güncel kalır. Uzak depoya erişilemezse yerel durum kullanılır.',
|
||||
'settings.behavior.page.systemPromptOptimization.restarting': 'System prompt optimizasyonunu uygulamak için OpenCode yeniden başlatılıyor…',
|
||||
'settings.behavior.page.field.systemPromptPlaceholder': 'Sen yardımcı bir AI asistanısın...\\n\\nBu alanı, AI\'ın tüm session\'lar ve provider\'lar genelinde nasıl davranacağına dair mutlak kuralları tanımlamak için kullanın.',
|
||||
'settings.behavior.page.section.responseStyle': 'Yanıt stili',
|
||||
|
||||
@@ -678,8 +678,9 @@ export const dict = {
|
||||
'sessions.sidebar.sessionDialogs.directory.errorOpenDescription': 'Masaüstü uygulaması dosya erişimi izni veremedi.',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle': 'Proje eklenemedi',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription': 'Geçerli bir dizin yolu seçin.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': 'Worktree kaldırılamadı',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'Worktree kaldırıldı',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': '“{name}” worktree kaldırılamadı',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removingTitle': '“{name}” worktree kaldırılıyor…',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': '“{name}” worktree kaldırıldı',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removed': 'Worktree kaldırıldı.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedWithRemote': 'Worktree ve uzak branch kaldırıldı.',
|
||||
'sessions.sidebar.sessionDialogs.worktree.attachedArchived': 'Bağlı worktree arşivlendi.',
|
||||
|
||||
@@ -575,10 +575,6 @@ export const settingsDict = {
|
||||
"settings.behavior.page.systemPromptOptimization.enable": "Оптимізувати розмір системного промпту",
|
||||
"settings.behavior.page.systemPromptOptimization.enableAria": "Оптимізувати розмір системного промпту OpenCode",
|
||||
"settings.behavior.page.systemPromptOptimization.info": "Зменшує системний промпт орієнтовно на 40% для агентів build і plan. Інші агенти не змінюються. Це може видалити власні визначення, які перевизначають build або plan, тому не вмикайте цю функцію для таких робочих процесів. Зміна застосовується після перезапуску OpenCode.",
|
||||
"settings.behavior.page.section.worktrees": "Worktree",
|
||||
"settings.behavior.page.worktreeFetchSource.enable": "Отримувати зміни з віддаленого перед створенням worktree",
|
||||
"settings.behavior.page.worktreeFetchSource.enableAria": "Отримати зміни з віддаленого репозиторію перед створенням worktree",
|
||||
"settings.behavior.page.worktreeFetchSource.info": "Нові worktree створюються з отриманого віддаленого стану поточної гілки, а не з локального checkout, тому не є застарілими. Якщо віддалений репозиторій недоступний, використовується локальний стан.",
|
||||
"settings.behavior.page.systemPromptOptimization.restarting": "Перезапуск OpenCode для застосування оптимізації системного промпту…",
|
||||
"settings.behavior.page.field.systemPromptPlaceholder": "Ви — корисний AI-асистент...\n\nВикористовуйте цей простір для визначення абсолютних правил поведінки AI для всіх сесій та провайдерів.",
|
||||
"settings.behavior.page.section.responseStyle": "Стиль відповіді",
|
||||
|
||||
@@ -697,8 +697,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.sessionDialogs.directory.errorOpenDescription": "Десктопний застосунок не зміг надати доступ до файлу.",
|
||||
"sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle": "Не вдалося додати проєкт",
|
||||
"sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription": "Виберіть правильний шлях до каталогу.",
|
||||
"sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle": "Не вдалося видалити worktree",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removedTitle": "Worktree видалено",
|
||||
"sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle": "Не вдалося видалити worktree «{name}»",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removingTitle": "Видаляємо worktree «{name}»…",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removedTitle": "Worktree «{name}» видалено",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removed": "Worktree видалено.",
|
||||
"sessions.sidebar.sessionDialogs.worktree.removedWithRemote": "Worktree та віддалену гілку видалено.",
|
||||
"sessions.sidebar.sessionDialogs.worktree.attachedArchived": "Прикріплене worktree заархівовано.",
|
||||
|
||||
@@ -575,10 +575,6 @@ export const settingsDict = {
|
||||
'settings.behavior.page.systemPromptOptimization.enable': '优化系统提示词大小',
|
||||
'settings.behavior.page.systemPromptOptimization.enableAria': '优化 OpenCode 系统提示词大小',
|
||||
'settings.behavior.page.systemPromptOptimization.info': '预计可将 build 和 plan 代理的系统提示词缩减约 40%,其他代理不会改变。这可能会移除覆盖 build 或 plan 的自定义定义,因此此类工作流请勿启用。重启 OpenCode 后应用更改。',
|
||||
'settings.behavior.page.section.worktrees': '工作树',
|
||||
'settings.behavior.page.worktreeFetchSource.enable': '创建工作树前先从远程获取',
|
||||
'settings.behavior.page.worktreeFetchSource.enableAria': '创建工作树前先从远程仓库获取变更',
|
||||
'settings.behavior.page.worktreeFetchSource.info': '新工作树基于当前分支从远程获取的状态创建,而不是本地检出,因此不会滞后。远程不可用时将回退为本地状态。',
|
||||
'settings.behavior.page.systemPromptOptimization.restarting': '正在重启 OpenCode 以应用系统提示词优化…',
|
||||
'settings.behavior.page.field.systemPromptPlaceholder': '你是一个有帮助的 AI 助手...\n\n使用此空间定义 AI 在所有会话和提供商中的绝对行为规则。',
|
||||
'settings.behavior.page.section.responseStyle': '回复风格',
|
||||
|
||||
@@ -697,8 +697,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.sessionDialogs.directory.errorOpenDescription': '桌面端无法授予文件访问权限。',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle': '添加项目失败',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription': '请选择有效的目录路径。',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': '移除工作树失败',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': '工作树已移除',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': '移除工作树“{name}”失败',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removingTitle': '正在移除工作树“{name}”…',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': '工作树“{name}”已移除',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removed': '工作树已移除。',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedWithRemote': '工作树和远程分支已移除。',
|
||||
'sessions.sidebar.sessionDialogs.worktree.attachedArchived': '关联工作树已归档。',
|
||||
|
||||
@@ -572,10 +572,6 @@ export const settingsDict = {
|
||||
'settings.behavior.page.systemPromptOptimization.enable': '最佳化系統提示詞大小',
|
||||
'settings.behavior.page.systemPromptOptimization.enableAria': '最佳化 OpenCode 系統提示詞大小',
|
||||
'settings.behavior.page.systemPromptOptimization.info': '預計可將 build 和 plan 代理程式的系統提示詞縮減約 40%,其他代理程式不會變更。這可能會移除覆寫 build 或 plan 的自訂定義,因此此類工作流程請勿啟用。重新啟動 OpenCode 後套用變更。',
|
||||
'settings.behavior.page.section.worktrees': '工作樹',
|
||||
'settings.behavior.page.worktreeFetchSource.enable': '建立工作樹前先從遠端擷取',
|
||||
'settings.behavior.page.worktreeFetchSource.enableAria': '建立工作樹前先從遠端儲存庫擷取變更',
|
||||
'settings.behavior.page.worktreeFetchSource.info': '新工作樹基於目前分支從遠端擷取的狀態建立,而非本機檢出,因此不會落後。遠端無法使用時將改用本機狀態。',
|
||||
'settings.behavior.page.systemPromptOptimization.restarting': '正在重新啟動 OpenCode 以套用系統提示詞最佳化…',
|
||||
'settings.behavior.page.field.systemPromptPlaceholder': '你是一個有幫助的 AI 助助理...\n\n在此設定 AI 在所有工作階段與服務提供者中都必須遵守的絕對行為規則。',
|
||||
'settings.behavior.page.section.responseStyle': '回覆風格',
|
||||
|
||||
@@ -710,8 +710,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.sessionDialogs.directory.errorOpenDescription': '桌面端無法授予檔案存取權限。',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectTitle': '新增專案失敗',
|
||||
'sessions.sidebar.sessionDialogs.directory.errorAddProjectDescription': '請選擇有效的目錄路徑。',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': '移除 worktree 失敗',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'worktree 已移除',
|
||||
'sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle': '移除 worktree「{name}」失敗',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removingTitle': '正在移除 worktree「{name}」…',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedTitle': 'worktree「{name}」已移除',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removed': 'worktree 已移除。',
|
||||
'sessions.sidebar.sessionDialogs.worktree.removedWithRemote': 'worktree 和遠端分支已移除。',
|
||||
'sessions.sidebar.sessionDialogs.worktree.attachedArchived': '關聯 worktree 已封存。',
|
||||
|
||||
@@ -1278,9 +1278,6 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
||||
if (typeof candidate.autoCreateWorktree === 'boolean') {
|
||||
result.autoCreateWorktree = candidate.autoCreateWorktree;
|
||||
}
|
||||
if (typeof candidate.worktreeFetchSource === 'boolean') {
|
||||
result.worktreeFetchSource = candidate.worktreeFetchSource;
|
||||
}
|
||||
if (typeof candidate.gitmojiEnabled === 'boolean') {
|
||||
result.gitmojiEnabled = candidate.gitmojiEnabled;
|
||||
}
|
||||
|
||||
@@ -607,12 +607,6 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
|
||||
titleKey: 'settings.openchamber.worktrees.setup.waitForCommands',
|
||||
keywords: ['worktree', 'setup commands', 'bootstrap', 'wait'],
|
||||
},
|
||||
{
|
||||
id: 'behavior.worktree-fetch-source',
|
||||
page: 'behavior',
|
||||
titleKey: 'settings.behavior.page.section.worktrees',
|
||||
keywords: ['worktree', 'fetch', 'remote', 'source branch', 'stale'],
|
||||
},
|
||||
{
|
||||
id: 'remote-instances.client-auth',
|
||||
page: 'remote-instances',
|
||||
|
||||
@@ -7,7 +7,6 @@ interface MockBranchTracking {
|
||||
|
||||
const project: ProjectRef = { id: 'project-1', path: '/repo' };
|
||||
|
||||
let fetchSourceEnabled = true;
|
||||
let projectRoot = '/repo';
|
||||
let gitStatus: {
|
||||
current: string;
|
||||
@@ -16,25 +15,7 @@ let gitStatus: {
|
||||
behind: number;
|
||||
} | null = null;
|
||||
let branchTracking: MockBranchTracking = {};
|
||||
let gitFetchError: Error | null = null;
|
||||
let gitFetchSuccess = true;
|
||||
const gitFetchCalls: Array<{ directory: string; remote?: string; branch?: string }> = [];
|
||||
const createdPayloads: CreateWorktreeArgs[] = [];
|
||||
const toastWarnings: string[] = [];
|
||||
|
||||
mock.module('@/components/ui', () => ({
|
||||
toast: {
|
||||
warning: (message: string) => {
|
||||
toastWarnings.push(message);
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('@/stores/useConfigStore', () => ({
|
||||
useConfigStore: {
|
||||
getState: () => ({ settingsWorktreeFetchSource: fetchSourceEnabled }),
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('@/lib/gitApi', () => ({
|
||||
getGitStatus: () => (gitStatus ? Promise.resolve(gitStatus) : Promise.reject(new Error('no status'))),
|
||||
@@ -45,13 +26,6 @@ mock.module('@/lib/gitApi', () => ({
|
||||
? { main: { current: false, name: 'main', commit: 'abc1234', label: '', tracking: branchTracking.main } }
|
||||
: {},
|
||||
}),
|
||||
gitFetch: (directory: string, options: { remote?: string; branch?: string } = {}) => {
|
||||
gitFetchCalls.push({ directory, ...options });
|
||||
if (gitFetchError) {
|
||||
return Promise.reject(gitFetchError);
|
||||
}
|
||||
return Promise.resolve({ success: gitFetchSuccess });
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('@/lib/worktrees/worktreeStatus', () => ({
|
||||
@@ -78,7 +52,7 @@ mock.module('@/lib/worktrees/worktreeManager', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const { createWorktreeWithDefaults, withWorktreeFetchedStartRef } = await import('./worktreeCreate');
|
||||
const { createWorktreeWithDefaults, withWorktreeRemoteStartRef } = await import('./worktreeCreate');
|
||||
|
||||
const baseArgs = (overrides: CreateWorktreeArgs = {}): CreateWorktreeArgs => ({
|
||||
preferredName: 'openchamber/feature',
|
||||
@@ -88,174 +62,116 @@ const baseArgs = (overrides: CreateWorktreeArgs = {}): CreateWorktreeArgs => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('withWorktreeFetchedStartRef', () => {
|
||||
describe('withWorktreeRemoteStartRef', () => {
|
||||
beforeEach(() => {
|
||||
fetchSourceEnabled = true;
|
||||
projectRoot = '/repo';
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 0 };
|
||||
branchTracking = {};
|
||||
gitFetchError = null;
|
||||
gitFetchSuccess = true;
|
||||
gitFetchCalls.length = 0;
|
||||
createdPayloads.length = 0;
|
||||
toastWarnings.length = 0;
|
||||
});
|
||||
|
||||
test('bases the new worktree on the fetched remote-tracking ref', async () => {
|
||||
test('selects the tracked remote ref for the runtime to fetch', async () => {
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 15 };
|
||||
|
||||
const args = await withWorktreeFetchedStartRef(project, baseArgs());
|
||||
const args = await withWorktreeRemoteStartRef(project, baseArgs());
|
||||
|
||||
expect(args.startRef).toBe('remotes/origin/main');
|
||||
expect(gitFetchCalls).toEqual([{ directory: '/repo', remote: 'origin', branch: 'main' }]);
|
||||
});
|
||||
|
||||
test('fetches the branch the explicit start ref names when it is the current branch', async () => {
|
||||
test('selects the tracked remote ref when the explicit start ref names the current branch', async () => {
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 3 };
|
||||
|
||||
const args = await withWorktreeFetchedStartRef(project, baseArgs({ startRef: 'main' }));
|
||||
const args = await withWorktreeRemoteStartRef(project, baseArgs({ startRef: 'main' }));
|
||||
|
||||
expect(args.startRef).toBe('remotes/origin/main');
|
||||
expect(gitFetchCalls).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('falls back to the original args when the fetch fails', async () => {
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 15 };
|
||||
gitFetchError = new Error('network down');
|
||||
|
||||
const args = baseArgs();
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(resolved.startRef).toBe(undefined);
|
||||
expect(gitFetchCalls).toHaveLength(1);
|
||||
expect(toastWarnings).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('falls back to the original args when the fetch resolves unsuccessful', async () => {
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 15 };
|
||||
gitFetchSuccess = false;
|
||||
|
||||
const args = baseArgs();
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(resolved.startRef).toBe(undefined);
|
||||
expect(gitFetchCalls).toHaveLength(1);
|
||||
expect(toastWarnings).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('does not fetch when disabled in config', async () => {
|
||||
fetchSourceEnabled = false;
|
||||
|
||||
const args = baseArgs();
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
expect(toastWarnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('does not fetch when the base branch has local-only commits', async () => {
|
||||
test('keeps the local base when it has unpublished commits', async () => {
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 2, behind: 15 };
|
||||
|
||||
const args = baseArgs();
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
const resolved = await withWorktreeRemoteStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('does not fetch without upstream tracking', async () => {
|
||||
test('keeps the local base without upstream tracking', async () => {
|
||||
gitStatus = { current: 'main', tracking: null, ahead: 0, behind: 0 };
|
||||
|
||||
const args = baseArgs();
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
const resolved = await withWorktreeRemoteStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('does not fetch when git status is unavailable', async () => {
|
||||
test('keeps the requested base when git status is unavailable', async () => {
|
||||
gitStatus = null;
|
||||
|
||||
const args = baseArgs();
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
const resolved = await withWorktreeRemoteStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('keeps an explicit non-root local start ref untouched', async () => {
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 15 };
|
||||
|
||||
const args = baseArgs({ startRef: 'release/1.2' });
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
const resolved = await withWorktreeRemoteStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('keeps an explicit remote start ref untouched', async () => {
|
||||
const args = baseArgs({ startRef: 'remotes/origin/main' });
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
const resolved = await withWorktreeRemoteStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('keeps a commit SHA start ref untouched', async () => {
|
||||
const sha = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0';
|
||||
const args = baseArgs({ startRef: sha });
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
const resolved = await withWorktreeRemoteStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('does not fetch in existing mode', async () => {
|
||||
test('keeps the requested base in existing mode', async () => {
|
||||
const args = baseArgs({ mode: 'existing', existingBranch: 'origin/feature' });
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
const resolved = await withWorktreeRemoteStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('does not fetch on a detached root checkout', async () => {
|
||||
test('keeps the requested base on a detached root checkout', async () => {
|
||||
gitStatus = { current: '', tracking: null, ahead: 0, behind: 0 };
|
||||
|
||||
const args = baseArgs();
|
||||
const resolved = await withWorktreeFetchedStartRef(project, args);
|
||||
const resolved = await withWorktreeRemoteStartRef(project, args);
|
||||
|
||||
expect(resolved).toBe(args);
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('reads the root checkout state, not the project directory state', async () => {
|
||||
projectRoot = '/primary';
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 15 };
|
||||
|
||||
const args = await withWorktreeFetchedStartRef(project, baseArgs());
|
||||
const args = await withWorktreeRemoteStartRef(project, baseArgs());
|
||||
|
||||
expect(args.startRef).toBe('remotes/origin/main');
|
||||
expect(gitFetchCalls).toEqual([{ directory: '/repo', remote: 'origin', branch: 'main' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWorktreeWithDefaults fetch integration', () => {
|
||||
describe('createWorktreeWithDefaults remote source integration', () => {
|
||||
beforeEach(() => {
|
||||
fetchSourceEnabled = true;
|
||||
projectRoot = '/repo';
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 0 };
|
||||
branchTracking = { main: 'origin/main' };
|
||||
gitFetchError = null;
|
||||
gitFetchSuccess = true;
|
||||
gitFetchCalls.length = 0;
|
||||
createdPayloads.length = 0;
|
||||
toastWarnings.length = 0;
|
||||
});
|
||||
|
||||
test('sets the new branch\'s own upstream when refreshed from remote', async () => {
|
||||
test('sets the new branch\'s own upstream when using the tracked remote source', async () => {
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 15 };
|
||||
|
||||
await createWorktreeWithDefaults(project, baseArgs());
|
||||
@@ -279,19 +195,6 @@ describe('createWorktreeWithDefaults fetch integration', () => {
|
||||
expect(createdPayloads[0].upstreamBranch).toBe('openchamber/feature');
|
||||
});
|
||||
|
||||
test('keeps upstream defaults when the fetch falls back', async () => {
|
||||
gitStatus = { current: 'main', tracking: 'origin/main', ahead: 0, behind: 15 };
|
||||
gitFetchError = new Error('network down');
|
||||
|
||||
await createWorktreeWithDefaults(project, baseArgs());
|
||||
|
||||
expect(createdPayloads).toHaveLength(1);
|
||||
expect(createdPayloads[0].startRef).toBe(undefined);
|
||||
expect(createdPayloads[0].setUpstream).toBe(true);
|
||||
expect(createdPayloads[0].upstreamRemote).toBe('origin');
|
||||
expect(createdPayloads[0].upstreamBranch).toBe('openchamber/feature');
|
||||
});
|
||||
|
||||
test('passes an explicit remote start ref through with upstream defaults as before', async () => {
|
||||
await createWorktreeWithDefaults(project, baseArgs({ startRef: 'remotes/origin/main' }));
|
||||
|
||||
@@ -300,6 +203,5 @@ describe('createWorktreeWithDefaults fetch integration', () => {
|
||||
expect(createdPayloads[0].setUpstream).toBe(true);
|
||||
expect(createdPayloads[0].upstreamRemote).toBe('origin');
|
||||
expect(createdPayloads[0].upstreamBranch).toBe('openchamber/feature');
|
||||
expect(gitFetchCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { toast } from '@/components/ui';
|
||||
import { getGitBranches, getGitStatus, gitFetch } from '@/lib/gitApi';
|
||||
import { formatMessage, useI18nStore } from '@/lib/i18n';
|
||||
import { getGitBranches, getGitStatus } from '@/lib/gitApi';
|
||||
import type { CreateWorktreeArgs, ProjectRef } from '@/lib/worktrees/worktreeManager';
|
||||
import { createWorktree } from '@/lib/worktrees/worktreeManager';
|
||||
import { getRootBranch, resolveProjectRoot } from '@/lib/worktrees/worktreeStatus';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
|
||||
const parseTrackingRef = (tracking: string | null | undefined): { remote: string; branch: string } | null => {
|
||||
const value = String(tracking || '').trim().replace(/^remotes\//, '');
|
||||
@@ -122,17 +119,13 @@ const normalizeLocalBranchName = (value: string): string => {
|
||||
.replace(/^refs\/heads\//, '');
|
||||
};
|
||||
|
||||
export const withWorktreeFetchedStartRef = async (
|
||||
export const withWorktreeRemoteStartRef = async (
|
||||
project: ProjectRef,
|
||||
args: CreateWorktreeArgs
|
||||
): Promise<CreateWorktreeArgs> => {
|
||||
if (args.mode === 'existing') {
|
||||
return args;
|
||||
}
|
||||
if (useConfigStore.getState().settingsWorktreeFetchSource === false) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const rawStartRef = String(args.startRef || '').trim();
|
||||
if (rawStartRef && rawStartRef !== 'HEAD') {
|
||||
if (REMOTE_START_REF_PATTERN.test(rawStartRef) || COMMIT_SHA_PATTERN.test(rawStartRef)) {
|
||||
@@ -161,22 +154,6 @@ export const withWorktreeFetchedStartRef = async (
|
||||
return args;
|
||||
}
|
||||
|
||||
const fallbackToLocalSource = () => {
|
||||
toast.warning(
|
||||
formatMessage(useI18nStore.getState().dictionary, 'session.newWorktree.toast.fetchSourceFailed'),
|
||||
);
|
||||
return args;
|
||||
};
|
||||
|
||||
try {
|
||||
const fetchResult = await gitFetch(projectDirectory, { remote: tracking.remote, branch: tracking.branch });
|
||||
if (fetchResult.success !== true) {
|
||||
return fallbackToLocalSource();
|
||||
}
|
||||
} catch {
|
||||
return fallbackToLocalSource();
|
||||
}
|
||||
|
||||
return { ...args, startRef: `remotes/${tracking.remote}/${tracking.branch}` };
|
||||
};
|
||||
|
||||
@@ -185,7 +162,7 @@ export const createWorktreeWithDefaults = async (
|
||||
args: CreateWorktreeArgs,
|
||||
options?: { resolvedRootTrackingRemote?: string | null }
|
||||
) => {
|
||||
const fetchedArgs = await withWorktreeFetchedStartRef(project, args);
|
||||
const resolvedArgs = await withWorktreeUpstreamDefaults(project.path, fetchedArgs, options);
|
||||
const remoteArgs = await withWorktreeRemoteStartRef(project, args);
|
||||
const resolvedArgs = await withWorktreeUpstreamDefaults(project.path, remoteArgs, options);
|
||||
return createWorktree(project, resolvedArgs);
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ const createdWorktree = {
|
||||
let createdWorktreeResult: GitWorktreeCreateResult = createdWorktree;
|
||||
const bootstrapWatcherCalls: string[] = [];
|
||||
const bootstrapWatcherOptions: Array<{ onReady?: () => void }> = [];
|
||||
const warningToasts: string[] = [];
|
||||
|
||||
const sessionState = {
|
||||
availableWorktreesByProject: new Map<string, WorktreeMetadata[]>(),
|
||||
@@ -40,6 +41,17 @@ mock.module('@/lib/openchamberConfig', () => ({
|
||||
substituteCommandVariables: (command: string) => command,
|
||||
}));
|
||||
|
||||
mock.module('@/components/ui', () => ({
|
||||
toast: {
|
||||
warning: (message: string) => warningToasts.push(message),
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('@/lib/i18n', () => ({
|
||||
formatMessage: () => 'session.newWorktree.toast.fetchSourceFailed',
|
||||
useI18nStore: { getState: () => ({ dictionary: {} }) },
|
||||
}));
|
||||
|
||||
mock.module('@/lib/worktrees/worktreeBootstrap', () => ({
|
||||
clearWorktreeBootstrapState: mock(),
|
||||
markWorktreeBootstrapPending: mock(),
|
||||
@@ -130,6 +142,7 @@ describe('worktreeManager list invalidation', () => {
|
||||
validatePayloads.length = 0;
|
||||
bootstrapWatcherCalls.length = 0;
|
||||
bootstrapWatcherOptions.length = 0;
|
||||
warningToasts.length = 0;
|
||||
createdWorktreeResult = createdWorktree;
|
||||
sessionState.availableWorktreesByProject = new Map();
|
||||
sessionState.availableWorktrees = [];
|
||||
@@ -137,6 +150,17 @@ describe('worktreeManager list invalidation', () => {
|
||||
attachmentState.attachments = new Map();
|
||||
});
|
||||
|
||||
test('warns when worktree creation falls back after a source fetch failure', async () => {
|
||||
createdWorktreeResult = { ...createdWorktree, sourceFetchFailed: true };
|
||||
|
||||
await createWorktree({ id: 'project-fetch-fallback', path: '/repo' }, {
|
||||
branchName: 'feature',
|
||||
worktreeName: 'feature',
|
||||
});
|
||||
|
||||
expect(warningToasts).toEqual(['session.newWorktree.toast.fetchSourceFailed']);
|
||||
});
|
||||
|
||||
test('retries an in-flight list when a worktree is created before it resolves', async () => {
|
||||
const project = { id: 'project-1', path: '/repo' };
|
||||
const listing = listProjectWorktrees(project);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { substituteCommandVariables } from '@/lib/openchamberConfig';
|
||||
import { toast } from '@/components/ui';
|
||||
import { formatMessage, useI18nStore } from '@/lib/i18n';
|
||||
import type { WorktreeMetadata } from '@/types/worktree';
|
||||
import {
|
||||
deleteRemoteBranch,
|
||||
@@ -58,6 +60,10 @@ const normalizePath = (value: string): string => {
|
||||
return replaced.length > 1 ? replaced.replace(/\/+$/, '') : replaced;
|
||||
};
|
||||
|
||||
/** The name the sidebar shows for a worktree, used in worktree-scoped toasts. */
|
||||
export const getWorktreeDisplayName = (worktree: WorktreeMetadata): string =>
|
||||
worktree.branch || worktree.label || worktree.path;
|
||||
|
||||
export const getLatestWorktreeMetadata = (metadata: WorktreeMetadata): WorktreeMetadata => {
|
||||
const target = normalizePath(metadata.path);
|
||||
const state = useSessionUIStore.getState();
|
||||
@@ -508,6 +514,11 @@ export async function createWorktree(project: ProjectRef, args: CreateWorktreeAr
|
||||
const payload = toCreatePayload(args, projectDirectory);
|
||||
|
||||
const created = await git.worktree.create(projectDirectory, payload);
|
||||
if (created?.sourceFetchFailed) {
|
||||
toast.warning(
|
||||
formatMessage(useI18nStore.getState().dictionary, 'session.newWorktree.toast.fetchSourceFailed'),
|
||||
);
|
||||
}
|
||||
const returnedName = typeof created?.name === 'string' ? created.name : '';
|
||||
const returnedBranch = typeof created?.branch === 'string' ? created.branch : '';
|
||||
const returnedPath = typeof created?.path === 'string' ? created.path : '';
|
||||
|
||||
@@ -56,7 +56,6 @@ interface OpenChamberDefaults {
|
||||
defaultVariant?: string;
|
||||
defaultAgent?: string;
|
||||
autoCreateWorktree?: boolean;
|
||||
worktreeFetchSource?: boolean;
|
||||
gitmojiEnabled?: boolean;
|
||||
defaultFileViewerPreview?: boolean;
|
||||
zenModel?: string;
|
||||
@@ -129,7 +128,6 @@ const requestOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
|
||||
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
|
||||
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
|
||||
worktreeFetchSource: typeof data?.worktreeFetchSource === 'boolean' ? data.worktreeFetchSource : undefined,
|
||||
gitmojiEnabled,
|
||||
defaultFileViewerPreview,
|
||||
zenModel: zenModel.length > 0 ? zenModel : undefined,
|
||||
@@ -176,7 +174,6 @@ const requestOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
|
||||
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
|
||||
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
|
||||
worktreeFetchSource: typeof data?.worktreeFetchSource === 'boolean' ? data.worktreeFetchSource : undefined,
|
||||
gitmojiEnabled,
|
||||
defaultFileViewerPreview,
|
||||
zenModel: zenModel.length > 0 ? zenModel : undefined,
|
||||
@@ -1056,7 +1053,6 @@ interface ConfigStore {
|
||||
// when neither our settingsDefaultModel nor the resolved agent pins a model.
|
||||
opencodeDefaultModel: string | undefined;
|
||||
settingsAutoCreateWorktree: boolean;
|
||||
settingsWorktreeFetchSource: boolean;
|
||||
settingsGitmojiEnabled: boolean;
|
||||
settingsDefaultFileViewerPreview: boolean;
|
||||
settingsZenModel: string | undefined;
|
||||
@@ -1144,7 +1140,6 @@ interface ConfigStore {
|
||||
setSettingsDefaultVariant: (variant: string | undefined) => void;
|
||||
setSettingsDefaultAgent: (agent: string | undefined) => void;
|
||||
setSettingsAutoCreateWorktree: (enabled: boolean) => void;
|
||||
setSettingsWorktreeFetchSource: (enabled: boolean) => void;
|
||||
setSettingsGitmojiEnabled: (enabled: boolean) => void;
|
||||
setSettingsDefaultFileViewerPreview: (enabled: boolean) => void;
|
||||
setSettingsZenModel: (model: string | undefined) => void;
|
||||
@@ -1226,7 +1221,6 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
opencodeDefaultAgent: undefined,
|
||||
opencodeDefaultModel: undefined,
|
||||
settingsAutoCreateWorktree: false,
|
||||
settingsWorktreeFetchSource: true,
|
||||
settingsGitmojiEnabled: false,
|
||||
settingsDefaultFileViewerPreview: false,
|
||||
settingsZenModel: undefined,
|
||||
@@ -2171,7 +2165,6 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
settingsDefaultVariant: openChamberDefaults.defaultVariant,
|
||||
settingsDefaultAgent: openChamberDefaults.defaultAgent,
|
||||
settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false,
|
||||
settingsWorktreeFetchSource: openChamberDefaults.worktreeFetchSource ?? true,
|
||||
settingsGitmojiEnabled: openChamberDefaults.gitmojiEnabled ?? false,
|
||||
settingsDefaultFileViewerPreview: openChamberDefaults.defaultFileViewerPreview ?? false,
|
||||
settingsZenModel: resolvedZenModel,
|
||||
@@ -2921,10 +2914,6 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
set({ settingsAutoCreateWorktree: enabled });
|
||||
},
|
||||
|
||||
setSettingsWorktreeFetchSource: (enabled: boolean) => {
|
||||
set({ settingsWorktreeFetchSource: enabled });
|
||||
},
|
||||
|
||||
setSettingsGitmojiEnabled: (enabled: boolean) => {
|
||||
set({ settingsGitmojiEnabled: enabled });
|
||||
},
|
||||
@@ -3467,7 +3456,6 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
settingsDefaultVariant: state.settingsDefaultVariant,
|
||||
settingsDefaultAgent: state.settingsDefaultAgent,
|
||||
settingsAutoCreateWorktree: state.settingsAutoCreateWorktree,
|
||||
settingsWorktreeFetchSource: state.settingsWorktreeFetchSource,
|
||||
settingsGitmojiEnabled: state.settingsGitmojiEnabled,
|
||||
settingsDefaultFileViewerPreview: state.settingsDefaultFileViewerPreview,
|
||||
settingsZenModel: state.settingsZenModel,
|
||||
|
||||
Reference in New Issue
Block a user