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:
Bohdan Triapitsyn
2026-08-28 12:03:55 +03:00
parent 9d279137ce
commit 4f53db17e6
21 changed files with 493 additions and 107 deletions
@@ -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"