Skip to content

fix: label worktree rows by the branch they are for, not the folder name - #24

Merged
xico42 merged 1 commit into
mainfrom
fix/main-branch-display
Jul 19, 2026
Merged

fix: label worktree rows by the branch they are for, not the folder name#24
xico42 merged 1 commit into
mainfrom
fix/main-branch-display

Conversation

@xico42

@xico42 xico42 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Problem

The main worktree vanished from ch list worktree and the TUI dashboard whenever its clone dir had a non-default branch checked out. Each row was labelled with whatever HEAD happened to be on, so a geomonitor clone sitting on docs/rbac-epic showed up as docs/rbac-epicmain never appeared, and nothing distinguished the main worktree from the branch it was holding. The CLI compounded it by dropping the head-state entirely.

Fix

Separate addressing from display. Ref stays the worktree's identity — the folder-derived key every operation feeds back — while a new resolveDisplay decides the label from authoritative sources rather than reconstructing it from the folder name:

  • git's live branch is the ground truth of what's checked out.
  • The branch a worktree is for — its original — is the configured default_branch for the main clone dir, or the branch a running session recorded in @codeherd_branch for any other worktree.
  • A row is shown diverged, <original> (on <live>), only when HEAD has actually left that original — so the main clone on a feature branch stays main (on docs/rbac-epic) and remains spottable.

A worktree with no known original (a non-main worktree with no session) is never treated as diverged: it shows exactly what git reports. This keeps the folder name out of the label — rendering the folder identity is what produced chore-cron-rework (on chore/restore-cron-rework) for a worktree simply sitting on its own branch whose directory name happens not to match it. The session is a refinement, never a dependency: the common case is a worktree with no session, and it resolves from git alone.

Both surfaces now render through one FormatBranchLabel, so the CLI listing and the TUI agree on how a diverged or detached worktree reads. Display resolution moves after the session join in List, because a non-main worktree's original branch lives on its session, not in the git worktree entry.

Tests

  • Domain unit — the full settled table: main diverged/detached, non-main no-session (shows live branch, no hint), session-proven divergence, session-agrees, detached-with-session recovery.
  • Formatter unitFormatBranchLabel.
  • TUI unit — no-session-live and session-divergence renders through buildItems → delegate.
  • CLI integration — real git + tmux isolation: TestListWorktree_noSession_showsLiveBranch (primary path) and TestListWorktree_sessionDivergence_showsRecordedBranch (real shell session).

make check passes: 85.5% coverage, integration green, lint 0 issues, build OK.

🤖 Generated with Claude Code

The main worktree vanished from `ch list worktree` and the TUI dashboard
whenever its clone dir had a non-default branch checked out. Each row was
labelled with whatever HEAD happened to be on, so a geomonitor clone
sitting on docs/rbac-epic showed up as "docs/rbac-epic" — "main" never
appeared, and nothing distinguished the main worktree from the branch it
was holding. The CLI compounded it by dropping the head-state entirely.

The fix separates addressing from display. Ref stays the worktree's
identity — the folder-derived key every operation feeds back — while a new
resolveDisplay decides the label from authoritative sources rather than
reconstructing it from the folder name. git's live branch is the ground
truth of what is checked out. The branch a worktree is *for* — its
original — is the configured default branch for the main clone dir, or the
branch a running session recorded in @codeherd_branch for any other
worktree. A row is only shown as diverged, "<original> (on <live>)", when
HEAD has actually left that original; the main clone on a feature branch
therefore stays "main (on docs/rbac-epic)" and remains spottable.

A worktree with no known original — a non-main worktree with no session —
is never treated as diverged: it shows exactly what git reports. This is
what keeps the folder name out of the label. Rendering the folder identity
is what produced "chore-cron-rework (on chore/restore-cron-rework)" for a
worktree simply sitting on its own branch, whose directory name happens
not to match it. The session is a refinement, never a dependency: the
common case is a worktree with no session, and it must resolve from git
alone.

Both surfaces now render through one FormatBranchLabel, so the CLI listing
and the TUI agree on how a diverged or detached worktree reads. Display
resolution moves after the session join in List, because a non-main
worktree's original branch lives on its session, not in the git worktree
entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@xico42
xico42 merged commit 0e1b6ea into main Jul 19, 2026
7 checks passed
@xico42
xico42 deleted the fix/main-branch-display branch July 19, 2026 17:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant