fix(worktree): normalize paths and improve worktree deletion workflow
This commit is contained in:
@@ -247,7 +247,8 @@ export const WorktreeSectionContent: React.FC = () => {
|
|||||||
|
|
||||||
// Delete worktree handler
|
// Delete worktree handler
|
||||||
const handleDeleteWorktree = React.useCallback((worktree: WorktreeMetadata) => {
|
const handleDeleteWorktree = React.useCallback((worktree: WorktreeMetadata) => {
|
||||||
const normalizedWorktreePath = worktree.path.replace(/\\/g, '/').replace(/\/+$/, '');
|
const normalize = (value: string): string => value.replace(/\\/g, '/').replace(/\/+$/, '');
|
||||||
|
const normalizedWorktreePath = normalize(worktree.path);
|
||||||
|
|
||||||
// Find sessions linked to this worktree by:
|
// Find sessions linked to this worktree by:
|
||||||
// 1. Worktree metadata path match
|
// 1. Worktree metadata path match
|
||||||
@@ -255,14 +256,14 @@ export const WorktreeSectionContent: React.FC = () => {
|
|||||||
const directSessions = sessions.filter((session) => {
|
const directSessions = sessions.filter((session) => {
|
||||||
// Check worktree metadata
|
// Check worktree metadata
|
||||||
const metadata = getWorktreeMetadata(session.id);
|
const metadata = getWorktreeMetadata(session.id);
|
||||||
if (metadata?.path === worktree.path) {
|
if (metadata?.path && normalize(metadata.path) === normalizedWorktreePath) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check session directory
|
// Check session directory
|
||||||
const sessionDir = (session as { directory?: string }).directory;
|
const sessionDir = (session as { directory?: string }).directory;
|
||||||
if (sessionDir) {
|
if (sessionDir) {
|
||||||
const normalizedSessionDir = sessionDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
const normalizedSessionDir = normalize(sessionDir);
|
||||||
if (normalizedSessionDir === normalizedWorktreePath) {
|
if (normalizedSessionDir === normalizedWorktreePath) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ export const SessionDialogs: React.FC = () => {
|
|||||||
return normalizeProjectDirectory(targetPath);
|
return normalizeProjectDirectory(targetPath);
|
||||||
}, [activeProjectId, currentDirectory, projects]);
|
}, [activeProjectId, currentDirectory, projects]);
|
||||||
|
|
||||||
|
const getProjectRefForWorktree = React.useCallback((worktree: WorktreeMetadata) => {
|
||||||
|
const normalized = normalizeProjectDirectory(worktree.projectDirectory);
|
||||||
|
const fallbackPath = normalized || projectDirectory;
|
||||||
|
const match = projects.find((project) => normalizeProjectDirectory(project.path) === fallbackPath) ?? null;
|
||||||
|
return { id: match?.id ?? `path:${fallbackPath}`, path: fallbackPath };
|
||||||
|
}, [projectDirectory, projects]);
|
||||||
|
|
||||||
const hasDirtyWorktrees = React.useMemo(
|
const hasDirtyWorktrees = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
(deleteDialog?.worktree?.status?.isDirty ?? false) ||
|
(deleteDialog?.worktree?.status?.isDirty ?? false) ||
|
||||||
@@ -276,11 +283,19 @@ export const SessionDialogs: React.FC = () => {
|
|||||||
|
|
||||||
if (deleteDialog.sessions.length === 0 && isWorktreeDelete && deleteDialog.worktree) {
|
if (deleteDialog.sessions.length === 0 && isWorktreeDelete && deleteDialog.worktree) {
|
||||||
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
||||||
await removeProjectWorktree(
|
try {
|
||||||
{ id: activeProjectId || `path:${projectDirectory}`, path: projectDirectory },
|
await removeProjectWorktree(
|
||||||
deleteDialog.worktree,
|
getProjectRefForWorktree(deleteDialog.worktree),
|
||||||
{ deleteRemoteBranch: shouldRemoveRemote, force: true }
|
deleteDialog.worktree,
|
||||||
);
|
{ deleteRemoteBranch: shouldRemoveRemote, force: true }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to remove worktree', {
|
||||||
|
description: renderToastDescription(error instanceof Error ? error.message : 'Please try again.'),
|
||||||
|
});
|
||||||
|
closeDeleteDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const archiveNote = shouldRemoveRemote ? 'Worktree and remote branch removed.' : 'Worktree removed.';
|
const archiveNote = shouldRemoveRemote ? 'Worktree and remote branch removed.' : 'Worktree removed.';
|
||||||
toast.success('Worktree removed', {
|
toast.success('Worktree removed', {
|
||||||
description: renderToastDescription(archiveNote),
|
description: renderToastDescription(archiveNote),
|
||||||
@@ -293,7 +308,9 @@ export const SessionDialogs: React.FC = () => {
|
|||||||
if (deleteDialog.sessions.length === 1) {
|
if (deleteDialog.sessions.length === 1) {
|
||||||
const target = deleteDialog.sessions[0];
|
const target = deleteDialog.sessions[0];
|
||||||
const success = await deleteSession(target.id, {
|
const success = await deleteSession(target.id, {
|
||||||
archiveWorktree: shouldArchive,
|
// In "worktree" mode, remove the selected worktree explicitly below.
|
||||||
|
// Don't try to derive worktree removal from per-session metadata (may be missing).
|
||||||
|
archiveWorktree: isWorktreeDelete ? false : shouldArchive,
|
||||||
deleteRemoteBranch: removeRemoteBranch,
|
deleteRemoteBranch: removeRemoteBranch,
|
||||||
});
|
});
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@@ -301,7 +318,7 @@ export const SessionDialogs: React.FC = () => {
|
|||||||
setIsProcessingDelete(false);
|
setIsProcessingDelete(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const archiveNote = shouldArchive
|
const archiveNote = !isWorktreeDelete && shouldArchive
|
||||||
? removeRemoteBranch
|
? removeRemoteBranch
|
||||||
? 'Worktree and remote branch removed.'
|
? 'Worktree and remote branch removed.'
|
||||||
: 'Attached worktree archived.'
|
: 'Attached worktree archived.'
|
||||||
@@ -316,12 +333,30 @@ export const SessionDialogs: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
const ids = deleteDialog.sessions.map((session) => session.id);
|
const ids = deleteDialog.sessions.map((session) => session.id);
|
||||||
const { deletedIds, failedIds } = await deleteSessions(ids, {
|
const { deletedIds, failedIds } = await deleteSessions(ids, {
|
||||||
archiveWorktree: shouldArchive,
|
archiveWorktree: isWorktreeDelete ? false : shouldArchive,
|
||||||
deleteRemoteBranch: removeRemoteBranch,
|
deleteRemoteBranch: removeRemoteBranch,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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.
|
||||||
|
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
||||||
|
try {
|
||||||
|
await removeProjectWorktree(
|
||||||
|
getProjectRefForWorktree(deleteDialog.worktree),
|
||||||
|
deleteDialog.worktree,
|
||||||
|
{ deleteRemoteBranch: shouldRemoveRemote, force: true }
|
||||||
|
);
|
||||||
|
await loadSessions();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to remove worktree', {
|
||||||
|
description: renderToastDescription(error instanceof Error ? error.message : 'Please try again.'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (deletedIds.length > 0) {
|
if (deletedIds.length > 0) {
|
||||||
const archiveNote = shouldArchive
|
const archiveNote = !isWorktreeDelete && shouldArchive
|
||||||
? removeRemoteBranch
|
? removeRemoteBranch
|
||||||
? 'Archived worktrees and removed remote branches.'
|
? 'Archived worktrees and removed remote branches.'
|
||||||
: 'Attached worktrees archived.'
|
: 'Attached worktrees archived.'
|
||||||
@@ -353,6 +388,22 @@ export const SessionDialogs: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isWorktreeDelete && deleteDialog.sessions.length === 1 && deleteDialog.worktree) {
|
||||||
|
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
||||||
|
try {
|
||||||
|
await removeProjectWorktree(
|
||||||
|
getProjectRefForWorktree(deleteDialog.worktree),
|
||||||
|
deleteDialog.worktree,
|
||||||
|
{ deleteRemoteBranch: shouldRemoveRemote, force: true }
|
||||||
|
);
|
||||||
|
await loadSessions();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to remove worktree', {
|
||||||
|
description: renderToastDescription(error instanceof Error ? error.message : 'Please try again.'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
closeDeleteDialog();
|
closeDeleteDialog();
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessingDelete(false);
|
setIsProcessingDelete(false);
|
||||||
@@ -366,8 +417,7 @@ export const SessionDialogs: React.FC = () => {
|
|||||||
shouldArchiveWorktree,
|
shouldArchiveWorktree,
|
||||||
isWorktreeDelete,
|
isWorktreeDelete,
|
||||||
canRemoveRemoteBranches,
|
canRemoveRemoteBranches,
|
||||||
projectDirectory,
|
getProjectRefForWorktree,
|
||||||
activeProjectId,
|
|
||||||
loadSessions,
|
loadSessions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ const getWorktreeMethod = (client: unknown, key: string): WorktreeRemovalMethod
|
|||||||
if (typeof candidate !== 'function') {
|
if (typeof candidate !== 'function') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return candidate as WorktreeRemovalMethod;
|
// Keep method binding; SDK methods use `this.client`.
|
||||||
|
return (params?: WorktreeRemovalParams) => (candidate as (this: unknown, p?: WorktreeRemovalParams) => Promise<unknown>).call(client, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildSdkStartCommand = (args: {
|
export const buildSdkStartCommand = (args: {
|
||||||
@@ -245,23 +246,44 @@ export async function removeProjectWorktree(project: ProjectRef, worktree: Workt
|
|||||||
if (worktree.source === 'sdk') {
|
if (worktree.source === 'sdk') {
|
||||||
const scoped = opencodeClient.getScopedApiClient(projectDirectory);
|
const scoped = opencodeClient.getScopedApiClient(projectDirectory);
|
||||||
const worktreeClient = scoped.worktree as unknown;
|
const worktreeClient = scoped.worktree as unknown;
|
||||||
|
const force = Boolean(options?.force ?? true);
|
||||||
|
|
||||||
|
const fallbackRemoveViaGit = async () => {
|
||||||
|
await removeGitWorktree(projectDirectory, { path: worktree.path, force });
|
||||||
|
};
|
||||||
|
|
||||||
const removeMethod = getWorktreeMethod(worktreeClient, 'remove');
|
const removeMethod = getWorktreeMethod(worktreeClient, 'remove');
|
||||||
if (removeMethod) {
|
if (removeMethod) {
|
||||||
await removeMethod({ worktreeRemoveInput: { directory: worktree.path } });
|
const raw = await removeMethod({ worktreeRemoveInput: { directory: worktree.path } });
|
||||||
|
const ok = unwrapSdkData(raw);
|
||||||
|
if (ok !== true) {
|
||||||
|
await fallbackRemoveViaGit();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const deleteMethod = getWorktreeMethod(worktreeClient, 'delete');
|
const deleteMethod = getWorktreeMethod(worktreeClient, 'delete');
|
||||||
if (deleteMethod) {
|
if (deleteMethod) {
|
||||||
await deleteMethod({ worktreeDeleteInput: { directory: worktree.path } });
|
const raw = await deleteMethod({ worktreeDeleteInput: { directory: worktree.path } });
|
||||||
|
const ok = unwrapSdkData(raw);
|
||||||
|
if (ok !== true) {
|
||||||
|
await fallbackRemoveViaGit();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const archiveMethod = getWorktreeMethod(worktreeClient, 'archive');
|
const archiveMethod = getWorktreeMethod(worktreeClient, 'archive');
|
||||||
if (archiveMethod) {
|
if (archiveMethod) {
|
||||||
await archiveMethod({ worktreeArchiveInput: { directory: worktree.path } });
|
const raw = await archiveMethod({ worktreeArchiveInput: { directory: worktree.path } });
|
||||||
|
const ok = unwrapSdkData(raw);
|
||||||
|
if (ok !== true) {
|
||||||
|
await fallbackRemoveViaGit();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Worktree removal is not supported by this SDK version.');
|
throw new Error('Worktree removal is not supported by this SDK version.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some OpenCode builds only update internal state; remove git worktree best-effort.
|
||||||
|
await fallbackRemoveViaGit().catch(() => undefined);
|
||||||
|
|
||||||
// Best-effort branch cleanup. Some OpenCode builds may keep the branch.
|
// Best-effort branch cleanup. Some OpenCode builds may keep the branch.
|
||||||
const branchName = (worktree.branch || '').replace(/^refs\/heads\//, '').trim();
|
const branchName = (worktree.branch || '').replace(/^refs\/heads\//, '').trim();
|
||||||
if (deleteLocalBranch && branchName) {
|
if (deleteLocalBranch && branchName) {
|
||||||
|
|||||||
@@ -157,6 +157,16 @@ const archiveSessionWorktree = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteSessionOnServer = async (sessionId: string, directory?: string | null): Promise<boolean> => {
|
||||||
|
const apiClient = opencodeClient.getApiClient();
|
||||||
|
const normalizedDirectory = normalizePath(directory ?? null);
|
||||||
|
const response = await apiClient.session.delete({
|
||||||
|
sessionID: sessionId,
|
||||||
|
...(normalizedDirectory ? { directory: normalizedDirectory } : {}),
|
||||||
|
});
|
||||||
|
return Boolean(response.data);
|
||||||
|
};
|
||||||
|
|
||||||
const normalizePath = (value?: string | null): string | null => {
|
const normalizePath = (value?: string | null): string | null => {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
return null;
|
return null;
|
||||||
@@ -913,54 +923,55 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
deleteSession: async (id: string, options) => {
|
deleteSession: async (id: string, options) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
const metadata = get().worktreeMetadata.get(id);
|
const metadata = get().worktreeMetadata.get(id);
|
||||||
|
const metadataPath = typeof metadata?.path === 'string' ? metadata.path : null;
|
||||||
|
const metadataProjectDirectory = typeof metadata?.projectDirectory === 'string' ? metadata.projectDirectory : null;
|
||||||
const sessionDirectory = getSessionDirectory(get().sessions, id);
|
const sessionDirectory = getSessionDirectory(get().sessions, id);
|
||||||
const overrideDirectory = metadata?.path ?? sessionDirectory;
|
const requestDirectory = normalizePath(metadataProjectDirectory)
|
||||||
let archivedMetadata: WorktreeMetadata | null = null;
|
?? normalizePath(sessionDirectory)
|
||||||
try {
|
?? normalizePath(opencodeClient.getDirectory() ?? null)
|
||||||
if (metadata && options?.archiveWorktree) {
|
?? null;
|
||||||
await archiveSessionWorktree(metadata, {
|
|
||||||
deleteRemoteBranch: options?.deleteRemoteBranch,
|
|
||||||
remoteName: options?.remoteName,
|
|
||||||
});
|
|
||||||
archivedMetadata = metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRequest = () => opencodeClient.deleteSession(id);
|
let archiveSucceeded = false;
|
||||||
const success = overrideDirectory
|
try {
|
||||||
? await opencodeClient.withDirectory(overrideDirectory, deleteRequest)
|
const success = await deleteSessionOnServer(id, requestDirectory);
|
||||||
: await deleteRequest();
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
set((state) => {
|
set({
|
||||||
const update: Partial<SessionStore> = {
|
isLoading: false,
|
||||||
isLoading: false,
|
error: "Failed to delete session",
|
||||||
error: "Failed to delete session",
|
|
||||||
};
|
|
||||||
if (archivedMetadata) {
|
|
||||||
const nextMetadata = new Map(state.worktreeMetadata);
|
|
||||||
nextMetadata.delete(id);
|
|
||||||
update.worktreeMetadata = nextMetadata;
|
|
||||||
}
|
|
||||||
return update;
|
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata && options?.archiveWorktree) {
|
||||||
|
try {
|
||||||
|
await archiveSessionWorktree(metadata, {
|
||||||
|
deleteRemoteBranch: options?.deleteRemoteBranch,
|
||||||
|
remoteName: options?.remoteName,
|
||||||
|
});
|
||||||
|
archiveSucceeded = true;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Failed to delete worktree";
|
||||||
|
set({ error: message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let nextCurrentId: string | null = null;
|
let nextCurrentId: string | null = null;
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const filteredSessions = state.sessions.filter((s) => s.id !== id);
|
const filteredSessions = state.sessions.filter((s) => s.id !== id);
|
||||||
nextCurrentId = state.currentSessionId === id ? null : state.currentSessionId;
|
nextCurrentId = state.currentSessionId === id ? null : state.currentSessionId;
|
||||||
const nextMetadata = new Map(state.worktreeMetadata);
|
const nextMetadata = new Map(state.worktreeMetadata);
|
||||||
nextMetadata.delete(id);
|
nextMetadata.delete(id);
|
||||||
const nextAvailableWorktrees = options?.archiveWorktree && metadata
|
const shouldRemoveWorktreeFromLists = Boolean(metadataPath && options?.archiveWorktree && archiveSucceeded);
|
||||||
? state.availableWorktrees.filter((entry) => normalizePath(entry.path) !== normalizePath(metadata.path))
|
const nextAvailableWorktrees = shouldRemoveWorktreeFromLists
|
||||||
|
? state.availableWorktrees.filter((entry) => normalizePath(entry.path) !== normalizePath(metadataPath))
|
||||||
: state.availableWorktrees;
|
: state.availableWorktrees;
|
||||||
const nextAvailableWorktreesByProject = new Map(state.availableWorktreesByProject);
|
const nextAvailableWorktreesByProject = new Map(state.availableWorktreesByProject);
|
||||||
if (options?.archiveWorktree && metadata) {
|
if (shouldRemoveWorktreeFromLists && metadataProjectDirectory) {
|
||||||
const projectKey = normalizePath(metadata.projectDirectory) ?? metadata.projectDirectory;
|
const projectKey = normalizePath(metadataProjectDirectory) ?? metadataProjectDirectory;
|
||||||
const projectWorktrees = nextAvailableWorktreesByProject.get(projectKey) ?? [];
|
const projectWorktrees = nextAvailableWorktreesByProject.get(projectKey) ?? [];
|
||||||
nextAvailableWorktreesByProject.set(
|
nextAvailableWorktreesByProject.set(
|
||||||
projectKey,
|
projectKey,
|
||||||
projectWorktrees.filter((entry) => normalizePath(entry.path) !== normalizePath(metadata.path))
|
projectWorktrees.filter((entry) => normalizePath(entry.path) !== normalizePath(metadataPath))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -974,28 +985,18 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const directoryToStore = overrideDirectory ?? opencodeClient.getDirectory() ?? null;
|
const directoryToStore = normalizePath(sessionDirectory)
|
||||||
|
?? normalizePath(opencodeClient.getDirectory() ?? null)
|
||||||
|
?? null;
|
||||||
storeSessionForDirectory(directoryToStore, nextCurrentId);
|
storeSessionForDirectory(directoryToStore, nextCurrentId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to delete session";
|
const message = error instanceof Error ? error.message : "Failed to delete session";
|
||||||
if (archivedMetadata) {
|
set({
|
||||||
set((state) => {
|
error: message,
|
||||||
const nextMetadata = new Map(state.worktreeMetadata);
|
isLoading: false,
|
||||||
nextMetadata.delete(id);
|
});
|
||||||
return {
|
|
||||||
worktreeMetadata: nextMetadata,
|
|
||||||
error: message,
|
|
||||||
isLoading: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
set({
|
|
||||||
error: message,
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1015,35 +1016,29 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
}
|
}
|
||||||
const deletedIds: string[] = [];
|
const deletedIds: string[] = [];
|
||||||
const failedIds: string[] = [];
|
const failedIds: string[] = [];
|
||||||
const archivedIds = new Set<string>();
|
const worktreesToArchive = new Map<string, WorktreeMetadata>();
|
||||||
|
|
||||||
const removedWorktrees: Array<{ path: string; projectDirectory: string }> = [];
|
|
||||||
const archivedWorktreePaths = new Set<string>();
|
const archivedWorktreePaths = new Set<string>();
|
||||||
|
|
||||||
for (const id of uniqueIds) {
|
for (const id of uniqueIds) {
|
||||||
try {
|
try {
|
||||||
const metadata = get().worktreeMetadata.get(id);
|
const metadata = get().worktreeMetadata.get(id);
|
||||||
const sessionDirectory = getSessionDirectory(get().sessions, id);
|
const sessionDirectory = getSessionDirectory(get().sessions, id);
|
||||||
const overrideDirectory = metadata?.path ?? sessionDirectory;
|
const requestDirectory = normalizePath(metadata?.projectDirectory ?? null)
|
||||||
if (metadata && options?.archiveWorktree && !archivedWorktreePaths.has(metadata.path)) {
|
?? normalizePath(sessionDirectory)
|
||||||
await archiveSessionWorktree(metadata, {
|
?? normalizePath(opencodeClient.getDirectory() ?? null)
|
||||||
deleteRemoteBranch: options?.deleteRemoteBranch,
|
?? null;
|
||||||
remoteName: options?.remoteName,
|
|
||||||
});
|
if (metadata && options?.archiveWorktree) {
|
||||||
archivedIds.add(id);
|
const key = normalizePath(metadata.path) ?? metadata.path;
|
||||||
removedWorktrees.push({ path: metadata.path, projectDirectory: metadata.projectDirectory });
|
if (!archivedWorktreePaths.has(key)) {
|
||||||
archivedWorktreePaths.add(metadata.path);
|
archivedWorktreePaths.add(key);
|
||||||
|
worktreesToArchive.set(key, metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRequest = () => opencodeClient.deleteSession(id);
|
const success = await deleteSessionOnServer(id, requestDirectory);
|
||||||
const success = overrideDirectory
|
|
||||||
? await opencodeClient.withDirectory(overrideDirectory, deleteRequest)
|
|
||||||
: await deleteRequest();
|
|
||||||
if (success) {
|
if (success) {
|
||||||
deletedIds.push(id);
|
deletedIds.push(id);
|
||||||
if (metadata?.path && !removedWorktrees.some((entry) => entry.path === metadata.path)) {
|
|
||||||
removedWorktrees.push({ path: metadata.path, projectDirectory: metadata.projectDirectory });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
failedIds.push(id);
|
failedIds.push(id);
|
||||||
}
|
}
|
||||||
@@ -1052,8 +1047,30 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const archivedWorktrees: Array<{ path: string; projectDirectory: string }> = [];
|
||||||
|
const archiveFailures: string[] = [];
|
||||||
|
|
||||||
|
if (options?.archiveWorktree && worktreesToArchive.size > 0) {
|
||||||
|
for (const metadata of worktreesToArchive.values()) {
|
||||||
|
try {
|
||||||
|
await archiveSessionWorktree(metadata, {
|
||||||
|
deleteRemoteBranch: options?.deleteRemoteBranch,
|
||||||
|
remoteName: options?.remoteName,
|
||||||
|
});
|
||||||
|
archivedWorktrees.push({ path: metadata.path, projectDirectory: metadata.projectDirectory });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Failed to delete worktree";
|
||||||
|
archiveFailures.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (archiveFailures.length > 0) {
|
||||||
|
set({ error: archiveFailures[0] });
|
||||||
|
}
|
||||||
|
|
||||||
const directoryStore = useDirectoryStore.getState();
|
const directoryStore = useDirectoryStore.getState();
|
||||||
removedWorktrees.forEach(({ path, projectDirectory }) => {
|
archivedWorktrees.forEach(({ path, projectDirectory }) => {
|
||||||
if (directoryStore.currentDirectory === path) {
|
if (directoryStore.currentDirectory === path) {
|
||||||
directoryStore.setDirectory(projectDirectory, { showOverlay: false });
|
directoryStore.setDirectory(projectDirectory, { showOverlay: false });
|
||||||
}
|
}
|
||||||
@@ -1077,25 +1094,19 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
for (const removedId of deletedSet) {
|
for (const removedId of deletedSet) {
|
||||||
nextMetadata.delete(removedId);
|
nextMetadata.delete(removedId);
|
||||||
}
|
}
|
||||||
for (const archivedId of archivedIds) {
|
|
||||||
nextMetadata.delete(archivedId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const removedPaths = new Set(
|
const removedPaths = new Set(
|
||||||
removedWorktrees
|
archivedWorktrees
|
||||||
.map((entry) => normalizePath(entry.path))
|
.map((entry) => normalizePath(entry.path))
|
||||||
.filter((p): p is string => Boolean(p))
|
.filter((p): p is string => Boolean(p))
|
||||||
);
|
);
|
||||||
const nextAvailableWorktrees =
|
const nextAvailableWorktrees = removedPaths.size > 0
|
||||||
removedPaths.size > 0
|
? state.availableWorktrees.filter((entry) => !removedPaths.has(normalizePath(entry.path) ?? entry.path))
|
||||||
? state.availableWorktrees.filter(
|
: state.availableWorktrees;
|
||||||
(entry) => !removedPaths.has(normalizePath(entry.path) ?? entry.path)
|
|
||||||
)
|
|
||||||
: state.availableWorktrees;
|
|
||||||
|
|
||||||
const nextAvailableWorktreesByProject = new Map(state.availableWorktreesByProject);
|
const nextAvailableWorktreesByProject = new Map(state.availableWorktreesByProject);
|
||||||
if (removedWorktrees.length > 0) {
|
if (archivedWorktrees.length > 0) {
|
||||||
const removedPathsByProject = removedWorktrees.reduce<Map<string, Set<string>>>((accumulator, entry) => {
|
const removedPathsByProject = archivedWorktrees.reduce<Map<string, Set<string>>>((accumulator, entry) => {
|
||||||
const projectKey = normalizePath(entry.projectDirectory) ?? entry.projectDirectory;
|
const projectKey = normalizePath(entry.projectDirectory) ?? entry.projectDirectory;
|
||||||
const pathKey = normalizePath(entry.path) ?? entry.path;
|
const pathKey = normalizePath(entry.path) ?? entry.path;
|
||||||
if (!accumulator.has(projectKey)) {
|
if (!accumulator.has(projectKey)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user