Skip to content

Fix sidebar branch refresh after terminal checkout - #2307

Closed
austinywang wants to merge 3 commits into
mainfrom
issue-666-sidebar-branch-update-pr
Closed

austinywang wants to merge 3 commits into
mainfrom
issue-666-sidebar-branch-update-pr

Conversation

@austinywang

@austinywang austinywang commented Mar 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #666.

Summary

  • add a regression test for sidebar branch state after a workspace terminal checks out a new branch
  • add a lightweight tracked git branch poll so sidebar branch labels refresh even when shell hooks miss the checkout
  • trigger the slower PR metadata refresh only when the polled branch actually changes

Verification

  • did not run local tests
  • ran ./scripts/reload.sh --tag sidebar-branch-update --launch
  • ran ./scripts/reload.sh --tag sidebar-branch-update

Summary by cubic

Fixes stale sidebar branch after a terminal checkout by polling the repo HEAD every ~2s. Sidebar branch updates quickly, and PR metadata refreshes only when the branch actually changes.

  • Bug Fixes
    • Added a 2s workspace git branch poll to update sidebar branch/dirty state when shell hooks miss checkouts.
    • Prevented overlapping branch/metadata probes; PR metadata refreshes only on branch change.
    • Disabled branch polling during unit tests to keep tests deterministic, and added a regression test that verifies sidebar updates after checkout and clears stale PR data.

Written for commit 977f7fd. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Continuous Git branch monitoring (polls ~every 2s) keeps sidebar branch labels and workspace branch state up-to-date and automatically refreshes related pull request info when the branch changes.
  • Bug Fixes
    • Ensures branch changes clear stale pull request entries and reflect clean/dirty status reliably.
  • Tests
    • Added unit coverage validating sidebar and workspace state update after branch checkout.

@vercel

vercel Bot commented Mar 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 30, 2026 1:11am

@coderabbitai

coderabbitai Bot commented Mar 28, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Added periodic (2s) Git-branch polling that captures branch + dirty state as a new private value type and applies branch snapshots to panel/workspace state with in-flight deduplication and triggers metadata refreshes when branch labels change.

Changes

Cohort / File(s) Summary
Git Branch Polling & Snapshot Logic
Sources/TabManager.swift
Added WorkspaceGitBranchSnapshot type, a 2s workspaceGitBranchPollTimer, in-flight dedup keys (workspaceGitBranchProbeInFlightKeys), candidate selection (trackedWorkspaceGitBranchPollCandidatePanelIds), async scheduling (scheduleTrackedWorkspaceGitBranchRefreshIfPossible), apply logic (applyTrackedWorkspaceGitBranchSnapshot), and lifecycle/test integration (init/deinit and refreshTrackedWorkspaceGitMetadataForTesting). Refactored initial metadata snapshot to derive branch via workspaceGitBranchSnapshot(for:).
Unit Test
cmuxTests/TabManagerUnitTests.swift
Added testTrackedWorkspaceGitBranchRefreshUpdatesSidebarBranchAfterCheckout which creates a temp repo, checks out a branch, invokes test refresh, and asserts per-panel and workspace branch/isDirty updates and clearing of PR entries.

Sequence Diagram

sequenceDiagram
    participant Timer as Timer (2s)
    participant TM as TabManager
    participant Probe as Git Probe (async)
    participant Main as Main Actor
    participant Store as Workspace State

    Timer->>TM: tick
    TM->>TM: select candidate panels\n(filter active/eligible & in-flight dedupe)
    TM->>Probe: schedule probe (repo dir → git branch/status)
    Probe->>Probe: run git commands
    Probe->>Main: return WorkspaceGitBranchSnapshot
    Main->>TM: applyTrackedWorkspaceGitBranchSnapshot(snapshot)
    TM->>TM: validate workspace/panel & dir match
    TM->>Store: update/clear panelGitBranches
    alt branch label changed
        TM->>TM: scheduleWorkspaceGitMetadataRefreshIfPossible(reason: "branchPollChanged")
        TM->>Store: trigger PR/metadata refresh
    else no change
        TM->>Store: no metadata refresh
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped a timer, two seconds went by,

I sniffed every branch where the git-logs lie.
Sidebar no longer clings to the past,
Fresh names appear—quick as a dash.
Hooray, little repo, your labels now fly!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: fixing stale sidebar branch display after terminal checkout.
Description check ✅ Passed The description covers the Summary and Verification sections but lacks the Testing and Demo Video sections from the template.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #666: adds a lightweight 2s branch poll, updates sidebar branch in near real-time, triggers PR metadata refresh only on branch change, and includes a regression test.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #666: branch polling implementation, PR metadata refresh optimization, test seam updates, and regression test for sidebar branch refresh.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-666-sidebar-branch-update-pr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cmuxTests/TabManagerUnitTests.swift (1)

585-595: Fold workspace-level assertions into the wait predicate to reduce test flake risk.

waitForCondition on Line 586 currently gates only panel-level state, while Line 592-595 immediately assert workspace-level snapshot state. If those propagate one runloop later, this can intermittently fail.

Proposed stabilization diff
-        XCTAssertTrue(
-            waitForCondition {
-                workspace.panelGitBranches[panelId]?.branch == "feature/sidebar-branch-update"
-                    && workspace.panelGitBranches[panelId]?.isDirty == false
-                    && workspace.panelPullRequests[panelId] == nil
-            }
-        )
-        XCTAssertEqual(workspace.gitBranch?.branch, "feature/sidebar-branch-update")
-        XCTAssertEqual(workspace.gitBranch?.isDirty, false)
-        XCTAssertNil(workspace.pullRequest)
+        XCTAssertTrue(
+            waitForCondition {
+                workspace.panelGitBranches[panelId]?.branch == "feature/sidebar-branch-update"
+                    && workspace.panelGitBranches[panelId]?.isDirty == false
+                    && workspace.panelPullRequests[panelId] == nil
+                    && workspace.gitBranch?.branch == "feature/sidebar-branch-update"
+                    && workspace.gitBranch?.isDirty == false
+                    && workspace.pullRequest == nil
+            }
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/TabManagerUnitTests.swift` around lines 585 - 595, The test
currently waits only for panel-level state via waitForCondition {
workspace.panelGitBranches[panelId]? ... } but then immediately asserts
workspace-level snapshot (workspace.gitBranch and workspace.pullRequest), which
can race; update the waitForCondition predicate (the closure passed to
waitForCondition) to also assert workspace.gitBranch?.branch ==
"feature/sidebar-branch-update", workspace.gitBranch?.isDirty == false, and
workspace.pullRequest == nil so all relevant assertions are synchronized, then
remove the subsequent standalone XCTAssertEqual/XCTAssertNil lines; reference
waitForCondition, workspace.panelGitBranches, workspace.gitBranch,
workspace.pullRequest, and panelId when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/TabManager.swift`:
- Around line 1738-1751: The git invocations in workspaceGitBranchSnapshot (and
the similar method at lines ~1753-1758) currently call runGitCommand without a
timeout which can leave probes stuck; update these call sites to use a
timeout-aware variant of runGitCommand (or wrap the git Process call with a
timeout) so the branch and status commands return nil/error on timeout, then
handle that result by returning a safe WorkspaceGitBranchSnapshot (branch: nil,
isDirty: false) and ensuring any probe bookkeeping
(workspaceGitBranchProbeInFlightKeys) is cleared when a timeout occurs;
specifically modify workspaceGitBranchSnapshot and the sibling function to call
the timeout-enabled runGitCommand, handle timed-out/nil outputs gracefully, and
make sure in-flight probe keys are removed on timeout.

---

Nitpick comments:
In `@cmuxTests/TabManagerUnitTests.swift`:
- Around line 585-595: The test currently waits only for panel-level state via
waitForCondition { workspace.panelGitBranches[panelId]? ... } but then
immediately asserts workspace-level snapshot (workspace.gitBranch and
workspace.pullRequest), which can race; update the waitForCondition predicate
(the closure passed to waitForCondition) to also assert
workspace.gitBranch?.branch == "feature/sidebar-branch-update",
workspace.gitBranch?.isDirty == false, and workspace.pullRequest == nil so all
relevant assertions are synchronized, then remove the subsequent standalone
XCTAssertEqual/XCTAssertNil lines; reference waitForCondition,
workspace.panelGitBranches, workspace.gitBranch, workspace.pullRequest, and
panelId when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7685a005-2841-42a3-a78e-ecee4c209b15

📥 Commits

Reviewing files that changed from the base of the PR and between f1be397 and 5f6e7ca.

📒 Files selected for processing (2)
  • Sources/TabManager.swift
  • cmuxTests/TabManagerUnitTests.swift

Comment thread Sources/TabManager.swift
Comment on lines +1738 to 1751
private nonisolated static func workspaceGitBranchSnapshot(
for directory: String
) -> WorkspaceGitBranchSnapshot {
let branch = normalizedBranchName(
runGitCommand(directory: directory, arguments: ["branch", "--show-current"])
)
guard let branch else {
return WorkspaceGitBranchSnapshot(branch: nil, isDirty: false)
}

let statusOutput = runGitCommand(directory: directory, arguments: ["status", "--porcelain", "-uno"])
let isDirty = !(statusOutput?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
let pullRequest = workspacePullRequestSnapshot(directory: directory, branch: branch)
return InitialWorkspaceGitMetadataSnapshot(branch: branch, isDirty: isDirty, pullRequest: pullRequest)
return WorkspaceGitBranchSnapshot(branch: branch, isDirty: isDirty)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeouts to periodic git branch probes to prevent permanent in-flight stalls.

These commands run in the 2s polling path, but they currently have no timeout. If git hangs, the probe key remains in workspaceGitBranchProbeInFlightKeys and that panel can stop refreshing branch state indefinitely.

🛠️ Proposed fix
@@
-    private nonisolated static let workspacePullRequestProbeTimeout: TimeInterval = 5.0
+    private nonisolated static let workspacePullRequestProbeTimeout: TimeInterval = 5.0
+    private nonisolated static let workspaceGitBranchProbeTimeout: TimeInterval = 3.0
@@
     private nonisolated static func workspaceGitBranchSnapshot(
         for directory: String
     ) -> WorkspaceGitBranchSnapshot {
         let branch = normalizedBranchName(
-            runGitCommand(directory: directory, arguments: ["branch", "--show-current"])
+            runGitCommand(
+                directory: directory,
+                arguments: ["branch", "--show-current"],
+                timeout: workspaceGitBranchProbeTimeout
+            )
         )
         guard let branch else {
             return WorkspaceGitBranchSnapshot(branch: nil, isDirty: false)
         }
 
-        let statusOutput = runGitCommand(directory: directory, arguments: ["status", "--porcelain", "-uno"])
+        let statusOutput = runGitCommand(
+            directory: directory,
+            arguments: ["status", "--porcelain", "-uno"],
+            timeout: workspaceGitBranchProbeTimeout
+        )
         let isDirty = !(statusOutput?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
         return WorkspaceGitBranchSnapshot(branch: branch, isDirty: isDirty)
     }
 
-    private nonisolated static func runGitCommand(directory: String, arguments: [String]) -> String? {
+    private nonisolated static func runGitCommand(
+        directory: String,
+        arguments: [String],
+        timeout: TimeInterval? = nil
+    ) -> String? {
         runCommand(
             directory: directory,
             executable: "git",
-            arguments: arguments
+            arguments: arguments,
+            timeout: timeout
         )
     }

Also applies to: 1753-1758

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TabManager.swift` around lines 1738 - 1751, The git invocations in
workspaceGitBranchSnapshot (and the similar method at lines ~1753-1758)
currently call runGitCommand without a timeout which can leave probes stuck;
update these call sites to use a timeout-aware variant of runGitCommand (or wrap
the git Process call with a timeout) so the branch and status commands return
nil/error on timeout, then handle that result by returning a safe
WorkspaceGitBranchSnapshot (branch: nil, isDirty: false) and ensuring any probe
bookkeeping (workspaceGitBranchProbeInFlightKeys) is cleared when a timeout
occurs; specifically modify workspaceGitBranchSnapshot and the sibling function
to call the timeout-enabled runGitCommand, handle timed-out/nil outputs
gracefully, and make sure in-flight probe keys are removed on timeout.

@greptile-apps

greptile-apps Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes #666 by adding a lightweight 2-second git branch polling loop (startWorkspaceGitBranchPollTimer / refreshTrackedWorkspaceGitBranches) alongside the existing 30-second PR-metadata poll, so sidebar branch labels update promptly even when shell hooks miss a terminal checkout. The slower metadata refresh is gated on an actual branch change rather than running on every poll tick.

Key changes:

  • New WorkspaceGitBranchSnapshot value type and workspaceGitBranchSnapshot(for:) static helper; initialWorkspaceGitMetadataSnapshot is refactored to reuse it cleanly.
  • trackedWorkspaceGitPollCandidatePanelIds is extracted as a shared base; both the new branch-poll and the existing metadata-poll filter candidate panels from it.
  • Both in-flight probe sets (workspaceGitBranchProbeInFlightKeys and workspaceGitProbeGenerationByKey) are checked before scheduling to prevent concurrent probes for the same panel.
  • refreshTrackedWorkspaceGitMetadataForTesting() now calls both refreshers so the new integration test can drive the full path in one call.

Issues found:

  • The metadata-refresh trigger in applyTrackedWorkspaceGitBranchSnapshot is guarded by nextState?.branch != nil, which silently skips the refresh when a panel enters detached HEAD. The old PR remains in the sidebar for up to 30 s until the periodic metadata poll fires.
  • The regression test and the production fix land in the same commit, violating the project's two-commit regression test policy from CLAUDE.md.

Confidence Score: 4/5

Safe to merge with one minor logic fix; the detached HEAD edge case leaves a stale PR visible for up to 30 s but does not corrupt data.

The implementation is well-structured and correctly guards against concurrent probes. The only real logic gap (stale PR in detached HEAD) is a narrow edge case and not a regression from pre-PR behaviour for most users. Both findings are P2; the detached-HEAD fix is a one-line change worth doing before merge.

Sources/TabManager.swift — specifically the applyTrackedWorkspaceGitBranchSnapshot metadata-refresh condition at line 1688.

Important Files Changed

Filename Overview
Sources/TabManager.swift Adds a lightweight 2-second branch-polling loop alongside the existing 30-second metadata loop; correctly guards against concurrent probes and double-scheduling, but the metadata-refresh trigger condition in applyTrackedWorkspaceGitBranchSnapshot misses the branch→nil (detached HEAD) case, leaving a stale PR in the sidebar for up to 30 s.
cmuxTests/TabManagerUnitTests.swift Adds a real-git integration test covering the branch-refresh-after-checkout path; test logic is sound and uses existing waitForCondition/runGit helpers correctly, but violates the repo's two-commit regression-test policy (test and fix should be separate commits).

Sequence Diagram

sequenceDiagram
    participant Timer2s as Branch Poll (2 s)
    participant TimerMeta as Metadata Poll (30 s)
    participant Queue as initialWorkspaceGitProbeQueue
    participant Main as MainActor (TabManager)
    participant GitHub as GitHub API

    Timer2s->>Main: refreshTrackedWorkspaceGitBranches()
    Main->>Queue: workspaceGitBranchSnapshot(dir)
    Queue-->>Main: WorkspaceGitBranchSnapshot
    Main->>Main: applyTrackedWorkspaceGitBranchSnapshot()
    Note over Main: updatePanelGitBranch / clearPanelGitBranch
    alt branch changed (new branch != nil)
        Main->>Queue: scheduleWorkspaceGitMetadataRefreshIfPossible()
        Queue->>GitHub: gh pr view
        GitHub-->>Queue: PR result
        Queue-->>Main: applyWorkspaceGitMetadataSnapshot()
        Note over Main: update/clear panelPullRequests
    end

    TimerMeta->>Main: refreshTrackedWorkspaceGitMetadata()
    Main->>Queue: initialWorkspaceGitMetadataSnapshot(dir)
    Queue->>GitHub: gh pr view
    GitHub-->>Queue: PR result
    Queue-->>Main: applyWorkspaceGitMetadataSnapshot()
Loading

Reviews (1): Last reviewed commit: "fix: refresh sidebar branch after checko..." | Re-trigger Greptile

Comment thread Sources/TabManager.swift
Comment on lines +1688 to +1695
if previousState?.branch != nextState?.branch,
nextState?.branch != nil {
scheduleWorkspaceGitMetadataRefreshIfPossible(
workspaceId: probeKey.workspaceId,
panelId: probeKey.panelId,
reason: "branchPollChanged"
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale PR persists when branch transitions to detached HEAD

The nextState?.branch != nil guard prevents triggering a metadata refresh when the branch goes from a named branch to nil (i.e. the user enters detached HEAD via git checkout <sha>). In that case clearPanelGitBranch removes the branch label, but panelPullRequests[panelId] keeps the old PR. It will remain visible in the sidebar until the next 30-second periodic metadata poll — up to 30 seconds of incorrect sidebar state.

The condition should be loosened to fire on any branch change, not just changes that land on a valid branch:

Suggested change
if previousState?.branch != nextState?.branch,
nextState?.branch != nil {
scheduleWorkspaceGitMetadataRefreshIfPossible(
workspaceId: probeKey.workspaceId,
panelId: probeKey.panelId,
reason: "branchPollChanged"
)
}
if previousState?.branch != nextState?.branch {
scheduleWorkspaceGitMetadataRefreshIfPossible(
workspaceId: probeKey.workspaceId,
panelId: probeKey.panelId,
reason: "branchPollChanged"
)
}

initialWorkspaceGitMetadataSnapshot already handles the branch == nil case by returning pullRequest: .notFound, which will clear the stale PR entry.

Comment on lines +544 to 598
func testTrackedWorkspaceGitBranchRefreshUpdatesSidebarBranchAfterCheckout() throws {
let fileManager = FileManager.default
let repoURL = fileManager.temporaryDirectory.appendingPathComponent(
"cmux-git-branch-refresh-\(UUID().uuidString)"
)
try fileManager.createDirectory(at: repoURL, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: repoURL) }

try runGit(["init", "-b", "main"], in: repoURL)
try runGit(["config", "user.name", "cmux tests"], in: repoURL)
try runGit(["config", "user.email", "cmux@example.invalid"], in: repoURL)
try "seed\n".write(
to: repoURL.appendingPathComponent("README.md"),
atomically: true,
encoding: .utf8
)
try runGit(["add", "README.md"], in: repoURL)
try runGit(["commit", "-m", "Initial commit"], in: repoURL)

let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let panelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace with focused panel")
return
}

workspace.updatePanelDirectory(panelId: panelId, directory: repoURL.path)
workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false)
workspace.updatePanelPullRequest(
panelId: panelId,
number: 666,
label: "PR",
url: try XCTUnwrap(URL(string: "https://github.com/manaflow-ai/cmux/pull/666")),
status: .open,
branch: "main"
)

try runGit(["checkout", "-b", "feature/sidebar-branch-update"], in: repoURL)

manager.refreshTrackedWorkspaceGitMetadataForTesting()

XCTAssertTrue(
waitForCondition {
workspace.panelGitBranches[panelId]?.branch == "feature/sidebar-branch-update"
&& workspace.panelGitBranches[panelId]?.isDirty == false
&& workspace.panelPullRequests[panelId] == nil
}
)
XCTAssertEqual(workspace.gitBranch?.branch, "feature/sidebar-branch-update")
XCTAssertEqual(workspace.gitBranch?.isDirty, false)
XCTAssertNil(workspace.pullRequest)
}
}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Regression test and fix should be in separate commits

Per the project's regression test commit policy (CLAUDE.md), a regression test for a bug fix must be introduced in a separate commit before the fix so CI can confirm the test fails on the unfixed code and passes after the fix:

  1. Commit 1: Add testTrackedWorkspaceGitBranchRefreshUpdatesSidebarBranchAfterCheckout with no production changes — CI should go red.
  2. Commit 2: Add the startWorkspaceGitBranchPollTimer / refreshTrackedWorkspaceGitBranches fix — CI should go green.

As written, both the test and the fix land together, so the PR's commit history can't prove the test actually catches the regression.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
Sources/TabManager.swift (1)

1741-1761: ⚠️ Potential issue | 🟠 Major

Still missing timeouts on the branch poll git commands.

These calls back workspaceGitBranchProbeInFlightKeys. If either git process hangs, the completion path never runs and that panel stops polling permanently.

🛠️ Minimal fix
+    private nonisolated static let workspaceGitBranchProbeTimeout: TimeInterval = 3.0
@@
         let branch = normalizedBranchName(
-            runGitCommand(directory: directory, arguments: ["branch", "--show-current"])
+            runGitCommand(
+                directory: directory,
+                arguments: ["branch", "--show-current"],
+                timeout: workspaceGitBranchProbeTimeout
+            )
         )
@@
-        let statusOutput = runGitCommand(directory: directory, arguments: ["status", "--porcelain", "-uno"])
+        let statusOutput = runGitCommand(
+            directory: directory,
+            arguments: ["status", "--porcelain", "-uno"],
+            timeout: workspaceGitBranchProbeTimeout
+        )
@@
-    private nonisolated static func runGitCommand(directory: String, arguments: [String]) -> String? {
+    private nonisolated static func runGitCommand(
+        directory: String,
+        arguments: [String],
+        timeout: TimeInterval? = nil
+    ) -> String? {
         runCommand(
             directory: directory,
             executable: "git",
-            arguments: arguments
+            arguments: arguments,
+            timeout: timeout
         )
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TabManager.swift` around lines 1741 - 1761, The git invocations in
workspaceGitBranchSnapshot (via runGitCommand) can hang and block the probe flow
tied to workspaceGitBranchProbeInFlightKeys; update the implementation so
runGitCommand (or the underlying runCommand) enforces a reasonable timeout and
returns nil/error on timeout instead of blocking indefinitely; ensure
workspaceGitBranchSnapshot treats timeout as a failed/empty result (branch nil
or isDirty false) so the completion path always runs and the poll can continue.
Identify and modify the runGitCommand function (or runCommand it calls) to
accept/apply a timeout and to abort the Process if the timeout elapses, then
propagate a nil/string result back to workspaceGitBranchSnapshot.
🧹 Nitpick comments (1)
Sources/TabManager.swift (1)

1114-1123: Separate branch polling from the PR probe queue.

initialWorkspaceGitProbeQueue is serial, so one slow gh metadata probe on another panel/workspace can block these 2s branch snapshots behind it. That weakens the “fast branch / slow metadata” split this PR is introducing; a dedicated queue for branch probes would keep sidebar branch updates responsive while the existing per-key guards still prevent same-panel overlap.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TabManager.swift` around lines 1114 - 1123, The branch snapshot work
should run on a separate serial queue instead of initialWorkspaceGitProbeQueue;
add a dedicated DispatchQueue property (e.g., workspaceGitBranchProbeQueue) for
branch probes and use that queue when scheduling the call that computes
Self.workspaceGitBranchSnapshot(for: normalizedDirectory) and then dispatches
the Task { `@MainActor` ... self?.applyTrackedWorkspaceGitBranchSnapshot(...) };
keep the existing weak self, probeKey/key and
expectedDirectory/normalizedDirectory usage and retain the per-key guards so
same-panel overlap is still prevented while avoiding blocking branch probes
behind slow gh metadata probes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/TabManager.swift`:
- Around line 1691-1698: The current guard only schedules a metadata refresh
when the branch changed and the new branch is non-nil, so if the branch becomes
nil (detached HEAD) we skip refresh; update the condition in the block that
calls scheduleWorkspaceGitMetadataRefreshIfPossible (the one using
previousState, nextState and probeKey) to trigger whenever previousState?.branch
!= nextState?.branch (remove the nextState?.branch != nil check) so the refresh
runs both when a branch changes and when it becomes nil; keep the same
parameters (workspaceId: probeKey.workspaceId, panelId: probeKey.panelId,
reason: "branchPollChanged").

---

Duplicate comments:
In `@Sources/TabManager.swift`:
- Around line 1741-1761: The git invocations in workspaceGitBranchSnapshot (via
runGitCommand) can hang and block the probe flow tied to
workspaceGitBranchProbeInFlightKeys; update the implementation so runGitCommand
(or the underlying runCommand) enforces a reasonable timeout and returns
nil/error on timeout instead of blocking indefinitely; ensure
workspaceGitBranchSnapshot treats timeout as a failed/empty result (branch nil
or isDirty false) so the completion path always runs and the poll can continue.
Identify and modify the runGitCommand function (or runCommand it calls) to
accept/apply a timeout and to abort the Process if the timeout elapses, then
propagate a nil/string result back to workspaceGitBranchSnapshot.

---

Nitpick comments:
In `@Sources/TabManager.swift`:
- Around line 1114-1123: The branch snapshot work should run on a separate
serial queue instead of initialWorkspaceGitProbeQueue; add a dedicated
DispatchQueue property (e.g., workspaceGitBranchProbeQueue) for branch probes
and use that queue when scheduling the call that computes
Self.workspaceGitBranchSnapshot(for: normalizedDirectory) and then dispatches
the Task { `@MainActor` ... self?.applyTrackedWorkspaceGitBranchSnapshot(...) };
keep the existing weak self, probeKey/key and
expectedDirectory/normalizedDirectory usage and retain the per-key guards so
same-panel overlap is still prevented while avoiding blocking branch probes
behind slow gh metadata probes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a468ef67-2feb-4926-b73f-2417447f5d0c

📥 Commits

Reviewing files that changed from the base of the PR and between 5f6e7ca and 977f7fd.

📒 Files selected for processing (1)
  • Sources/TabManager.swift

Comment thread Sources/TabManager.swift
Comment on lines +1691 to +1698
if previousState?.branch != nextState?.branch,
nextState?.branch != nil {
scheduleWorkspaceGitMetadataRefreshIfPossible(
workspaceId: probeKey.workspaceId,
panelId: probeKey.panelId,
reason: "branchPollChanged"
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Refresh PR state when the polled branch becomes nil.

If a checkout lands in detached HEAD, this guard skips the follow-up metadata refresh, so the sidebar can show no branch while still keeping the previous PR badge until the 30s metadata poll runs.

🛠️ Minimal fix
-        if previousState?.branch != nextState?.branch,
-           nextState?.branch != nil {
+        if previousState?.branch != nextState?.branch {
             scheduleWorkspaceGitMetadataRefreshIfPossible(
                 workspaceId: probeKey.workspaceId,
                 panelId: probeKey.panelId,
                 reason: "branchPollChanged"
             )
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if previousState?.branch != nextState?.branch,
nextState?.branch != nil {
scheduleWorkspaceGitMetadataRefreshIfPossible(
workspaceId: probeKey.workspaceId,
panelId: probeKey.panelId,
reason: "branchPollChanged"
)
}
if previousState?.branch != nextState?.branch {
scheduleWorkspaceGitMetadataRefreshIfPossible(
workspaceId: probeKey.workspaceId,
panelId: probeKey.panelId,
reason: "branchPollChanged"
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TabManager.swift` around lines 1691 - 1698, The current guard only
schedules a metadata refresh when the branch changed and the new branch is
non-nil, so if the branch becomes nil (detached HEAD) we skip refresh; update
the condition in the block that calls
scheduleWorkspaceGitMetadataRefreshIfPossible (the one using previousState,
nextState and probeKey) to trigger whenever previousState?.branch !=
nextState?.branch (remove the nextState?.branch != nil check) so the refresh
runs both when a branch changes and when it becomes nil; keep the same
parameters (workspaceId: probeKey.workspaceId, panelId: probeKey.panelId,
reason: "branchPollChanged").

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview — 977f7fd9 Deployed Mar 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sidebar does not update branch name when agent checks out a new branch

3 participants