fix: hide internal worktree branch labels

Desktop worktree sessions should show the source project and worktree marker without surfacing implementation refs like worktree-desktop-* as user-facing branch state. The git-info response now keeps launch branch metadata separate from worktree identity, and the desktop chip hides branch and slug labels in isolated worktree mode.

Constraint: Git worktrees need an internal branch for isolated execution, but that ref is product plumbing rather than useful UI context
Rejected: Show both launch branch and worktree slug | duplicated noisy identifiers and confused the session location
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/sessions.test.ts -t "git-info"; cd desktop && bun run test src/components/shared/ProjectContextChip.test.tsx; bun run check:server; bun run check:desktop; git diff --check
Not-tested: Live desktop screenshot smoke
This commit is contained in:
程序员阿江(Relakkes) 2026-05-23 16:16:30 +08:00
parent 5f3da46354
commit 89ec7d66c9
4 changed files with 17 additions and 15 deletions

View File

@ -4,7 +4,7 @@ import '@testing-library/jest-dom'
import { ProjectContextChip } from './ProjectContextChip' import { ProjectContextChip } from './ProjectContextChip'
describe('ProjectContextChip', () => { describe('ProjectContextChip', () => {
it('keeps the source project label and adds worktree identity', () => { it('shows only the source project label and worktree marker for isolated worktrees', () => {
render( render(
<ProjectContextChip <ProjectContextChip
workDir="/workspace/OpenCutSkill/.claude/worktrees/desktop-main-54a09f85" workDir="/workspace/OpenCutSkill/.claude/worktrees/desktop-main-54a09f85"
@ -17,9 +17,9 @@ describe('ProjectContextChip', () => {
) )
expect(screen.getByText('OpenCutSkill')).toBeInTheDocument() expect(screen.getByText('OpenCutSkill')).toBeInTheDocument()
expect(screen.getByText('main')).toBeInTheDocument()
expect(screen.getByText('worktree')).toBeInTheDocument() expect(screen.getByText('worktree')).toBeInTheDocument()
expect(screen.getByText('desktop-main-54a09f85')).toBeInTheDocument() expect(screen.queryByText('main')).not.toBeInTheDocument()
expect(screen.queryByText('desktop-main-54a09f85')).not.toBeInTheDocument()
}) })
it('does not show worktree details for a normal checkout', () => { it('does not show worktree details for a normal checkout', () => {

View File

@ -26,6 +26,7 @@ export function ProjectContextChip({
const labelRoot = isWorktree ? (sourceWorkDir || workDir) : workDir const labelRoot = isWorktree ? (sourceWorkDir || workDir) : workDir
const label = branch ? (repoName || basename(labelRoot)) : (basename(labelRoot) || repoName || '') const label = branch ? (repoName || basename(labelRoot)) : (basename(labelRoot) || repoName || '')
const worktreeName = worktreeSlug || basename(worktreePath) || 'isolated' const worktreeName = worktreeSlug || basename(worktreePath) || 'isolated'
const showBranch = !!branch && !isWorktree
const title = [ const title = [
label, label,
branch ? `branch: ${branch}` : null, branch ? `branch: ${branch}` : null,
@ -43,7 +44,7 @@ export function ProjectContextChip({
compact ? 'gap-1.5 px-3 py-1.5 text-xs' : 'gap-2 px-4 py-2 text-sm' compact ? 'gap-1.5 px-3 py-1.5 text-xs' : 'gap-2 px-4 py-2 text-sm'
}`} }`}
> >
{branch ? ( {showBranch ? (
<svg width={compact ? 15 : 18} height={compact ? 15 : 18} viewBox="0 0 16 16" fill="currentColor" className="shrink-0 text-[var(--color-text-secondary)]"> <svg width={compact ? 15 : 18} height={compact ? 15 : 18} viewBox="0 0 16 16" fill="currentColor" className="shrink-0 text-[var(--color-text-secondary)]">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" /> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg> </svg>
@ -51,7 +52,7 @@ export function ProjectContextChip({
<span className={`material-symbols-outlined text-[var(--color-text-secondary)] ${compact ? 'text-[15px]' : 'text-[18px]'}`}>folder</span> <span className={`material-symbols-outlined text-[var(--color-text-secondary)] ${compact ? 'text-[15px]' : 'text-[18px]'}`}>folder</span>
)} )}
<span className="truncate font-medium text-[var(--color-text-primary)]">{label}</span> <span className="truncate font-medium text-[var(--color-text-primary)]">{label}</span>
{branch ? ( {showBranch ? (
<> <>
<span className="text-[var(--color-text-tertiary)]">|</span> <span className="text-[var(--color-text-tertiary)]">|</span>
<span className="truncate">{branch}</span> <span className="truncate">{branch}</span>
@ -63,7 +64,6 @@ export function ProjectContextChip({
<span className="shrink-0 rounded-full border border-[var(--color-border)] px-1.5 py-0.5 text-[10px] font-medium uppercase leading-none text-[var(--color-text-tertiary)]"> <span className="shrink-0 rounded-full border border-[var(--color-border)] px-1.5 py-0.5 text-[10px] font-medium uppercase leading-none text-[var(--color-text-tertiary)]">
worktree worktree
</span> </span>
<span className="max-w-[12rem] truncate">{worktreeName}</span>
</> </>
) : null} ) : null}
</div> </div>

View File

@ -1990,11 +1990,11 @@ describe('Sessions API', () => {
} }
}) })
it('GET /api/sessions/:id/git-info should include isolated worktree identity', async () => { it('GET /api/sessions/:id/git-info should keep the visible launch branch while including isolated worktree identity', async () => {
const workDir = await createCleanGitRepo(tmpDir) const workDir = await createCleanGitRepo(tmpDir)
const { sessionId } = await sessionService.createSession( const { sessionId } = await sessionService.createSession(
workDir, workDir,
{ branch: 'main', worktree: true }, { branch: 'feature/rail', worktree: true },
) )
const launchInfo = await sessionService.getSessionLaunchInfo(sessionId) const launchInfo = await sessionService.getSessionLaunchInfo(sessionId)
const repository = launchInfo?.repository const repository = launchInfo?.repository
@ -2002,7 +2002,7 @@ describe('Sessions API', () => {
expect(repository?.worktreeBranch).toBeTruthy() expect(repository?.worktreeBranch).toBeTruthy()
const activeWorktree = repository!.worktreePath! const activeWorktree = repository!.worktreePath!
git(workDir, 'worktree', 'add', '-b', repository!.worktreeBranch!, activeWorktree, 'main') git(workDir, 'worktree', 'add', '-b', repository!.worktreeBranch!, activeWorktree, 'feature/rail')
const sessionsMap = (conversationService as any).sessions as Map<string, { workDir: string }> const sessionsMap = (conversationService as any).sessions as Map<string, { workDir: string }>
sessionsMap.set(sessionId, { workDir: activeWorktree }) sessionsMap.set(sessionId, { workDir: activeWorktree })
@ -2022,7 +2022,7 @@ describe('Sessions API', () => {
branch: string | null branch: string | null
} | null } | null
} }
expect(body.branch).toBe(repository!.worktreeBranch) expect(body.branch).toBe('feature/rail')
expect(body.workDir).toBe(activeWorktree) expect(body.workDir).toBe(activeWorktree)
expect(body.worktree).toEqual({ expect(body.worktree).toEqual({
enabled: true, enabled: true,
@ -2067,7 +2067,7 @@ describe('Sessions API', () => {
branch: string | null branch: string | null
} | null } | null
} }
expect(body.branch).toBe('worktree-desktop-main-12345678') expect(body.branch).toBe('main')
expect(body.workDir).toBe(activeWorktree) expect(body.workDir).toBe(activeWorktree)
expect(body.worktree).toEqual({ expect(body.worktree).toEqual({
enabled: true, enabled: true,
@ -2121,7 +2121,7 @@ describe('Sessions API', () => {
branch: string | null branch: string | null
} | null } | null
} }
expect(body.branch).toBe('worktree-desktop-main-12345678') expect(body.branch).toBe('main')
expect(body.worktree).toMatchObject({ expect(body.worktree).toMatchObject({
path: activeWorktree, path: activeWorktree,
plannedPath: activeWorktree, plannedPath: activeWorktree,

View File

@ -670,9 +670,11 @@ async function getGitInfo(sessionId: string): Promise<Response> {
sameResolvedPath(workDir, worktree.path) || sameResolvedPath(workDir, worktree.path) ||
sameResolvedPath(workDir, worktree.plannedPath) sameResolvedPath(workDir, worktree.plannedPath)
) )
const branch = materializedWorktree const branch = sessionBranch || (
? (gitBranch || worktree.branch || sessionBranch) materializedWorktree
: (sessionBranch || gitBranch) ? (worktree.branch || gitBranch)
: gitBranch
)
// Get repo name from remote or directory // Get repo name from remote or directory
let repoName = '' let repoName = ''