From aa071556bf34e570b2be98547841eb92a7beb54b Mon Sep 17 00:00:00 2001 From: jwcrystal <121911854+jwcrystal@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:33:49 +0800 Subject: [PATCH] fix(worktree): fix worktree detection and state reset when switching (#779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(worktree): reset IntegrateCommitsSection state when switching worktrees Three fixes for the re-integrate commits panel getting stuck: 1. Add `key={worktreeMetadata.path}` to IntegrateCommitsSection so React fully remounts it when switching to a different worktree, clearing any stale `ui` state (conflict, loading, ready) from the previous session. 2. Add `cancelled` flag to the conflict-restore effect so that an async callback started for session A cannot overwrite session B's state after the user switches sessions. Without this guard the stale callback could restore the old session's conflict state on top of the new session's computed-ready state. 3. Fix off-by-one in continueIntegrate: `moved` was returning `remaining.length` (N-1, after shifting currentCommit out) instead of `state.remainingCommits.length` (N), undercounting the commit that was moved by `cherry-pick --continue`. * fix(worktree): add git-based fallback detection when store metadata is missing Root cause: the existing worktreeMetadata resolution relies entirely on cached store state (worktreeMap + availableWorktrees). When the store lookup fails—due to hydrateSessionWorktreeMetadata deleting entries on API failure, availableWorktrees being stale, or worktrees created externally via CLI—the "Re-integrate commits" section permanently shows "Available in worktree mode." with no way to recover. Fix: add useDetectedWorktreeMetadata hook that performs a lightweight git probe (`git rev-parse --absolute-git-dir --abbrev-ref HEAD`) when the store-based lookup returns undefined. If the current directory is a secondary git worktree, a minimal WorktreeMetadata is synthesised with the correct projectDirectory and branch, allowing IntegrateCommitsSection and other worktree features to function regardless of store state. The store-based lookup remains the primary fast path; the git probe only runs as a fallback and caches its result per directory. * fix(worktree): fix detection command and pass current branch from git status Two bugs in the fallback worktree detection hook: 1. `git rev-parse --absolute-git-dir --abbrev-ref HEAD` combines two independent rev-parse options whose combined output is unreliable – the two-line assumption (`lines.length < 2`) caused silent null returns, meaning the fallback never actually set worktreeMetadata. Now uses only `git rev-parse --absolute-git-dir` (single-line, deterministic output) for worktree detection. 2. The hook was called before `useGitStatus`, so no branch was available. Move the call to after `const status = useGitStatus(...)` and pass `status?.current` as `currentBranch`, eliminating the need for a second git command and keeping the branch in sync with the already-polled git status. Also removes `detected` from the useEffect deps array – it was an unnecessary dep that triggered a re-run on every detected state change. * fix(worktree): use worktree toplevel path and reset stale metadata immediately Two bugs in useDetectedWorktreeMetadata: 1. path was set from currentDirectory (the active sub-folder) instead of the worktree root. git rev-parse --show-toplevel now provides the actual worktree toplevel, so operations like `git worktree remove` receive a valid root path regardless of which sub-directory is open. 2. When currentDirectory changed with no storeMetadata, the hook kept returning the prior detected value until the async git probe finished. Calling setDetected(undefined) before launching the async task eliminates the stale-metadata window. --- packages/ui/src/components/views/GitView.tsx | 7 +- .../views/git/IntegrateCommitsSection.tsx | 6 + .../ui/src/hooks/useDetectedWorktreeRoot.ts | 110 ++++++++++++++++++ .../src/lib/git/integrateWorktreeCommits.ts | 2 +- 4 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 packages/ui/src/hooks/useDetectedWorktreeRoot.ts diff --git a/packages/ui/src/components/views/GitView.tsx b/packages/ui/src/components/views/GitView.tsx index ba5b2ed3..8ef8a75f 100644 --- a/packages/ui/src/components/views/GitView.tsx +++ b/packages/ui/src/components/views/GitView.tsx @@ -45,6 +45,7 @@ import { import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useUIStore } from '@/stores/useUIStore'; +import { useDetectedWorktreeMetadata } from '@/hooks/useDetectedWorktreeRoot'; import { IntegrateCommitsSection } from './git/IntegrateCommitsSection'; import { GitHeader } from './git/GitHeader'; @@ -251,7 +252,7 @@ export const GitView: React.FC = () => { return undefined; }, [availableWorktrees, normalizedCurrentDirectory, worktreeMap]); - const worktreeMetadata = React.useMemo(() => { + const storeWorktreeMetadata = React.useMemo(() => { if (currentSessionId) { return worktreeMap.get(currentSessionId) ?? inferredWorktreeMetadata; } @@ -263,12 +264,13 @@ export const GitView: React.FC = () => { return undefined; }, [currentSessionId, inferredWorktreeMetadata, newSessionDraft?.open, worktreeMap]); - const { profiles, globalIdentity, defaultGitIdentityId, loadProfiles, loadGlobalIdentity, loadDefaultGitIdentityId } = useGitIdentitiesStore(); const isGitRepo = useIsGitRepo(currentDirectory ?? null); const status = useGitStatus(currentDirectory ?? null); + + const worktreeMetadata = useDetectedWorktreeMetadata(currentDirectory, storeWorktreeMetadata, status?.current ?? undefined); const branches = useGitBranches(currentDirectory ?? null); const log = useGitLog(currentDirectory ?? null); const currentIdentity = useGitIdentity(currentDirectory ?? null); @@ -2078,6 +2080,7 @@ export const GitView: React.FC = () => {