From f92083ac0c0750ab7e07015ede88835b2056aebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 8 May 2026 18:37:42 +0800 Subject: [PATCH] fix: prefer native worktree identity for session badges CLI transcripts persist native worktree-state entries for resume, so the desktop git-info endpoint should treat that state as authoritative for worktree identity. Desktop launch metadata can be stale after placeholder cleanup or reopen, and should only fill gaps. The displayed business branch still prefers the desktop launch branch because CLI originalBranch means the source checkout before worktree creation, not necessarily the selected base ref. Constraint: CLI worktree-state carries runtime identity; Desktop repository metadata carries launch intent. Rejected: Let desktop repository metadata override worktree-state | stale placeholder metadata can resurrect the wrong slug, path, or source cwd after reload. Confidence: high Scope-risk: narrow Directive: Worktree identity fields should prefer CLI worktree-state; only branch display may prefer desktop launch intent. Tested: bun test src/server/__tests__/sessions.test.ts --test-name-pattern worktree-state|worktree identity|git-info Tested: cd desktop && bun run lint Tested: git diff --check --- src/server/__tests__/sessions.test.ts | 52 +++++++++++++++++++++++++++ src/server/api/sessions.ts | 11 +++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/server/__tests__/sessions.test.ts b/src/server/__tests__/sessions.test.ts index 83ec1bb1..0ed43cb5 100644 --- a/src/server/__tests__/sessions.test.ts +++ b/src/server/__tests__/sessions.test.ts @@ -1677,6 +1677,58 @@ describe('Sessions API', () => { }) }) + it('GET /api/sessions/:id/git-info should prefer CLI worktree-state identity over desktop metadata', async () => { + const workDir = await createCleanGitRepo(tmpDir) + const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + const activeWorktree = path.join(workDir, '.claude', 'worktrees', 'desktop-main-12345678') + git(workDir, 'worktree', 'add', '-b', 'worktree-desktop-main-12345678', activeWorktree, 'main') + await writeSessionFile(sanitizePath(activeWorktree), sessionId, [ + makeSnapshotEntry(), + { + type: 'session-meta', + isMeta: true, + workDir: activeWorktree, + repository: { + requestedWorkDir: '/stale/source', + repoRoot: '/stale/source', + branch: 'main', + worktree: true, + baseRef: 'main', + worktreePath: '/stale/source/.claude/worktrees/stale', + worktreeBranch: 'worktree-stale', + worktreeSlug: 'stale', + }, + timestamp: '2026-01-01T00:00:00.000Z', + }, + makeWorktreeStateEntry(sessionId, activeWorktree, { + originalCwd: await fs.realpath(workDir), + }), + makeUserEntry('Hello from persisted worktree state'), + ]) + + const res = await fetch(`${baseUrl}/api/sessions/${sessionId}/git-info`) + expect(res.status).toBe(200) + + const body = (await res.json()) as { + branch: string | null + worktree: { + path: string | null + plannedPath: string | null + sourceWorkDir: string | null + slug: string | null + branch: string | null + } | null + } + expect(body.branch).toBe('main') + expect(body.worktree).toMatchObject({ + path: activeWorktree, + plannedPath: activeWorktree, + sourceWorkDir: await fs.realpath(workDir), + slug: 'desktop-main-12345678', + branch: 'worktree-desktop-main-12345678', + }) + }) + it('DELETE /api/sessions/:id should delete the session', async () => { const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' await writeSessionFile('-tmp-api-test', sessionId, [makeSnapshotEntry()]) diff --git a/src/server/api/sessions.ts b/src/server/api/sessions.ts index 8eb82fd7..fdb1f169 100644 --- a/src/server/api/sessions.ts +++ b/src/server/api/sessions.ts @@ -541,15 +541,18 @@ async function getGitInfo(sessionId: string): Promise { const launchInfo = await sessionService.getSessionLaunchInfo(sessionId).catch(() => null) const repository = launchInfo?.repository const worktreeSession = launchInfo?.worktreeSession + // The visible business branch comes from Desktop's launch choice when present. + // CLI originalBranch is the source checkout before creating the worktree, which + // can differ from the selected base ref. const sessionBranch = repository?.branch || worktreeSession?.originalBranch || null const worktree = repository?.worktree || worktreeSession ? { enabled: true, path: worktreeSession?.worktreePath || workDir, - plannedPath: repository?.worktreePath || worktreeSession?.worktreePath || null, - sourceWorkDir: repository?.requestedWorkDir || repository?.repoRoot || worktreeSession?.originalCwd || null, - slug: repository?.worktreeSlug || worktreeSession?.worktreeName || null, - branch: repository?.worktreeBranch || worktreeSession?.worktreeBranch || null, + plannedPath: worktreeSession?.worktreePath || repository?.worktreePath || null, + sourceWorkDir: worktreeSession?.originalCwd || repository?.requestedWorkDir || repository?.repoRoot || null, + slug: worktreeSession?.worktreeName || repository?.worktreeSlug || null, + branch: worktreeSession?.worktreeBranch || repository?.worktreeBranch || null, } : null