Skip to content

cmux open: defer smart branch-base resolution past diff viewer first paint - #7605

Closed
lawrencecchen wants to merge 6 commits into
mainfrom
feat-diff-viewer-fast-first-paint
Closed

lawrencecchen wants to merge 6 commits into
mainfrom
feat-diff-viewer-fast-first-paint

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The diff viewer's initial "Loading diff: Unstaged" placeholder took 10-30s+ to be replaced. Before the selected diff was computed, writeCompleteGitDiffViewerHTMLSet (1) resolved a smart branch base for the selected repo AND every sibling repo candidate (capped at 12), each shelling out to gh pr view (~0.8s online, 4s timeout offline) serially, and (2) wrote ~60 placeholder pages (4 sources x 12 repos + base candidates), re-verifying the whole webview asset bundle per page (a getattrlist sweep over ~100 files each). In a checkout whose worktrees/ dir has 1000+ sibling repos the 12-candidate cap is always hit.

Fix

Pre-redirect work is now only: git-only option lists, the opening + selected placeholder, the selected diff itself, and a manifest listing only files that exist. Branch pages carry resolveBranchBaseOnComplete and resolve their smart base (cmuxBase -> PR base -> upstream -> origin/HEAD, or explicit --base as manual/high) at deferred-page completion, after the opening page has redirected; only a selected .branch page resolves its own repo's base up front. Non-selected placeholders are written after the redirect, then completed as before. ensureDiffViewerAssets is memoized per (source bundle, target dir) behind os_unfair_lock. The manifest loader skips a listed-but-missing local html page (that entry 404s) instead of failing the whole token, and deferred manifest refreshes use the flock-merge helper.

Deferred-page completion support moved to CLI/CMUXCLI+DiffViewerDeferredPages.swift (swift file length budget); first-paint regression tests live in cmuxTests/CMUXOpenDiffViewerFirstPaintTests.swift.

Measured on the 1134-sibling-worktree repro (warm): first paint 24-32s before -> 1.0s after (placeholder at 0.27s). The remaining sources/repos/bases complete in the background after the diff is visible.

Two stale assertions in testDiffCommandGeneratesCodeViewAndOpensBrowserSplit are updated to pin current main behavior (the fda9078 no-path= stdout contract and content-keyed asset dirs); they were failing on main-shaped code already, indicating the tests job hadn't been exercising them.

Test structure

Commit 1 adds only the regression test (red), commit 2 the gh deferral (green for that test). The first-paint test also asserts the selected page serves HTTP 200 through the viewer server at redirect time while sibling pages are still pending, and that the fake gh log is empty at first paint.


Note

Medium Risk
Touches the diff viewer open path, HTTP manifest allow-listing, and git/gh branch-base heurics; behavior changes are covered by new first-paint regression tests but the surface area is user-visible and CLI-critical.

Overview
cmux diff no longer blocks first paint on smart branch-base resolution (gh / per-sibling-repo work) or on writing every sibling placeholder up front.

Pre-redirect work is narrowed to git option lists, the opening/selected path, the selected diff, and an HTTP manifest that only lists files that already exist. Branch pages get resolveBranchBaseOnComplete so smart bases resolve during deferred completion (with a per-repo cache); only the selected branch source resolves its base before redirect when applicable. Placeholders for other sources/repos are written after redirect via writeDeferredDiffViewerPlaceholder, with manifest refreshes through refreshDeferredDiffViewerHTTPManifest. ensureDiffViewerAssets is memoized under a lock; manifest loading tolerates listed-but-missing HTML pages instead of failing the whole token.

Deferred helpers move to CMUXCLI+DiffViewerDeferredPages.swift (file-length budget). Tests split into CMUXOpenCommandTestSupport, CMUXOpenDiffViewerFirstPaintTests (no gh before first paint), and updated assertions for stdout and content-keyed asset paths.

Reviewed by Cursor Bugbot for commit 3cd6357. Bugbot is set up for automated code reviews on this repo. Configure here.

Regression test for the slow initial "Loading diff: Unstaged" state:
opening a git diff viewer with a non-branch selected source eagerly
resolved smart branch bases (gh pr view, up to 4s timeout) for the
selected repo and up to 12 sibling repo candidates before the selected
diff was computed. The fake gh on PATH logs every invocation; the test
asserts the log is still empty at the moment the opening page gains its
redirect.
The opening "Loading diff: <source>" page was only replaced after
writeCompleteGitDiffViewerHTMLSet resolved a smart branch base (each a
gh pr view network call, ~0.8s online / 4s timeout offline) for the
selected repo and every sibling repo candidate (up to 12), serially,
before the selected diff was even computed. In a checkout with many
sibling worktrees that blocked first paint for 10-45s.

Branch pages now carry resolveBranchBaseOnComplete and resolve their
smart base (cmuxBase -> PR base -> upstream -> origin/HEAD, or an
explicit --base as manual/high) at deferred-page completion time, after
the selected page is done and the opening page has redirected. Only a
selected .branch page still resolves its own repo's base up front, since
that base determines the rendered diff. Resolution is cached per repo
for the completion pass; picker payloads stay gated on the persisted
branch session, and a completed branch page still renders against the
exact ref its picker advertises.
@vercel

vercel Bot commented Jul 8, 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 Jul 8, 2026 7:36pm
cmux-staging Building Building Preview, Comment Jul 8, 2026 7:36pm

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The diff viewer now defers branch-base resolution and placeholder completion work until after first paint, while refreshing manifests and preparing completed pages from deferred state. Tests were updated for resolved asset URLs and a new regression covers the browser-open ordering.

Changes

Deferred diff viewer completion

Layer / File(s) Summary
Deferred state and resolution types
CLI/cmux_open.swift
Adds deferred branch-base resolution types, expanded deferred source/page state, and runtime-scoped asset memoization.
Conditional branch context and placeholder pages
CLI/cmux_open.swift
Adds conditional branch-base lookup for selected and fallback contexts, and builds deferred placeholder pages with branch picker data gated by completion state.
Deferred completion, manifest refresh, and fallbacks
CLI/cmux_open.swift
Adds deferred placeholder writes, branch-base resolution and picker reconstruction, prepared-page completion handling, empty-state/error writes, manifest refresh behavior, and relaxed manifest loading for missing HTML files.
Project wiring and regression coverage
cmux.xcodeproj/project.pbxproj, cmuxTests/CMUXOpenCommandTests.swift, cmuxTests/CMUXOpenDiffViewerFirstPaintTests.swift
Adds the new first-paint regression test, updates existing diff-viewer assertions to use resolved asset URLs, and wires the new Swift files into the Xcode project.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#7329: Both PRs modify branch diff-base resolution logic in CLI/cmux_open.swift, one restructuring the deferred resolution flow and the other adjusting related error handling.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Prod writeDiffViewerStatusHTML now sets pollForReplacement: true for deferred pages, emitting pendingReplacement; the viewer JS fetch-polls on that flag. Replace the poll/retry flow with an explicit completion signal (callback/notification/actor state) for deferred pages; keep polling only in tests.
Cmux No Ambient Global State ❌ Error CLI/cmux_open.swift adds private enum DiffViewerAssetsMemo with file-scope static mutable caches, which is ambient global runtime state. Move the asset memo/cache onto an owned injectable type or instance state instead of a file-scope static namespace.
Description check ⚠️ Warning The description covers the problem, fix, and testing, but it omits the required template sections and review-trigger/checklist content. Rewrite the PR body to use the repo template: Summary, Testing, Demo Video, Review Trigger, and Checklist sections.
✅ Passed checks (22 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed CMUXCLI is a plain struct; the new production helpers add no @MainActor/nonisolated/Sendable isolation changes, and the access-control edits are tests-only.
Cmux Browser Automation Off-Main ✅ Passed PR only changes diff-viewer code/tests; it doesn’t touch browser routing files or move any waiting browser command off-main. The only browser command in new tests is existing browser.open_split.
Cmux Expensive Synchronous Load ✅ Passed The PR only defers git branch-base work and writes placeholders; no new agent-history loader or large JSON/JSONL sync load was added to an interactive path.
Cmux Cache Substitution Correctness ✅ Passed The branch-base cache is per deferred completion pass; persisted HTML/session data is still built from fresh resolution, with sessionPersisted gating and cold-cache fallback.
Cmux No Hacky Sleeps ✅ Passed No covered non-Swift runtime changes were introduced; the only sleeps/polling are in test scaffolding, which the rule explicitly allows.
Cmux Algorithmic Complexity ✅ Passed The new deferred-page code uses cached per-repo resolution and only fixed-size loops (DiffSource.allCases, repoOptions≤12, branchBaseOptions≤4); no new scalable nested scans were introduced.
Cmux Swift Concurrency ✅ Passed Parent diff only changes DiffViewerAllowedFile visibility; no new DispatchQueue, Task, Combine, or completion-handler async patterns were introduced.
Cmux Swift @Concurrent ✅ Passed No Swift async/await functions, @concurrent annotations, @MainActor attributes, or nonisolated async work introduced in changed Swift files; all code uses synchronous throws functions.
Cmux Swift File And Package Boundaries ✅ Passed Focused 198-line helper extraction; existing 8k-line CLI file grew by ~248 lines, under the >250-line fail threshold, with no new mixed-responsibility oversized file.
Cmux Swiftpm Lockfiles ✅ Passed cmux.xcodeproj only adds source files; no SwiftPM package refs, .gitignore, or Package.resolved files changed.
Cmux Swift Logging ✅ Passed No new runtime logging was added; searches found no added print/NSLog/Logger use, and existing prints are user-facing CLI output or tests.
Cmux User-Facing Error Privacy ✅ Passed Feature diff adds deferred plumbing only; no new user-facing errors/output with vendor names, tokens, manifests, or raw upstream messages.
Cmux Full Internationalization ✅ Passed The only production diff is an access-level change on DiffViewerAllowedFile; no user-facing Swift text, localization keys, or xcstrings files were added or changed.
Cmux Swiftui State Layout ✅ Passed PASS: Full PR diff only touches CLI/helpers/tests; no SwiftUI views or state/layout anti-patterns, and no ObservableObject/@Published/GeometryReader/List/ForEach/store refs were found.
Cmux Architecture Rethink ✅ Passed Production code introduces no timing delays, locks, observers, or split lifecycle ownership. Deferred completion is synchronous with clear ownership. Per rules, test-only polling/waitUntil patterns...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No native-window code changed; the PR only touches CLI diff-viewer HTML logic and tests, with no NSWindow/WindowGroup or cmuxAuxiliaryWindowIdentifiers edits.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/config/test files; no logs, temp dirs, build output, DerivedData, caches, or other artifact paths appear.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No Swift files under Sources/** changed in this PR, so the production-source seam rule doesn’t apply; the new CLI helper has no test/debug markers.
Title check ✅ Passed The title is specific, concise, and matches the main change: deferring smart branch-base resolution until after first paint.
✨ 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 feat-diff-viewer-fast-first-paint

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.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR speeds up diff viewer first paint by deferring branch-base work. The main changes are:

  • Deferred branch pages resolve their smart base during page completion.
  • Non-selected placeholders and manifest entries are written after the initial redirect.
  • Diff viewer assets are memoized per source bundle and target directory.
  • Deferred-page completion helpers moved into a new Swift file.
  • First-paint coverage was added for the diff viewer.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
CLI/CMUXCLI+DiffViewerDeferredPages.swift Adds helpers for deferred branch-base resolution, placeholder pages, error pages, and empty-state pages.
CLI/cmux_open.swift Updates the diff viewer flow so only the selected page is prepared before redirect and deferred pages are completed later.
cmuxTests/CMUXOpenDiffViewerFirstPaintTests.swift Adds coverage for first-paint behavior and delayed branch-base resolution.

Reviews (4): Last reviewed commit: "tests: split CMUXOpenCommandTests shared..." | Re-trigger Greptile

Comment thread CLI/cmux_open.swift
_ page: DiffViewerDeferredSourcePage,
sourceSet: DiffViewerDeferredSourceSet
) throws -> DiffViewerDeferredCompletion {
let preparedPage = try preparedDeferredDiffViewerSourcePage(page, sourceSet: sourceSet)

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 Deferred Resolution Skips Error Page

When a deferred branch page resolves its base on completion and that repo has no usable PR base, upstream, or origin fallback, this try runs before the existing error/empty-state handling in the completion body. The loading page can be left polling forever instead of being replaced with the same inline error page that later diff failures use.

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/CMUXOpenCommandTests.swift`:
- Around line 1815-1997: The test in
testDiffCommandDoesNotResolveSmartBranchBaseBeforeUnstagedFirstPaint only checks
that ghLogURL stays empty before the first paint, but it never verifies that
deferred smart-branch-base resolution eventually runs. Update the test to add a
post-exit assertion after process.waitUntilExit() that confirms the gh
invocation happened (for example by checking ghLogURL is non-empty or that the
expected branch-resolution output was produced), using the existing helpers and
fixtures like ghLogURL, openedHTMLURLBox, and the diff command flow.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 78b1ce77-08c2-4b90-b17d-29f96e46d184

📥 Commits

Reviewing files that changed from the base of the PR and between 82a1106 and 3245c8b.

📒 Files selected for processing (2)
  • CLI/cmux_open.swift
  • cmuxTests/CMUXOpenCommandTests.swift

Comment thread cmuxTests/CMUXOpenCommandTests.swift Outdated
Comment on lines +1815 to +1997
func testDiffCommandDoesNotResolveSmartBranchBaseBeforeUnstagedFirstPaint() throws {
let cliPath = try bundledCLIPath()
let rootURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let repoURL = rootURL.appendingPathComponent("repo", isDirectory: true)
let fakeBinURL = rootURL.appendingPathComponent("bin", isDirectory: true)
let fakeGitURL = fakeBinURL.appendingPathComponent("git", isDirectory: false)
let fakeGHURL = fakeBinURL.appendingPathComponent("gh", isDirectory: false)
let diffStartedURL = rootURL.appendingPathComponent("diff-started", isDirectory: false)
let releaseDiffURL = rootURL.appendingPathComponent("release-diff", isDirectory: false)
let alternateStartedURL = rootURL.appendingPathComponent("alternate-started", isDirectory: false)
let releaseAlternateURL = rootURL.appendingPathComponent("release-alternate", isDirectory: false)
let ghLogURL = rootURL.appendingPathComponent("gh.log", isDirectory: false)
try FileManager.default.createDirectory(at: repoURL.appendingPathComponent(".git", isDirectory: true), withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: fakeBinURL, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: rootURL) }

try """
#!/bin/sh
if [ "${1:-}" = "-C" ]; then
shift 2
fi
if [ "${1:-}" = "rev-parse" ] && [ "${2:-}" = "--show-toplevel" ]; then
printf '%s\\n' "$CMUX_FAKE_GIT_REPO_ROOT"
exit 0
fi
if [ "${1:-}" = "rev-parse" ] && [ "${2:-}" = "--abbrev-ref" ]; then
printf 'feature/dvfast\\n'
exit 0
fi
if [ "${1:-}" = "rev-parse" ] && [ "${2:-}" = "--verify" ]; then
exit 0
fi
if [ "${1:-}" = "symbolic-ref" ]; then
printf 'origin/main\\n'
exit 0
fi
if [ "${1:-}" = "config" ]; then
exit 1
fi
if [ "${1:-}" = "merge-base" ]; then
printf 'mergebase\\n'
exit 0
fi
if [ "${1:-}" = "diff" ]; then
for arg in "$@"; do
if [ "$arg" = "--cached" ]; then
: > "$CMUX_FAKE_GIT_ALTERNATE_STARTED"
while [ ! -f "$CMUX_FAKE_GIT_RELEASE_ALTERNATE" ]; do
sleep 0.05
done
cat <<'PATCH'
diff --git a/staged.txt b/staged.txt
index 1111111..2222222 100644
--- a/staged.txt
+++ b/staged.txt
@@ -1 +1 @@
-old staged
+new staged
PATCH
exit 0
fi
done
: > "$CMUX_FAKE_GIT_STARTED"
while [ ! -f "$CMUX_FAKE_GIT_RELEASE" ]; do
sleep 0.05
done
cat <<'PATCH'
diff --git a/worktree.txt b/worktree.txt
index 1111111..2222222 100644
--- a/worktree.txt
+++ b/worktree.txt
@@ -1 +1 @@
-old line
+new line
PATCH
exit 0
fi
if [ "${1:-}" = "for-each-ref" ]; then
exit 0
fi
if [ "${1:-}" = "rev-list" ]; then
printf '0\\t1\\n'
exit 0
fi
exit 1
""".write(to: fakeGitURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fakeGitURL.path)

try """
#!/bin/sh
printf '%s\\n' "$*" >> "$CMUX_FAKE_GH_LOG"
printf 'main\\n'
exit 0
""".write(to: fakeGHURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fakeGHURL.path)

let socketPath = makeSocketPath("diff-gh")
let listenerFD = try bindUnixSocket(at: socketPath)
let state = MockSocketServerState()
let openedURLBox = AsyncValueBox<String?>(nil)
let openedHTMLURLBox = AsyncValueBox<URL?>(nil)
let openHandled = expectation(description: "browser opened before smart branch base lookup")
defer {
Darwin.close(listenerFD)
unlink(socketPath)
}

let serverClosed = startMockServer(listenerFD: listenerFD, state: state) { line in
guard let payload = Self.v2Payload(from: line),
let id = payload["id"] as? String,
let method = payload["method"] as? String,
method == "browser.open_split",
let params = payload["params"] as? [String: Any],
let rawURL = params["url"] as? String else {
return Self.v2Response(id: "unknown", ok: false, error: ["code": "unexpected"])
}
openedURLBox.set(rawURL)
if let htmlURL = Self.diffViewerHTMLFileURLFromHTTPManifest(for: rawURL) {
openedHTMLURLBox.set(htmlURL)
}
openHandled.fulfill()
return Self.v2Response(
id: id,
ok: true,
result: ["surface_id": "surface-id", "pane_id": "pane-id", "url": rawURL]
)
}

let process = Process()
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
var environment = ProcessInfo.processInfo.environment
environment["PATH"] = "\(fakeBinURL.path):\(environment["PATH"] ?? "")"
environment["CMUX_SOCKET_PATH"] = socketPath
environment["CMUX_CLI_SENTRY_DISABLED"] = "1"
environment["CMUX_CLAUDE_HOOK_SENTRY_DISABLED"] = "1"
environment["CMUX_FAKE_GIT_REPO_ROOT"] = repoURL.path
environment["CMUX_FAKE_GIT_STARTED"] = diffStartedURL.path
environment["CMUX_FAKE_GIT_RELEASE"] = releaseDiffURL.path
environment["CMUX_FAKE_GIT_ALTERNATE_STARTED"] = alternateStartedURL.path
environment["CMUX_FAKE_GIT_RELEASE_ALTERNATE"] = releaseAlternateURL.path
environment["CMUX_FAKE_GH_LOG"] = ghLogURL.path
process.executableURL = URL(fileURLWithPath: cliPath)
process.arguments = ["diff", "--unstaged", "--cwd", repoURL.path, "--title", "Fast first paint", "--no-focus"]
process.environment = environment
process.currentDirectoryURL = repoURL
process.standardInput = FileHandle.nullDevice
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
try process.run()
defer { terminateProcess(process) }

wait(for: [openHandled], timeout: 5)
XCTAssertNotNil(openedURLBox.get())
XCTAssertFalse(FileManager.default.fileExists(atPath: ghLogURL.path))
FileManager.default.createFile(atPath: releaseDiffURL.path, contents: Data())

let openingHTMLURL = try XCTUnwrap(openedHTMLURLBox.get())
XCTAssertTrue(waitUntil(timeout: 5) {
let html = (try? String(contentsOf: openingHTMLURL, encoding: .utf8)) ?? ""
return html.contains("data-cmux-diff-redirect=")
&& FileManager.default.fileExists(atPath: alternateStartedURL.path)
})
let ghLogAtFirstPaint = (try? String(contentsOf: ghLogURL, encoding: .utf8)) ?? ""
XCTAssertEqual(ghLogAtFirstPaint, "")

FileManager.default.createFile(atPath: releaseAlternateURL.path, contents: Data())
let finished = DispatchSemaphore(value: 0)
DispatchQueue.global(qos: .userInitiated).async {
process.waitUntilExit()
finished.signal()
}
XCTAssertEqual(finished.wait(timeout: .now() + 5), .success)
wait(for: [serverClosed], timeout: 5)

let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
XCTAssertEqual(process.terminationStatus, 0, stderr)
XCTAssertTrue(stdout.contains("OK surface=surface-id pane=pane-id"), stdout)
XCTAssertTrue(FileManager.default.fileExists(atPath: diffStartedURL.path))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test only proves absence, never proves the deferred resolution actually happens.

The test asserts gh.log is empty before/at the redirect (Lines 1970, 1980), which correctly guards the "no premature gh call" regression. But it never asserts that gh (or the branch-base resolution path) is eventually invoked once the process completes — the process is torn down without re-checking ghLogURL's contents. If deferred branch-base resolution silently regresses to a no-op (e.g., the completion pass is skipped entirely), this test would still pass, since it only checks an early-state absence.

Consider adding a final assertion after the process exits, e.g. confirming ghLogURL is non-empty (or the branch page picker data was eventually populated), to ensure the test also catches a "resolution never runs" regression, not just a "runs too early" regression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/CMUXOpenCommandTests.swift` around lines 1815 - 1997, The test in
testDiffCommandDoesNotResolveSmartBranchBaseBeforeUnstagedFirstPaint only checks
that ghLogURL stays empty before the first paint, but it never verifies that
deferred smart-branch-base resolution eventually runs. Update the test to add a
post-exit assertion after process.waitUntilExit() that confirms the gh
invocation happened (for example by checking ghLogURL is non-empty or that the
expected branch-resolution output was produced), using the existing helpers and
fixtures like ghLogURL, openedHTMLURLBox, and the diff command flow.

Round 1 removed the gh network calls from the pre-redirect path, but
profiling the built app on a checkout with 1134 sibling worktrees still
showed 7-24s to first paint: writeCompleteGitDiffViewerHTMLSet wrote
~60 placeholder pages (4 sources x 12 repos + base candidates) before
completing the selected page, and every page write re-verified the whole
webview asset bundle (a getattrlist sweep per page).

Pre-redirect work is now only: repo/source/base option lists (git-only),
the opening + selected placeholder, and a manifest listing just the
files that exist. Non-selected placeholders are written after the
opening page redirects, then completed as before. ensureDiffViewerAssets
is memoized per (source bundle, target dir) behind os_unfair_lock. The
manifest loader now skips a listed-but-missing local html page (404 for
that entry) instead of failing the whole token, and deferred manifest
refreshes go through the flock-merge helper so regenerate entries are
not dropped.

Deferred-page completion support moves to CMUXCLI+DiffViewerDeferredPages.swift
to satisfy the swift file length budget; first-paint regression tests
live in CMUXOpenDiffViewerFirstPaintTests.swift and now also assert the
selected page serves HTTP 200 at redirect time while sibling pages are
still pending. Two stale assertions in
testDiffCommandGeneratesCodeViewAndOpensBrowserSplit (pre-existing rot:
the fda9078 stdout contract and content-keyed asset dirs) are updated
to pin current main behavior.
… split

DiffViewerURLMapper became internal for the CMUXCLI+DiffViewerDeferredPages
split, so its allowedFile/allowedRemotePatchFile results can no longer be a
private type.

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/CMUXOpenDiffViewerFirstPaintTests.swift`:
- Around line 180-184: The current wait in CMUXOpenDiffViewerFirstPaintTests
around `selectedPageURL` and `alternateStartedURL` is using file absence as the
handoff signal, which can race and cause false timeouts. Update the `waitUntil`
in this test to wait only for the redirect via
`Self.diffViewerRedirectURL(from:)`, then verify the alternate diff start with a
separate positive assertion or explicit release gate instead of
`!FileManager.default.fileExists(atPath:)`.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 557842b8-6528-4863-885f-06362dda9384

📥 Commits

Reviewing files that changed from the base of the PR and between 3245c8b and 8d2c926.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • CLI/CMUXCLI+DiffViewerDeferredPages.swift
  • CLI/cmux_open.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CMUXOpenCommandTests.swift
  • cmuxTests/CMUXOpenDiffViewerFirstPaintTests.swift

Comment on lines +180 to +184
XCTAssertTrue(waitUntil(timeout: 5) {
let html = (try? String(contentsOf: openingHTMLURL, encoding: .utf8)) ?? ""
selectedPageURL = Self.diffViewerRedirectURL(from: html)
return selectedPageURL != nil && !FileManager.default.fileExists(atPath: alternateStartedURL.path)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant test and the sibling reference.
git ls-files cmuxTests/CMUXOpenDiffViewerFirstPaintTests.swift cmuxTests/CMUXOpenCommandTests.swift

echo '--- CMUXOpenDiffViewerFirstPaintTests.swift (around lines 150-230) ---'
sed -n '150,230p' cmuxTests/CMUXOpenDiffViewerFirstPaintTests.swift

echo '--- CMUXOpenCommandTests.swift (around the referenced lines) ---'
sed -n '1768,1805p' cmuxTests/CMUXOpenCommandTests.swift

Repository: manaflow-ai/cmux

Length of output: 6875


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the helper logic that creates the staged/alternate signals.
echo '--- CMUXOpenDiffViewerFirstPaintTests.swift (helpers and setup) ---'
sed -n '1,170p' cmuxTests/CMUXOpenDiffViewerFirstPaintTests.swift

echo '--- Search for the fake git env vars and alternate-start handling ---'
rg -n "CMUX_FAKE_GIT_(ALTERNATE_STARTED|RELEASE_ALTERNATE|STARTED|RELEASE)|alternateStartedURL|releaseAlternateURL|releaseDiffURL" cmuxTests Sources CLI Packages -g '!**/DerivedData/**'

Repository: manaflow-ai/cmux

Length of output: 11543


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the diff-viewer redirect and alternate-diff sequencing in production code.
rg -n "data-cmux-diff-redirect|diffViewerRedirect|alternate-started|release-alternate|release-diff|browser.open_split|gh" Sources CLI Packages cmuxTests -g '!**/DerivedData/**'

echo '--- waitUntil helper definition ---'
rg -n "func waitUntil|waitUntil\\(" cmuxTests Sources CLI Packages -g '!**/DerivedData/**'

Repository: manaflow-ai/cmux

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the production diff flow that writes the redirect and triggers the alternate diff.
rg -n "data-cmux-diff-redirect|pendingReplacement|browser.open_split|diff.*cached|git diff --cached|gh " Sources/Panels Sources -g '!**/DerivedData/**' | head -n 200

echo '--- likely diff flow around redirect ---'
# Try to locate the smallest file/section with the redirect logic.
fd -a ".*Diff.*swift|.*Open.*swift|.*Browser.*swift" Sources cmuxTests | sed -n '1,80p'

Repository: manaflow-ai/cmux

Length of output: 29283


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the smallest relevant production/test symbols, then read the exact slices.
echo '--- BrowserPanel.swift outline: diff-viewer related symbols ---'
ast-grep outline Sources/Panels/BrowserPanel.swift --view expanded | sed -n '1,220p'

echo '--- TerminalController.swift outline: browser.open_split handlers ---'
ast-grep outline Sources/TerminalController.swift --view expanded | sed -n '2220,2295p'

echo '--- BrowserPanel.swift around diff redirect / pending page handling ---'
sed -n '2320,2385p' Sources/Panels/BrowserPanel.swift

echo '--- TerminalController.swift around browser.open_split ---'
sed -n '2240,2278p' Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 20596


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the exact diff-viewer branch route and nearby flow.
sed -n '2110,2188p' Sources/Panels/BrowserPanel.swift

echo '---'
sed -n '2188,2260p' Sources/Panels/BrowserPanel.swift

echo '---'
sed -n '2390,2475p' Sources/Panels/BrowserPanel.swift

echo '--- v2BrowserOpenSplit implementation ---'
rg -n "func v2BrowserOpenSplit|v2BrowserOpenSplit\\(" Sources/TerminalController.swift Sources/Panels/BrowserPanel.swift
sed -n '2488,2535p' Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 12513


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '6601,6735p' Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 6899


Use a stable handoff signal here. The !alternateStartedURL.exists check can miss the brief window between the redirect appearing and the alternate diff starting, turning this into a timeout even when the ordering is correct. Wait on the redirect itself, then assert the alternate path with a separate positive signal or release gate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/CMUXOpenDiffViewerFirstPaintTests.swift` around lines 180 - 184,
The current wait in CMUXOpenDiffViewerFirstPaintTests around `selectedPageURL`
and `alternateStartedURL` is using file absence as the handoff signal, which can
race and cause false timeouts. Update the `waitUntil` in this test to wait only
for the redirect via `Self.diffViewerRedirectURL(from:)`, then verify the
alternate diff start with a separate positive assertion or explicit release gate
instead of `!FileManager.default.fileExists(atPath:)`.

…TestSupport

CMUXOpenCommandTests.swift is over the 900-line hard cap, so the +6 line
growth from the resolved-asset-URL assertions failed workflow-guard-tests.
Move the suite's shared process/socket/mock-server helpers into a new
CMUXOpenCommandTestSupport.swift extension file (wired into cmuxTests) and
ratchet the file's length budget down 2882 -> 2507.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Triaged Greptile's P2 (deferred resolution skips error page): not reproducible in the code path. Throws from preparedDeferredDiffViewerSourcePage (including base resolution with no PR base/upstream/origin fallback) are caught by both callers of completeDeferredDiffViewerSource — completeDeferredDiffViewerSelectedSource and the completeDeferredDiffViewerSources loop — which write the inline error page via writeDeferredDiffViewerError with pollForReplacement: false, so the loading page is replaced rather than left polling. The opening page additionally gets a redirect or error page in the completeDeferred catch in CLI/cmux_open.swift.

Also pushed 3cd6357: workflow-guard-tests failed on the 900-line hard cap for cmuxTests/CMUXOpenCommandTests.swift (any growth fails; the budget bump to 2882 could not satisfy it). Split the suite's shared process/socket/mock-server helpers into cmuxTests/CMUXOpenCommandTestSupport.swift (wired into cmuxTests) and ratcheted the budget entry down 2882 -> 2507.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3cd6357. Configure here.

Comment thread CLI/cmux_open.swift
) {
let message = diffViewerErrorMessage(error)
try? writeDiffViewerStatusHTML(
to: page.url,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deferred errors drop branch picker

Medium Severity

When deferred branch page completion fails after preparedDeferredDiffViewerSourcePage has resolved the smart base, writeDeferredDiffViewerError still receives the original DiffViewerDeferredSourcePage from the loop. That struct never gets updated branchPickerBase or branchBaseRef, so error HTML omits the branch picker and resolved base even though resolution already ran.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3cd6357. Configure here.

@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 – cmux — 3cd63572 Deployed Jul 8, 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.

2 participants