feat: support fast worktree-backed session flows
Add a directory-created fast path for worktree creation so session and send flows can continue once the target directory exists while Git attachment and bootstrap finish in the background. Track bootstrap status explicitly in shared UI contracts, including pending, ready, and failed states. Background watchers now surface failures and timeouts, update stored worktree metadata, and keep web and VS Code runtime behavior in parity. Move GitHub issue/PR worktree sessions and assistant-answer fork sessions onto the unified send path so provider, model, agent, and variant selections are preserved. The assistant-answer fork dialog can optionally create a worktree outside VS Code. Make worktree deletion dialogs close after linked-session cleanup while removing the worktree in the background, and clean up failed fast-create artifacts safely without recursively deleting user or agent-written files. Validation: bun test packages/ui/src/lib/worktrees/worktreeBootstrap.test.ts packages/ui/src/lib/worktrees/worktreeManager.test.ts; bun run type-check; bun run lint.
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
|
||||
import { AgentSelector } from '@/components/sections/commands/AgentSelector';
|
||||
import { ThinkingPill } from '@/components/session/ThinkingPill';
|
||||
@@ -15,6 +16,7 @@ import { useAgentsStore } from '@/stores/useAgentsStore';
|
||||
import { isPrimaryMode } from '@/components/chat/mobileControlsUtils';
|
||||
import { EXECUTION_FORK_DEFAULT_INSTRUCTIONS } from '@/lib/messages/executionMeta';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
|
||||
export type ForkSessionExecution = {
|
||||
providerID: string;
|
||||
@@ -22,6 +24,7 @@ export type ForkSessionExecution = {
|
||||
variant: string;
|
||||
agent: string;
|
||||
instructions: string;
|
||||
createWorktree?: boolean;
|
||||
};
|
||||
|
||||
type ForkSessionDialogProps = {
|
||||
@@ -50,6 +53,8 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
|
||||
const [variant, setVariant] = React.useState(currentVariant);
|
||||
const [agent, setAgent] = React.useState(currentAgentName);
|
||||
const [instructions, setInstructions] = React.useState(EXECUTION_FORK_DEFAULT_INSTRUCTIONS);
|
||||
const [createWorktree, setCreateWorktree] = React.useState(false);
|
||||
const showCreateWorktree = React.useMemo(() => !isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -69,6 +74,7 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
|
||||
setVariant(config.currentVariant || '');
|
||||
setAgent(config.currentAgentName || '');
|
||||
setInstructions(EXECUTION_FORK_DEFAULT_INSTRUCTIONS);
|
||||
setCreateWorktree(false);
|
||||
}, [open]);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -106,8 +112,15 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
|
||||
|
||||
const handleSubmit = React.useCallback(() => {
|
||||
if (!canConfirm || submitting) return;
|
||||
void onConfirm({ providerID, modelID, variant, agent, instructions });
|
||||
}, [canConfirm, submitting, onConfirm, providerID, modelID, variant, agent, instructions]);
|
||||
void onConfirm({
|
||||
providerID,
|
||||
modelID,
|
||||
variant,
|
||||
agent,
|
||||
instructions,
|
||||
createWorktree: showCreateWorktree && createWorktree,
|
||||
});
|
||||
}, [canConfirm, submitting, onConfirm, providerID, modelID, variant, agent, instructions, showCreateWorktree, createWorktree]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -173,15 +186,35 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)} disabled={submitting}>
|
||||
{t('rightSidebar.contextNotesTodo.sendDialog.actions.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSubmit} disabled={!canConfirm || submitting}>
|
||||
{submitting
|
||||
? t('rightSidebar.contextNotesTodo.sendDialog.actions.sending')
|
||||
: t('rightSidebar.contextNotesTodo.sendDialog.actions.send')}
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 ${showCreateWorktree ? 'justify-between' : 'justify-end'}`}>
|
||||
{showCreateWorktree ? (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Checkbox
|
||||
checked={createWorktree}
|
||||
onChange={setCreateWorktree}
|
||||
disabled={submitting}
|
||||
ariaLabel={t('chat.messageBody.forkDialog.createWorktree')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="truncate typography-ui-label text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={submitting}
|
||||
onClick={() => setCreateWorktree((value) => !value)}
|
||||
>
|
||||
{t('chat.messageBody.forkDialog.createWorktree')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)} disabled={submitting}>
|
||||
{t('rightSidebar.contextNotesTodo.sendDialog.actions.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSubmit} disabled={!canConfirm || submitting}>
|
||||
{submitting
|
||||
? t('rightSidebar.contextNotesTodo.sendDialog.actions.sending')
|
||||
: t('rightSidebar.contextNotesTodo.sendDialog.actions.send')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -19,10 +19,8 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useSelectionStore } from '@/sync/selection-store';
|
||||
import * as sessionActions from '@/sync/session-actions';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useContextStore } from '@/stores/contextStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { renderMagicPrompt } from '@/lib/magicPrompts';
|
||||
import { parseModelIdentifier } from '@/lib/modelIdentifier';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
@@ -248,9 +246,9 @@ export function GitHubIssuePickerDialog({
|
||||
const resolveDefaultVariant = React.useCallback((providerID: string, modelID: string): string | undefined => {
|
||||
const configState = useConfigStore.getState();
|
||||
const settingsDefaultVariant = configState.settingsDefaultVariant;
|
||||
if (!settingsDefaultVariant) {
|
||||
return undefined;
|
||||
}
|
||||
const currentVariant = configState.currentProviderId === providerID && configState.currentModelId === modelID
|
||||
? configState.currentVariant
|
||||
: undefined;
|
||||
|
||||
const provider = configState.providers.find((p) => p.id === providerID);
|
||||
const model = provider?.models.find((m: Record<string, unknown>) => (m as { id?: string }).id === modelID) as
|
||||
@@ -258,12 +256,15 @@ export function GitHubIssuePickerDialog({
|
||||
| undefined;
|
||||
const variants = model?.variants;
|
||||
if (!variants) {
|
||||
return undefined;
|
||||
return settingsDefaultVariant || currentVariant || undefined;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(variants, settingsDefaultVariant)) {
|
||||
return undefined;
|
||||
if (settingsDefaultVariant && Object.prototype.hasOwnProperty.call(variants, settingsDefaultVariant)) {
|
||||
return settingsDefaultVariant;
|
||||
}
|
||||
return settingsDefaultVariant;
|
||||
if (currentVariant && Object.prototype.hasOwnProperty.call(variants, currentVariant)) {
|
||||
return currentVariant;
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const startSession = React.useCallback(async (issueNumber: number, sourceRepo?: GitHubRepoSelector | null) => {
|
||||
@@ -366,12 +367,14 @@ export function GitHubIssuePickerDialog({
|
||||
|
||||
const sessionTitle = `#${issue.number} ${issue.title}`.trim();
|
||||
|
||||
const { sessionId, sessionDirectory } = await (async () => {
|
||||
const { sessionId } = await (async () => {
|
||||
if (createInWorktree) {
|
||||
const preferred = `issue-${issue.number}-${generateBranchSlug()}`;
|
||||
const created = await createWorktreeSessionForNewBranch(
|
||||
projectDirectory,
|
||||
preferred
|
||||
preferred,
|
||||
undefined,
|
||||
{ returnAfterDirectoryCreated: true }
|
||||
);
|
||||
if (!created?.id) {
|
||||
throw new Error('Failed to create worktree session');
|
||||
@@ -412,64 +415,27 @@ export function GitHubIssuePickerDialog({
|
||||
|
||||
const variant = resolveDefaultVariant(providerID, modelID);
|
||||
|
||||
try {
|
||||
useContextStore.getState().saveSessionModelSelection(sessionId, providerID, modelID);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (agentName) {
|
||||
try {
|
||||
configState.setAgent(agentName);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
useContextStore.getState().saveSessionAgentSelection(sessionId, agentName);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
useContextStore.getState().saveAgentModelForSession(sessionId, agentName, providerID, modelID);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (variant !== undefined) {
|
||||
try {
|
||||
configState.setCurrentVariant(variant);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
useContextStore.getState().saveAgentModelVariantForSession(sessionId, agentName, providerID, modelID, variant);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const visiblePromptText = await renderMagicPrompt('github.issue.review.visible', {
|
||||
issue_number: String(issue.number),
|
||||
});
|
||||
const instructionsText = await renderMagicPrompt('github.issue.review.instructions');
|
||||
const contextText = buildIssueContextText({ repo: issueRes.repo, issue, comments });
|
||||
|
||||
void opencodeClient.sendMessage({
|
||||
id: sessionId,
|
||||
void useSessionUIStore.getState().sendMessage(
|
||||
visiblePromptText,
|
||||
providerID,
|
||||
modelID,
|
||||
agent: agentName,
|
||||
variant,
|
||||
text: visiblePromptText,
|
||||
additionalParts: [
|
||||
agentName,
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
{ text: instructionsText, synthetic: true },
|
||||
{ text: contextText, synthetic: true },
|
||||
],
|
||||
directory: sessionDirectory,
|
||||
}).catch((e) => {
|
||||
variant,
|
||||
undefined,
|
||||
{ sessionId },
|
||||
).catch((e) => {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
toast.error(t('session.githubIssuePicker.toast.sendContextFailed'), {
|
||||
description: message,
|
||||
|
||||
@@ -31,13 +31,11 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useSelectionStore } from '@/sync/selection-store';
|
||||
import * as sessionActions from '@/sync/session-actions';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useContextStore } from '@/stores/contextStore';
|
||||
import { validateWorktreeCreate, createWorktree } from '@/lib/worktrees/worktreeManager';
|
||||
import { withWorktreeUpstreamDefaults } from '@/lib/worktrees/worktreeCreate';
|
||||
import { getWorktreeSetupCommands } from '@/lib/openchamberConfig';
|
||||
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
|
||||
import { generateBranchSlug } from '@/lib/git/branchNameGenerator';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { renderMagicPrompt } from '@/lib/magicPrompts';
|
||||
import { parseModelIdentifier } from '@/lib/modelIdentifier';
|
||||
import { rankBranchesForQuery } from '@/lib/worktrees/branchSearch';
|
||||
@@ -446,66 +444,19 @@ export function NewWorktreeDialog({
|
||||
const resolveDefaultVariant = React.useCallback((providerID: string, modelID: string): string | undefined => {
|
||||
const configState = useConfigStore.getState();
|
||||
const settingsDefaultVariant = configState.settingsDefaultVariant;
|
||||
if (!settingsDefaultVariant) return undefined;
|
||||
const currentVariant = configState.currentProviderId === providerID && configState.currentModelId === modelID
|
||||
? configState.currentVariant
|
||||
: undefined;
|
||||
|
||||
const provider = configState.providers.find((p) => p.id === providerID);
|
||||
const model = provider?.models.find((m: Record<string, unknown>) => (m as { id?: string }).id === modelID) as
|
||||
| { variants?: Record<string, unknown> }
|
||||
| undefined;
|
||||
const variants = model?.variants;
|
||||
if (!variants) return undefined;
|
||||
if (!Object.prototype.hasOwnProperty.call(variants, settingsDefaultVariant)) return undefined;
|
||||
return settingsDefaultVariant;
|
||||
}, []);
|
||||
|
||||
const applySessionModelAndAgentDefaults = React.useCallback((args: {
|
||||
sessionId: string;
|
||||
providerID: string;
|
||||
modelID: string;
|
||||
agentName?: string;
|
||||
variant?: string;
|
||||
}) => {
|
||||
const configState = useConfigStore.getState();
|
||||
|
||||
try {
|
||||
useContextStore.getState().saveSessionModelSelection(args.sessionId, args.providerID, args.modelID);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (!args.agentName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
configState.setAgent(args.agentName);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
useContextStore.getState().saveSessionAgentSelection(args.sessionId, args.agentName);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
useContextStore.getState().saveAgentModelForSession(args.sessionId, args.agentName, args.providerID, args.modelID);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (args.variant !== undefined) {
|
||||
try {
|
||||
configState.setCurrentVariant(args.variant);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
useContextStore
|
||||
.getState()
|
||||
.saveAgentModelVariantForSession(args.sessionId, args.agentName, args.providerID, args.modelID, args.variant);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (!variants) return settingsDefaultVariant || currentVariant || undefined;
|
||||
if (settingsDefaultVariant && Object.prototype.hasOwnProperty.call(variants, settingsDefaultVariant)) return settingsDefaultVariant;
|
||||
if (currentVariant && Object.prototype.hasOwnProperty.call(variants, currentVariant)) return currentVariant;
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const sendLinkedContextMessage = React.useCallback(async (args: {
|
||||
@@ -533,14 +484,6 @@ export function NewWorktreeDialog({
|
||||
|
||||
const variant = resolveDefaultVariant(providerID, modelID);
|
||||
|
||||
applySessionModelAndAgentDefaults({
|
||||
sessionId: args.sessionId,
|
||||
providerID,
|
||||
modelID,
|
||||
agentName,
|
||||
variant,
|
||||
});
|
||||
|
||||
if (args.issue) {
|
||||
if (!github.issueGet || !github.issueComments) {
|
||||
return;
|
||||
@@ -566,19 +509,21 @@ export function NewWorktreeDialog({
|
||||
comments: commentsRes.comments ?? [],
|
||||
});
|
||||
|
||||
await opencodeClient.sendMessage({
|
||||
id: args.sessionId,
|
||||
await useSessionUIStore.getState().sendMessage(
|
||||
visiblePromptText,
|
||||
providerID,
|
||||
modelID,
|
||||
agent: agentName,
|
||||
variant,
|
||||
text: visiblePromptText,
|
||||
additionalParts: [
|
||||
agentName,
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
{ text: instructionsText, synthetic: true },
|
||||
{ text: contextText, synthetic: true },
|
||||
],
|
||||
directory: args.directory,
|
||||
});
|
||||
variant,
|
||||
undefined,
|
||||
{ sessionId: args.sessionId },
|
||||
);
|
||||
|
||||
toast.success(t('session.newWorktree.toast.sessionFromIssue'));
|
||||
return;
|
||||
@@ -603,24 +548,25 @@ export function NewWorktreeDialog({
|
||||
const instructionsText = await renderMagicPrompt('github.pr.review.instructions');
|
||||
const contextText = buildPullRequestContextText(prContext);
|
||||
|
||||
await opencodeClient.sendMessage({
|
||||
id: args.sessionId,
|
||||
await useSessionUIStore.getState().sendMessage(
|
||||
visiblePromptText,
|
||||
providerID,
|
||||
modelID,
|
||||
agent: agentName,
|
||||
variant,
|
||||
text: visiblePromptText,
|
||||
additionalParts: [
|
||||
agentName,
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
{ text: instructionsText, synthetic: true },
|
||||
{ text: contextText, synthetic: true },
|
||||
],
|
||||
directory: args.directory,
|
||||
});
|
||||
variant,
|
||||
undefined,
|
||||
{ sessionId: args.sessionId },
|
||||
);
|
||||
|
||||
toast.success(t('session.newWorktree.toast.sessionFromPr'));
|
||||
}
|
||||
}, [
|
||||
applySessionModelAndAgentDefaults,
|
||||
github,
|
||||
projectDirectory,
|
||||
resolveDefaultAgentName,
|
||||
@@ -854,8 +800,13 @@ export function NewWorktreeDialog({
|
||||
setIsCreating(true);
|
||||
|
||||
try {
|
||||
const setupCommands = await getWorktreeSetupCommands(projectRef);
|
||||
const linkedPr = mode === 'new-branch' ? newBranchState.linkedPr : null;
|
||||
const linkedIssue = mode === 'new-branch' ? newBranchState.linkedIssue : null;
|
||||
const linkedPrState = mode === 'new-branch' ? newBranchState.linkedPr : null;
|
||||
const includePrDiff = mode === 'new-branch' ? newBranchState.includePrDiff : false;
|
||||
const shouldCreateSession = Boolean(linkedIssue || linkedPrState);
|
||||
|
||||
const setupCommands = await getWorktreeSetupCommands(projectRef);
|
||||
const sourceBranch = newBranchState.sourceBranch;
|
||||
|
||||
let sourceLabel = '';
|
||||
@@ -873,6 +824,7 @@ export function NewWorktreeDialog({
|
||||
setUpstream: prConfig.setUpstream,
|
||||
upstreamRemote: prConfig.upstreamRemote,
|
||||
upstreamBranch: prConfig.upstreamBranch,
|
||||
returnAfterDirectoryCreated: true,
|
||||
...(prConfig.ensureRemoteName ? { ensureRemoteName: prConfig.ensureRemoteName } : {}),
|
||||
...(prConfig.ensureRemoteUrl ? { ensureRemoteUrl: prConfig.ensureRemoteUrl } : {}),
|
||||
};
|
||||
@@ -886,20 +838,18 @@ export function NewWorktreeDialog({
|
||||
worktreeName: normalizedWorktree,
|
||||
existingBranch: mode === 'existing-branch' ? normalizedBranch : undefined,
|
||||
setupCommands,
|
||||
returnAfterDirectoryCreated: true,
|
||||
...(sourceBranch && mode === 'new-branch' ? { startRef: sourceBranch } : {}),
|
||||
};
|
||||
})();
|
||||
|
||||
const resolvedArgs = await withWorktreeUpstreamDefaults(projectDirectory, args);
|
||||
const metadata = await createWorktree(projectRef, resolvedArgs);
|
||||
|
||||
const linkedIssue = mode === 'new-branch' ? newBranchState.linkedIssue : null;
|
||||
const linkedPrState = mode === 'new-branch' ? newBranchState.linkedPr : null;
|
||||
const includePrDiff = mode === 'new-branch' ? newBranchState.includePrDiff : false;
|
||||
const metadata = await createWorktree(projectRef, resolvedArgs);
|
||||
|
||||
let createdSessionId: string | null = null;
|
||||
|
||||
if (linkedIssue || linkedPrState) {
|
||||
if (shouldCreateSession) {
|
||||
const sessionTitle = linkedIssue
|
||||
? `#${linkedIssue.number} ${linkedIssue.title}`.trim()
|
||||
: linkedPrState
|
||||
@@ -912,6 +862,10 @@ export function NewWorktreeDialog({
|
||||
}
|
||||
|
||||
createdSessionId = session.id;
|
||||
onWorktreeCreated?.(metadata.path, { sessionId: createdSessionId });
|
||||
onOpenChange(false);
|
||||
setIsCreating(false);
|
||||
|
||||
void sessionActions.updateSessionTitle(session.id, sessionTitle).catch(() => undefined);
|
||||
|
||||
try {
|
||||
@@ -919,6 +873,9 @@ export function NewWorktreeDialog({
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
onOpenChange(false);
|
||||
setIsCreating(false);
|
||||
}
|
||||
|
||||
// Save source branch preference (only if not from PR)
|
||||
@@ -932,10 +889,7 @@ export function NewWorktreeDialog({
|
||||
}),
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
|
||||
if (createdSessionId) {
|
||||
onWorktreeCreated?.(metadata.path, { sessionId: createdSessionId });
|
||||
void sendLinkedContextMessage({
|
||||
sessionId: createdSessionId,
|
||||
directory: metadata.path,
|
||||
|
||||
@@ -373,6 +373,25 @@ export const SessionDialogs: React.FC = () => {
|
||||
}
|
||||
}, [canRemoveRemoteBranches, currentDirectory, deleteDialogShouldRemoveRemote, getProjectRefForWorktree, newSessionDraft?.directoryOverride, newSessionDraft?.open, setDraftBootstrapPendingDirectory, setNewSessionDraftTarget, t]);
|
||||
|
||||
const removeSelectedWorktreeInBackground = React.useCallback((
|
||||
worktree: WorktreeMetadata,
|
||||
deleteLocalBranch: boolean
|
||||
): void => {
|
||||
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
||||
void (async () => {
|
||||
const removed = await removeSelectedWorktree(worktree, deleteLocalBranch);
|
||||
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'), {
|
||||
description: renderToastDescription(archiveNote),
|
||||
});
|
||||
})();
|
||||
}, [canRemoveRemoteBranches, deleteDialogShouldRemoveRemote, removeSelectedWorktree, t]);
|
||||
|
||||
const handleConfirmDelete = React.useCallback(async () => {
|
||||
if (!deleteDialog) {
|
||||
return;
|
||||
@@ -385,18 +404,7 @@ export const SessionDialogs: React.FC = () => {
|
||||
const deleteLocalBranch = shouldArchive && deleteDialogShouldDeleteLocalBranch;
|
||||
|
||||
if (deleteDialog.sessions.length === 0 && isWorktreeDelete && deleteDialog.worktree) {
|
||||
const removed = await removeSelectedWorktree(deleteDialog.worktree, deleteLocalBranch);
|
||||
if (!removed) {
|
||||
closeDeleteDialog();
|
||||
return;
|
||||
}
|
||||
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
||||
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),
|
||||
});
|
||||
removeSelectedWorktreeInBackground(deleteDialog.worktree, deleteLocalBranch);
|
||||
closeDeleteDialog();
|
||||
return;
|
||||
}
|
||||
@@ -450,7 +458,7 @@ export const SessionDialogs: React.FC = () => {
|
||||
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.
|
||||
await removeSelectedWorktree(deleteDialog.worktree, deleteLocalBranch);
|
||||
removeSelectedWorktreeInBackground(deleteDialog.worktree, deleteLocalBranch);
|
||||
// sync handles session refresh automatically
|
||||
}
|
||||
|
||||
@@ -506,7 +514,7 @@ export const SessionDialogs: React.FC = () => {
|
||||
}
|
||||
|
||||
if (isWorktreeDelete && deleteDialog.sessions.length === 1 && deleteDialog.worktree) {
|
||||
await removeSelectedWorktree(deleteDialog.worktree, deleteLocalBranch);
|
||||
removeSelectedWorktreeInBackground(deleteDialog.worktree, deleteLocalBranch);
|
||||
// sync bootstrap refreshes sessions automatically
|
||||
}
|
||||
|
||||
@@ -525,8 +533,7 @@ export const SessionDialogs: React.FC = () => {
|
||||
closeDeleteDialog,
|
||||
shouldArchiveWorktree,
|
||||
isWorktreeDelete,
|
||||
canRemoveRemoteBranches,
|
||||
removeSelectedWorktree,
|
||||
removeSelectedWorktreeInBackground,
|
||||
t,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1719,7 +1719,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
setCurrentSession(options.sessionId, worktreePath);
|
||||
return;
|
||||
}
|
||||
openNewSessionDraft({ directoryOverride: worktreePath });
|
||||
openNewSessionDraft({ directoryOverride: worktreePath, preserveDirectoryOverride: true });
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user