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'
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(
<ProjectContextChip
workDir="/workspace/OpenCutSkill/.claude/worktrees/desktop-main-54a09f85"
@ -17,9 +17,9 @@ describe('ProjectContextChip', () => {
)
expect(screen.getByText('OpenCutSkill')).toBeInTheDocument()
expect(screen.getByText('main')).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', () => {

View File

@ -26,6 +26,7 @@ export function ProjectContextChip({
const labelRoot = isWorktree ? (sourceWorkDir || workDir) : workDir
const label = branch ? (repoName || basename(labelRoot)) : (basename(labelRoot) || repoName || '')
const worktreeName = worktreeSlug || basename(worktreePath) || 'isolated'
const showBranch = !!branch && !isWorktree
const title = [
label,
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'
}`}
>
{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)]">
<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>
@ -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="truncate font-medium text-[var(--color-text-primary)]">{label}</span>
{branch ? (
{showBranch ? (
<>
<span className="text-[var(--color-text-tertiary)]">|</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)]">
worktree
</span>
<span className="max-w-[12rem] truncate">{worktreeName}</span>
</>
) : null}
</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 { sessionId } = await sessionService.createSession(
workDir,
{ branch: 'main', worktree: true },
{ branch: 'feature/rail', worktree: true },
)
const launchInfo = await sessionService.getSessionLaunchInfo(sessionId)
const repository = launchInfo?.repository
@ -2002,7 +2002,7 @@ describe('Sessions API', () => {
expect(repository?.worktreeBranch).toBeTruthy()
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 }>
sessionsMap.set(sessionId, { workDir: activeWorktree })
@ -2022,7 +2022,7 @@ describe('Sessions API', () => {
branch: string | null
} | null
}
expect(body.branch).toBe(repository!.worktreeBranch)
expect(body.branch).toBe('feature/rail')
expect(body.workDir).toBe(activeWorktree)
expect(body.worktree).toEqual({
enabled: true,
@ -2067,7 +2067,7 @@ describe('Sessions API', () => {
branch: string | null
} | null
}
expect(body.branch).toBe('worktree-desktop-main-12345678')
expect(body.branch).toBe('main')
expect(body.workDir).toBe(activeWorktree)
expect(body.worktree).toEqual({
enabled: true,
@ -2121,7 +2121,7 @@ describe('Sessions API', () => {
branch: string | null
} | null
}
expect(body.branch).toBe('worktree-desktop-main-12345678')
expect(body.branch).toBe('main')
expect(body.worktree).toMatchObject({
path: activeWorktree,
plannedPath: activeWorktree,

View File

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