fix(worktrees): protect user changes across ambiguous move failures
Post-merge hardening of the session-to-worktree move (#2998), driven by review findings on the follow-up pass: - an ambiguous transport failure (relay abort, timeout) on the change-carrying move no longer force-deletes the fresh worktree that may hold the user's only copy of their changes; both intent kinds surface honest guidance and refresh both directories - assertSdkSuccess re-tags ambiguous transport errors when wrapping SDK failures, so ambiguity classification survives the wrapper on every path, matching the prompt-send precedent - session liveness checks scan all child stores plus the global status index, and report unknown (not idle) when no store covers the session — an evicted background directory can no longer make a busy session look movable - incomplete-rollback errors carry the changes-may-be-in-destination guidance instead of swallowing it - move-message assembly shared across the three call sites; tests now exercise the real ambiguity classifier (extracted to send-failure-classification.ts) instead of a hand-mirrored mock - i18n fallout from the merge train: Turkish gains the 21 worktree-move keys, all 12 locales get the hedged ambiguous-failure toast; owning DOCUMENTATION.md files record the new contracts
This commit is contained in:
@@ -71,7 +71,7 @@ import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { buildExportFilename, downloadAsMarkdown, formatSessionAsMarkdown, saveAsMarkdownDesktop } from '@/lib/exportSession';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { requestSessionTreeMove, useIsSessionWorktreeMovePending } from '@/lib/worktrees/sessionWorktreeMove';
|
||||
import { buildSessionTreeMoveMessages, requestSessionTreeMove, useIsSessionWorktreeMovePending } from '@/lib/worktrees/sessionWorktreeMove';
|
||||
|
||||
const DESKTOP_HEADER_ICON_BUTTON_CLASS = 'app-region-no-drag inline-flex h-8 w-8 items-center justify-center gap-2 rounded-md typography-ui-label font-medium text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:bg-interactive-hover transition-colors';
|
||||
|
||||
@@ -1064,12 +1064,10 @@ export const Header: React.FC = () => {
|
||||
root,
|
||||
descendants,
|
||||
sourceDirectory: sessionDirectory,
|
||||
messages: {
|
||||
success: t('sessions.sidebar.session.moveToWorktree.success'),
|
||||
failure: t('sessions.sidebar.session.moveToWorktree.failed'),
|
||||
sourceVerificationFailed: t('sessions.sidebar.session.moveToWorktree.sourceVerificationFailed'),
|
||||
applyChangesFailed: t('sessions.sidebar.session.moveToWorktree.applyChangesFailed'),
|
||||
},
|
||||
messages: buildSessionTreeMoveMessages(t, {
|
||||
success: 'sessions.sidebar.session.moveToWorktree.success',
|
||||
failure: 'sessions.sidebar.session.moveToWorktree.failed',
|
||||
}),
|
||||
});
|
||||
}, [currentSessionId, isCurrentSessionActive, isCurrentSessionMovingToWorktree, sessionDirectory, t]);
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ kept at this root in `types.ts` and `utils.tsx`.
|
||||
changes and roll back session-only if a later descendant fails. The root moves
|
||||
last and carries source changes once, which prevents rollback from replaying the
|
||||
transferred patch into the source.
|
||||
- Failure cleanup: a worktree created for the move is removed only after a
|
||||
definite failure. When the change-carrying request fails without confirming
|
||||
its outcome, that worktree is KEPT (it may hold the only copy of the user's
|
||||
changes), both directories are refreshed authoritatively because the session
|
||||
may have moved server-side, and the toast points the user at the destination.
|
||||
Existing destinations are never removed; they get the same guidance.
|
||||
|
||||
`MainLayout` and `VSCodeLayout` call `useSessionListSync({ isVSCode })`
|
||||
unconditionally. The hook publishes complete directory bootstrap demand,
|
||||
|
||||
@@ -33,6 +33,7 @@ const makeMoveMessages = (): SessionTreeMoveMessages => ({
|
||||
failure: 'move failed',
|
||||
sourceVerificationFailed: 'source verification failed',
|
||||
applyChangesFailed: 'apply changes failed',
|
||||
changesMayBeInDestination: 'changes may be in destination',
|
||||
});
|
||||
|
||||
const makeExistingIntent = (): SessionTreeMoveIntent => ({
|
||||
|
||||
@@ -48,6 +48,7 @@ import { MultiRunFusionDialog } from '@/components/multirun/MultiRunFusionDialog
|
||||
import { FusionIcon } from '@/components/icons/FusionIcon';
|
||||
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
||||
import {
|
||||
buildSessionTreeMoveMessages,
|
||||
requestSessionTreeMove,
|
||||
useIsSessionWorktreeMovePending,
|
||||
} from '@/lib/worktrees/sessionWorktreeMove';
|
||||
@@ -1047,12 +1048,10 @@ function SessionNodeItemComponent(props: SessionNodeItemProps): React.ReactNode
|
||||
descendants: collectNodeDescendantSessions(node),
|
||||
sourceDirectory: sessionDirectory,
|
||||
destination: target.metadata,
|
||||
messages: {
|
||||
success: t('sessions.sidebar.session.moveToWorktree.existingSuccess'),
|
||||
failure: t('sessions.sidebar.session.moveToWorktree.existingFailed'),
|
||||
sourceVerificationFailed: t('sessions.sidebar.session.moveToWorktree.sourceVerificationFailed'),
|
||||
applyChangesFailed: t('sessions.sidebar.session.moveToWorktree.applyChangesFailed'),
|
||||
},
|
||||
messages: buildSessionTreeMoveMessages(t, {
|
||||
success: 'sessions.sidebar.session.moveToWorktree.existingSuccess',
|
||||
failure: 'sessions.sidebar.session.moveToWorktree.existingFailed',
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -1086,12 +1085,10 @@ function SessionNodeItemComponent(props: SessionNodeItemProps): React.ReactNode
|
||||
root: resolvedSession,
|
||||
descendants: collectNodeDescendantSessions(node),
|
||||
sourceDirectory: sessionDirectory,
|
||||
messages: {
|
||||
success: t('sessions.sidebar.session.moveToWorktree.success'),
|
||||
failure: t('sessions.sidebar.session.moveToWorktree.failed'),
|
||||
sourceVerificationFailed: t('sessions.sidebar.session.moveToWorktree.sourceVerificationFailed'),
|
||||
applyChangesFailed: t('sessions.sidebar.session.moveToWorktree.applyChangesFailed'),
|
||||
},
|
||||
messages: buildSessionTreeMoveMessages(t, {
|
||||
success: 'sessions.sidebar.session.moveToWorktree.success',
|
||||
failure: 'sessions.sidebar.session.moveToWorktree.failed',
|
||||
}),
|
||||
});
|
||||
}}
|
||||
className="[&>svg]:mr-1"
|
||||
|
||||
Reference in New Issue
Block a user