Skip to content

Fix browser panes stuck black after a failed discard-restore - #7533

Merged
austinywang merged 42 commits into
mainfrom
issue-7504-browser-discard-restore-black
Jul 9, 2026
Merged

austinywang merged 42 commits into
mainfrom
issue-7504-browser-discard-restore-black

Conversation

@austinywang

@austinywang austinywang commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7504

Summary

  • Keeps discarded browser-pane restore state alive until a real main-frame commit, main-frame download, or controlled no-navigation reactivation instead of clearing it before restore navigation runs.
  • Wires WebKit commit/fail/cancel/download/policy-cancel callbacks into one shared restoreDiscardedWebViewIfNeeded path, with queued-remote dedupe, stale-cancel protection, stall recovery, and reveal-only blank-shell healing.
  • Addresses the latest PR review comments: insecure-HTTP Cancel stays retryable, failed external opens no longer report .openedExternally, and delayed policy-prompt terminal completions still honor the restore attempt after WebKit's immediate provisional cancel.
  • Adds behavior coverage for manager retry, panel retry, queued remote restore dedupe, stale cancels, terminal policy outcomes, insecure-HTTP prompt cancel/open-failure/delayed-terminal cases, and heal/stall predicates.

Testing

  • git diff --check
  • ./scripts/lint-pbxproj-test-wiring.sh
  • python3 scripts/swift_file_length_budget.py (expected existing unrelated debt remains in Sources/DockSplitStore.swift and Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift; touched files stay within budget/hard-cap limits)
  • CI-style budget probe: python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv --base-ref "$BASE_REF" --merge-ref origin/main --merge-head HEAD (local git falls back from speculative merge-tree mode, budget respected)
  • xcodebuild -quiet -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-issue-7504-pr-comments-unit -only-testing:cmuxTests/BrowserDiscardRestorePolicyCancelTests/cancelledInsecureHTTPPromptKeepsDiscardRestoreRetryable -only-testing:cmuxTests/BrowserDiscardRestorePolicyCancelTests/failedInsecureHTTPExternalOpenDoesNotReportTerminalRestore -only-testing:cmuxTests/BrowserDiscardRestorePolicyCancelTests/terminalPolicyCompletionAfterProvisionalCancelCompletesDiscardRestore -only-testing:cmuxTests/BrowserDiscardRestorePolicyCancelTests/staleInsecureHTTPPromptDoesNotCompleteNewerRestore -only-testing:cmuxTests/BrowserDiscardRestorePolicyCancelTests/staleRestoreCancelDoesNotClearCurrentAttemptedRequest test
  • xcodebuild -quiet -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-issue-7504-pr-comments-unit -only-testing:cmuxTests/BrowserDiscardedWebViewRestoreRetryGreenTests/policyCancelledRestoreClearsDiscardStateInsteadOfReplaying -only-testing:cmuxTests/BrowserDiscardedWebViewRestoreRetryGreenTests/intentBrowserFallbackPolicyCancelStaysRetryableUntilFallbackCommits -only-testing:cmuxTests/BrowserDiscardedWebViewRestoreRetryGreenTests/unknownCancellationAfterClearedAttemptedURLKeepsRestoreRetryable test
  • ./scripts/reload.sh --tag issue-7504-pr-comments

Localization audit: no new user-facing strings were added in the latest review-fix commits. The insecure-HTTP open-failure path does not add copy; no Resources/Localizable.xcstrings changes were required.

Demo Video

  • Video URL or attachment: Not captured. This is a browser restore state-machine fix; verification is via focused restore tests and the tagged debug build above.

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Touches core WebKit navigation and memory-discard lifecycle for browser panels; behavior is heavily tested but regressions could affect tab restore, downloads, and external/insecure-HTTP flows.

Overview
Fixes black browser panes after memory discard when a restore navigation fails or never commits (#7504).

Discard manager no longer clears discard state when restoreIfNeeded runs the restore closure; it stays discarded until noteRestoreNavigationCommitted, reactivateWithoutNavigation, or noteRestoreNavigationDidNotCommit. Adds isRestoreNavigationPending, force reload to restart in-flight restores, and start/commit/did-not-commit hooks.

New BrowserDiscardRestoreHeal centralizes restoreDiscardedWebViewIfNeeded: stall detection, sticky Stop, queued remote dedupe, WKNavigation identity for stale cancel/fail callbacks, restore-attempt UUIDs, reveal-only blank-shell heal (skips error page, crash recovery, insecure HTTP), and about:blank reactivation without navigation.

BrowserNavigationDelegate passes WKNavigation? on fail/cancel, adds didCancelNavigationPolicy and didBecomeDownload with restore-attempt IDs; terminal policy cancels (external open, new tab, insecure HTTP) complete restore bookkeeping correctly.

BrowserPanel wires commits (non-about:blank), downloads, and policy outcomes into that path; stopLoading sets a flag so reveal cannot auto-reload; lifecycle payload adds restore_pending / has_committed_document. External navigation returns a typed result so failed opens are not treated as terminal.

Adds focused unit/integration tests and moves insecure-HTTP test hooks to BrowserInsecureHTTPAlertTestSupport.

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


Summary by cubic

Prevents black browser panes after a failed discard‑restore by keeping discard state until a real document commit, a main‑frame download, or a terminal, tokenized policy cancel, with stall detection and a reveal‑only blank‑shell heal that honors Stop and never heals over the browser error page (fixes #7504). Download callbacks are now scoped to the active restore attempt to avoid stale completions.

  • Bug Fixes
    • Discard manager: per‑attempt pending‑restore state; in‑flight restores deduped; explicit reloads can force‑restart; discard clears only on real commit, main‑frame download, or reactivation without navigation.
    • Browser panel: attributes fail/cancel to the matching WKNavigation and ignores stale callbacks; stalled restores clear tracked navigation on next touch; reveal‑only blank‑shell heal gated by user Stop, crash‑recovery, insecure‑HTTP consent, and active error page; about:blank/no‑URL restores reactivate in place; prewarmed adoption seeds the committed flag; queued remote restores treated as in‑flight until explicit reload; nil‑target tabs now complete terminal restore cleanly; adds restore_pending and has_committed_document diagnostics; restore/heal flow moved into BrowserDiscardRestoreHeal.
    • Navigation delegate and prompts: callbacks carry WKNavigation?; new didCancelNavigationPolicy reports typed, tokenized terminal cancels keyed to the restore attempt; didBecomeDownload reports main‑frame flag and restore‑attempt ID so download outcomes complete the correct attempt; preserves restore tokens across policy prompts; didBecomeDownload treats main‑frame downloads as committed outcomes; external navigation returns a typed result so browser‑fallback is not marked terminal; insecure‑HTTP prompts remain retryable and then complete or continue restores based on user choice.
    • CI: fixed PR comment guard regressions to keep staged macOS jobs running when unrelated web jobs skip.

Written for commit 348e3ee. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved hidden web view discard/restore retry flow, including stalled/failed restores, deduping, and correct cancellation vs forced reload behavior.
    • Refined restore bookkeeping so discard state finalizes after successful commit, including main-frame download outcomes.
    • Strengthened about:blank restore/heal behavior and updated blank/background rendering decisions to avoid unintended healing.
  • New Features
    • Added a navigation download callback and improved external/insecure-HTTP navigation handling with clearer typed outcomes and terminal-cancellation reporting.
  • Tests
    • Added extensive coverage for restore retry/state transitions, blank-heal predicates, and navigation cancellation/insecure-HTTP prompt flows.

austinywang and others added 2 commits July 7, 2026 13:13
…try (#7504)

A discarded browser webview whose restore navigation never commits
(connection refused, WebKit content-process death, dead localhost dev
server) permanently consumes its discard state, so every later reveal,
reload, or automation touch no-ops and the pane stays black forever.

Red tests only, per the two-commit regression policy:
- R1: manager-level — a restore whose navigation never starts/commits
  must leave the pane discarded and retryable.
- R2: panel end-to-end — connection-refused restore must leave the next
  restore touch able to retry.

CI on this commit is expected to fail these tests; the fix lands in the
next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the discard state armed until a restore navigation actually
commits, so failed restores retry on the next touch instead of leaving
the pane permanently black:

- BrowserHiddenWebViewDiscardManager: restoreIfNeeded no longer clears
  the discard state before navigating; new isRestoreNavigationPending
  state machine (noteRestoreNavigationStarted / Committed /
  DidNotCommit) driven by real navigation-delegate signals; in-flight
  restores dedupe instead of double-navigating;
  reactivateWithoutNavigation no longer consumes state without a
  commit.
- BrowserPanel: didCommit / didFailNavigation /
  didCancelProvisionalNavigation hooks drive the state machine;
  error-page commits do not clear the state; stall detection retries
  silently-dead restores on the next reveal/automation touch;
  blank-shell heal re-navigates a never-committed shell that still has
  a URL intent on reveal transitions (never on visibility heartbeats,
  and never while an insecure-HTTP consent alert is pending);
  restore_pending / has_committed_document diagnostics.
- BrowserDiscardRestoreHeal (new): pure, unit-testable predicates for
  heal and stall eligibility, plus relocated lifecycle diagnostics
  helpers to stay inside the BrowserPanel.swift length budget.
- Green tests for the new state machine and heal predicates.

Fixes #7504

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jul 9, 2026 9:41am
cmux-staging Building Building Preview, Comment Jul 9, 2026 9:41am

@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds discard/restore healing helpers, tracks restore-navigation state through discard and navigation lifecycles, refactors external and insecure-HTTP navigation resolution, and adds project wiring plus tests for restore retry and blank-shell behavior.

Changes

Discard/Restore Heal State Machine

Layer / File(s) Summary
BrowserDiscardRestoreHeal helpers
Sources/Panels/BrowserDiscardRestoreHeal.swift
Defines restore commit classification, restore orchestration, blank-shell predicates, restore-stall detection, lifecycle serialization, and background-drawing helpers.
Discard manager restore-pending state
Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
Adds isRestoreNavigationPending, changes restore finalization to wait for commit, and reorders reactivation to clear discard state after reactivation.
BrowserPanel and navigation restore flow
Sources/Panels/BrowserPanel.swift, Sources/Panels/BrowserNavigationDelegate.swift
Adds restore and blank-shell tracking fields, updates lifecycle payloads and visibility handling, wires restore retries through navigation callbacks and downloads, and refactors external and insecure-HTTP navigation handling.
Xcode project registration and restore-retry tests
cmux.xcodeproj/project.pbxproj, cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift, cmuxTests/BrowserDiscardRestoreHealPredicateTests.swift, cmuxTests/BrowserInsecureHTTPAlertTestSupport.swift
Adds project entries for the new Swift files and adds restore-retry and predicate tests covering stalled restores, blank-shell healing, cancellation paths, and pending-restore retry behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant BrowserPanel
  participant BrowserHiddenWebViewDiscardManager
  participant BrowserNavigationDelegate
  participant BrowserDiscardRestoreHeal
  BrowserPanel->>BrowserHiddenWebViewDiscardManager: noteRestoreNavigationStarted(reason)
  BrowserNavigationDelegate->>BrowserPanel: didBecomeDownload(...)
  BrowserPanel->>BrowserHiddenWebViewDiscardManager: noteRestoreNavigationCommitted(reason)
  BrowserPanel->>BrowserDiscardRestoreHeal: isRestoreStalled(...)
Loading

Possibly related PRs

  • manaflow-ai/cmux#4244: Modifies the same hidden webview discard/restore flow and lifecycle bookkeeping.
  • manaflow-ai/cmux#4245: Extends the same hidden WebView discard/restore machinery with additional policy and state handling.

Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swift File And Package Boundaries ❌ Error BrowserPanel is already 11k lines and this PR adds 334 lines there while shrinking it only 30, so the oversized-file extraction exception isn’t met. Move more restore/policy state-machine code out of BrowserPanel so the app file shrinks by >200 lines, or keep only thin AppKit glue there.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code changes and tests align with #7504 by preserving discard-recovery state until commit/download/reactivation and adding reveal retry coverage.
Out of Scope Changes check ✅ Passed The touched files are all tied to browser discard/restore flow, navigation handling, tests, and project wiring for the new files.
Cmux Swift Actor Isolation ✅ Passed All new production APIs stay MainActor-bound or are explicitly nonisolated pure helpers; no new unsafe Sendable or background UI access was introduced.
Cmux Swift Blocking Runtime ✅ Passed The head commit only adds a DEBUG guard in BrowserPanel; no new production waits/sleeps/semaphores/locks were introduced.
Cmux Browser Automation Off-Main ✅ Passed No browser.* socket routing changed; diff only touches restore/state helpers and tests, not TerminalController or ControlCommandExecutionPolicy.
Cmux Expensive Synchronous Load ✅ Passed No touched hunk adds/moves RestorableAgentSessionIndex.load or agent-history/transcript JSON parsing onto main-actor paths; the new restore code is navigation-only.
Cmux Cache Substitution Correctness ✅ Passed PASS: The PR keeps live webView/currentURL reads first and uses RestoredSessionHistory only as an event-driven fallback, with realignment before snapshotting; no unhandled cold/stale cache swap.
Cmux No Hacky Sleeps ✅ Passed PASS: Diff changes only Swift, pbxproj, and a TSV; no TS/JS/shell/runtime files or fixed sleep/polling additions appear in changed non-Swift code.
Cmux Algorithmic Complexity ✅ Passed No new nested scans, repeated filtering/sorting, or other scalable-collection hot-path regressions appear in the production diff.
Cmux Swift Concurrency ✅ Passed Only boundary callbacks/timers changed; no new background queues, Combine app-state, or fire-and-forget lifecycle Tasks were introduced.
Cmux Swift @Concurrent ✅ Passed No changed code adds invalid @concurrent/nonisolated async, and new async-style work explicitly hops to @MainActor or stays synchronous/pure.
Cmux Swiftpm Lockfiles ✅ Passed No Package.resolved, .gitignore, or SwiftPM package-reference changes appear in the diff; the pbxproj edit only adds source files.
Cmux Swift Logging ✅ Passed Full PR diff vs merge base shows no added or modified print/debugPrint/dump/NSLog/Logger calls in changed production Swift files; remaining NSLog sites are preexisting.
Cmux User-Facing Error Privacy ✅ Passed Only new user-facing copy is generic browser/insecure-HTTP text; no vendor names, raw upstream messages, or secrets appear in production changes.
Cmux Full Internationalization ✅ Passed No new production user-facing copy or locale data was added; changed Swift uses existing localized APIs, and no catalog/web locale files changed.
Cmux Swiftui State Layout ✅ Passed No new SwiftUI view/layout patterns were introduced; the changes are AppKit browser-state helpers and tests, with no GeometryReader, lazy-row store refs, or render-time mutation.
Cmux Architecture Rethink ✅ Passed Localized restore-state fix with clear owners/invariants; no runtime sleeps/polling or new side-channel ownership was introduced, and test-only sync is allowed.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The diff only changes BrowserPanel alert hooks and test-only fixtures; no new standalone NSWindow/NSPanel/WindowGroup or cmux.* identifier changes, and cmuxAuxiliaryWindowIdentifiers is untouched.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test/project files; no logs, caches, DerivedData, or scratch dirs appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test-only/debug seam was added under Sources; the only new DEBUG guards are logging/diagnostics, and test helpers live under cmuxTests.
Cmux No Ambient Global State ✅ Passed New restore logic stays on BrowserPanel/manager types; added top-level enums are case-bearing, and no new ambient mutable globals or runtime singletons were introduced.
Title check ✅ Passed The title clearly matches the main change: fixing browser panes stuck black after a failed discard-restore.
Description check ✅ Passed The PR description includes the required Summary, Testing, Demo Video, Review Trigger, and Checklist sections with substantive content.
✨ 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-7504-browser-discard-restore-black

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 7, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes #7504 by keeping isDiscardedForMemory alive through the entire restore navigation lifecycle — only noteRestoreNavigationCommitted (real document commit, main-frame download, or terminal policy cancel) clears it — so a failed restore leaves the pane retryable instead of permanently black. A new BrowserDiscardRestoreHeal.swift extension centralises the restore-touch path with stall detection, queued-remote dedup, stale WKNavigation matching, blank-shell heal gating, and about:blank reactivation-in-place.

  • State machine hardened: BrowserHiddenWebViewDiscardManager gains isRestoreNavigationPending and lifecycle note methods; restoreIfNeeded no longer calls clearDiscardState prematurely; explicit reload (force: true) can restart an in-flight pending restore.
  • WebKit callbacks extended: didFailNavigation and didCancelProvisionalNavigation now carry WKNavigation? for stale-cancel protection; new didCancelNavigationPolicy and didBecomeDownload callbacks deliver typed, attempt-scoped terminal outcomes for external-app, new-tab, checkout, and download paths.
  • Test seam migration: the three insecure-HTTP alert test hooks previously in a #if DEBUG Sources/ block are removed and re-hosted in cmuxTests/BrowserInsecureHTTPAlertTestSupport.swift via @testable import — the canonical pattern per cmux PR 6452.

Confidence Score: 5/5

Safe to merge — the restore state machine is well-reasoned, all previous review issues are addressed, and the change is thoroughly covered by focused unit tests.

All edge cases are guarded by attempt-scoped UUIDs and WKNavigation identity checks so stale completions cannot corrupt newer restore state. Test seam migration follows the canonical pattern. No correctness gaps identified.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Panels/BrowserDiscardRestoreHeal.swift New file centralising restore-touch logic with stall detection, queued-remote dedup, blank-shell heal gating, and fully unit-tested static predicates; ISO8601DateFormatter cached nonisolated(unsafe) for allocation-free lifecycle payloads.
Sources/Panels/BrowserHiddenWebViewDiscardManager.swift Adds isRestoreNavigationPending plus noteRestoreNavigationStarted/Committed/DidNotCommit; restoreIfNeeded no longer clears discard state prematurely; reactivateWithoutNavigation safely reordered.
Sources/Panels/BrowserNavigationDelegate.swift Adds WKNavigation? to fail/cancel callbacks for stale-cancel protection; new didCancelNavigationPolicy and didBecomeDownload deliver typed attempt-scoped terminal outcomes.
Sources/Panels/BrowserPanel.swift Adds hasCommittedDocumentSinceWebViewReplacement, userStoppedLoadSinceWebViewReplacement, pendingDiscardRestoreNavigation, currentDiscardRestoreAttemptID; private members widened to internal replacing removed #if DEBUG seams; handleInsecureHTTPAlertResponse gains openExternalURL injection for correct open-failure handling.
cmuxTests/BrowserInsecureHTTPAlertTestSupport.swift Test-target extension re-hosting the three insecure-HTTP alert hooks via @testable import — canonical pattern replacing the removed #if DEBUG Sources/ block.
cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift 498 lines covering manager retry, connection-refused panel retry, queued-remote dedup, force-restart, policy-cancel outcomes, download completion, and about:blank reactivation.
cmuxTests/BrowserDiscardRestoreHealPredicateTests.swift Pure-predicate coverage for all shouldHealBlankShell gates, isQueuedRemoteRestoreInFlight, isRestoreStalled, and stale/terminal prompt/cancel scenarios.
.github/swift-file-length-budget.tsv Updates BrowserNavigationDelegate.swift budget from 620 to 633 to reflect new callbacks.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Rev as Reveal/Reload
    participant BP as BrowserPanel
    participant Mgr as DiscardManager
    participant WK as WebKit
    participant ND as NavigationDelegate

    Note over Mgr: isDiscardedForMemory = true
    Rev->>BP: restoreDiscardedWebViewIfNeeded
    BP->>Mgr: restoreIfNeeded (isDiscardedForMemory stays true)
    BP->>WK: navigateWithoutInsecureHTTPPrompt
    Note over Mgr: isRestoreNavigationPending = true

    alt Commits
        WK-->>ND: didCommit
        ND->>BP: noteRestoreNavigationCommitted
        Note over Mgr: isDiscardedForMemory = false
    else Fails or cancelled
        WK-->>ND: didFail / didCancel (WKNavigation?)
        ND->>BP: noteRestoreNavigationDidNotCommit
        Note over Mgr: isDiscardedForMemory = true (retryable)
    else Terminal policy cancel
        ND->>BP: didCancelNavigationPolicy(.terminal)
        Note over Mgr: isDiscardedForMemory = false
    else Main-frame download
        WK-->>ND: didBecomeDownload
        ND->>BP: noteRestoreNavigationCommitted(download)
        Note over Mgr: isDiscardedForMemory = false
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Rev as Reveal/Reload
    participant BP as BrowserPanel
    participant Mgr as DiscardManager
    participant WK as WebKit
    participant ND as NavigationDelegate

    Note over Mgr: isDiscardedForMemory = true
    Rev->>BP: restoreDiscardedWebViewIfNeeded
    BP->>Mgr: restoreIfNeeded (isDiscardedForMemory stays true)
    BP->>WK: navigateWithoutInsecureHTTPPrompt
    Note over Mgr: isRestoreNavigationPending = true

    alt Commits
        WK-->>ND: didCommit
        ND->>BP: noteRestoreNavigationCommitted
        Note over Mgr: isDiscardedForMemory = false
    else Fails or cancelled
        WK-->>ND: didFail / didCancel (WKNavigation?)
        ND->>BP: noteRestoreNavigationDidNotCommit
        Note over Mgr: isDiscardedForMemory = true (retryable)
    else Terminal policy cancel
        ND->>BP: didCancelNavigationPolicy(.terminal)
        Note over Mgr: isDiscardedForMemory = false
    else Main-frame download
        WK-->>ND: didBecomeDownload
        ND->>BP: noteRestoreNavigationCommitted(download)
        Note over Mgr: isDiscardedForMemory = false
    end
Loading

Reviews (36): Last reviewed commit: "Fix PR comment CI guard regressions" | Re-trigger Greptile

Comment thread cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift
Comment thread cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift
@vercel
vercel Bot temporarily deployed to Preview – cmux July 7, 2026 20:34 Inactive
Comment thread Sources/Panels/BrowserPanel.swift Outdated

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Panels/BrowserPanel.swift (1)

3872-3889: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle restore commits that land on an error page (Sources/Panels/BrowserPanel.swift:3872-3889)

If activeErrorPageDisplayURL is set here, the restore never reaches a terminal state: hasCommittedDocumentSinceWebViewReplacement flips to true, but isRestoreNavigationPending stays stuck and isDiscardedForMemory never clears. Add noteDiscardedWebViewRestoreNavigationDidNotCommit(reason: "error_page") in the else branch so this webview can recover normally.

🤖 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 `@Sources/Panels/BrowserPanel.swift` around lines 3872 - 3889, The restore
commit path in navigationDelegate.didCommit leaves error-page restores stuck
because it only calls noteDiscardedWebViewRestoreNavigationCommitted when
activeErrorPageDisplayURL is nil. Update the didCommit closure in BrowserPanel
so the error-page case calls
noteDiscardedWebViewRestoreNavigationDidNotCommit(reason: "error_page") in the
else branch, allowing isRestoreNavigationPending and isDiscardedForMemory to
clear correctly while preserving the existing commit handling for normal
navigations.
🤖 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 `@Sources/Panels/BrowserDiscardRestoreHeal.swift`:
- Around line 3-65: The issue is that BrowserDiscardRestoreHeal is just a
stateless namespace, but it is unnecessarily declared as a caseless enum and
marked `@MainActor`. Move the pure boolean helpers into the existing BrowserPanel
extension as nonisolated static funcs, matching the pattern already used there,
so they no longer depend on main-actor isolation. Keep the formatter-backed
helpers (webViewLifecycleTimestamp and webViewHiddenDurationMilliseconds)
actor-isolated or otherwise protected, since they share the mutable
ISO8601DateFormatter instance.

In `@Sources/Panels/BrowserHiddenWebViewDiscardManager.swift`:
- Around line 260-274: The restore-navigation lifecycle methods in
BrowserHiddenWebViewDiscardManager are ignoring their reason parameter, so add
DEBUG-only cmuxDebugLog calls in noteRestoreNavigationStarted(reason:) and
noteRestoreNavigationDidNotCommit(reason:) to mirror the existing
noteSystemWillSleep/noteSystemDidWake logging. Include the passed reason in the
log message, and keep the current state changes in place so the transitions
remain diagnosable without changing behavior.

---

Outside diff comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 3872-3889: The restore commit path in navigationDelegate.didCommit
leaves error-page restores stuck because it only calls
noteDiscardedWebViewRestoreNavigationCommitted when activeErrorPageDisplayURL is
nil. Update the didCommit closure in BrowserPanel so the error-page case calls
noteDiscardedWebViewRestoreNavigationDidNotCommit(reason: "error_page") in the
else branch, allowing isRestoreNavigationPending and isDiscardedForMemory to
clear correctly while preserving the existing commit handling for normal
navigations.
🪄 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: a1f5242f-fe53-4de7-8c5a-59138e1b99ff

📥 Commits

Reviewing files that changed from the base of the PR and between 044896e and 17684f9.

📒 Files selected for processing (5)
  • Sources/Panels/BrowserDiscardRestoreHeal.swift
  • Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
  • Sources/Panels/BrowserPanel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift

Comment thread Sources/Panels/BrowserDiscardRestoreHeal.swift Outdated
Comment thread Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
Comment thread Sources/Panels/BrowserPanel.swift Outdated

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift (1)

87-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test connects to a fixed loopback port instead of using a local fake server.

browserPanelRetriesDiscardedRestoreAfterConnectionRefused navigates to http://127.0.0.1:1/... to force a connection-refused error. This relies on real OS-level socket behavior on a hardcoded port rather than a controlled local fixture, which can behave inconsistently across sandboxed CI/dev environments (firewall rules, IPv6/IPv4 resolution order, or restricted socket entitlements).

As per path instructions for {cmuxTests/**,...}: "Do not bind a fixed non-zero port or hit a live network host in tests; use a local fake or an ephemeral server instead." Consider binding an ephemeral local listener that immediately closes (guaranteeing ECONNREFUSED deterministically) instead of hardcoding port 1.

🤖 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/BrowserDiscardedWebViewRestoreRetryTests.swift` around lines 87 -
110, The test in BrowserDiscardedWebViewRestoreRetryTests should not rely on the
hardcoded loopback port to trigger connection refusal. Replace the fixed
http://127.0.0.1:1 URL in
browserPanelRetriesDiscardedRestoreAfterConnectionRefused with a controlled
local fake or ephemeral server fixture that deterministically produces
ECONNREFUSED, and keep the rest of the BrowserPanel discard/restore retry flow
unchanged. Use the existing test helper/setup patterns in cmuxTests to locate
the right place to introduce the ephemeral listener or fake server.

Source: Path instructions

🤖 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.

Outside diff comments:
In `@cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift`:
- Around line 87-110: The test in BrowserDiscardedWebViewRestoreRetryTests
should not rely on the hardcoded loopback port to trigger connection refusal.
Replace the fixed http://127.0.0.1:1 URL in
browserPanelRetriesDiscardedRestoreAfterConnectionRefused with a controlled
local fake or ephemeral server fixture that deterministically produces
ECONNREFUSED, and keep the rest of the BrowserPanel discard/restore retry flow
unchanged. Use the existing test helper/setup patterns in cmuxTests to locate
the right place to introduce the ephemeral listener or fake server.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b41d8c94-effb-47dc-907b-2fd57f3309cd

📥 Commits

Reviewing files that changed from the base of the PR and between 17684f9 and 1dcab64.

📒 Files selected for processing (6)
  • Sources/Panels/BrowserDiscardRestoreHeal.swift
  • Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
  • Sources/Panels/BrowserNavigationDelegate.swift
  • Sources/Panels/BrowserPanel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift

austinywang and others added 4 commits July 7, 2026 23:55
A navigation commit to about:blank (e.g. the placeholder document) must not
count as a successful discarded-webview restore; gate the restore-commit
bookkeeping on a real committed URL. Harden the retry test to wait for the
restore-pending flag to clear instead of only waiting for loading to settle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…scard-restore-black

# Conflicts:
#	cmux.xcodeproj/project.pbxproj
…ad callback

The discarded-restore fix adds a didBecomeDownload callback (property plus
two delegate call sites, +3 lines) to BrowserNavigationDelegate.swift.
Accept the growth in the checked-in budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
webViewLifecycleTimestamp and webViewHiddenDurationMilliseconds are pure
formatters and do not need MainActor isolation; align them with the
sibling nonisolated helpers in BrowserDiscardRestoreHeal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/Panels/BrowserPanel.swift Outdated

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift (1)

104-127: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Avoid fixed localhost port assumptions in this regression test.

Line 106 relies on 127.0.0.1:1 being connection-refused. That is environment-dependent; use a test-owned ephemeral local server/port and close it before the restore attempt, or inject a deterministic navigation failure path.

As per coding guidelines, tests must not hit a live network host or rely on a fixed non-zero port.

Source: Coding guidelines

Sources/Panels/BrowserPanel.swift (1)

3826-3842: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not mark ignored restore commits as committed documents.

Line 3830 flips hasCommittedDocumentSinceWebViewReplacement before the about:blank / error-page gate. If a pending discarded restore commits about:blank, restore_pending remains true, but later isRestoreStalled(... hasCommittedDocument: true) returns false, so the stale pending restore is never cleared/retried.

Proposed fix
-                self.hasCommittedDocumentSinceWebViewReplacement = true
+                let isRecoverableDocumentCommit = self.shouldTreatCommitAsDiscardedRestoreCommit(from: webView)
+                if isRecoverableDocumentCommit {
+                    self.hasCommittedDocumentSinceWebViewReplacement = true
+                }
                 // Reset playback tracking only once the new top-level document has
@@
-                if self.shouldTreatCommitAsDiscardedRestoreCommit(from: webView) {
+                if isRecoverableDocumentCommit {
                     self.noteDiscardedWebViewRestoreNavigationCommitted()
                 }

As per path instructions, correctness-critical restore lifecycle state should have one reliable state transition rather than disagreeing fallback flags.

🤖 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 `@Sources/Panels/BrowserPanel.swift` around lines 3826 - 3842, The
restore-commit path in navigationDelegate.didCommit is marking every commit as a
committed document too early, which breaks ignored about:blank/error-page
restore handling. Move the hasCommittedDocumentSinceWebViewReplacement update so
it only happens after the restore gate in didCommit, or explicitly avoid setting
it for discarded restore commits before calling
shouldTreatCommitAsDiscardedRestoreCommit, and keep the state transition
consistent with noteDiscardedWebViewRestoreNavigationCommitted and
isRestoreStalled.

Source: Path instructions

🤖 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.

Outside diff comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 3826-3842: The restore-commit path in navigationDelegate.didCommit
is marking every commit as a committed document too early, which breaks ignored
about:blank/error-page restore handling. Move the
hasCommittedDocumentSinceWebViewReplacement update so it only happens after the
restore gate in didCommit, or explicitly avoid setting it for discarded restore
commits before calling shouldTreatCommitAsDiscardedRestoreCommit, and keep the
state transition consistent with noteDiscardedWebViewRestoreNavigationCommitted
and isRestoreStalled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 906720b8-3c7d-4a74-bbe2-17e137e98b23

📥 Commits

Reviewing files that changed from the base of the PR and between 1dcab64 and 1e042d2.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (3)
  • Sources/Panels/BrowserPanel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift
💤 Files with no reviewable changes (1)
  • cmux.xcodeproj/project.pbxproj

Two review findings on the restore retry state machine:

- A main-frame download cleared discard state but never committed a
  document, so blank-shell healing re-navigated to the download URL on
  every reveal, restarting the download. Treat a main-frame download as
  a committed terminal outcome for the replaced web view.

- A discarded pane whose restore URL is nil or about:blank navigated (or
  skipped navigating) into a state whose commit is intentionally ignored,
  leaving the manager marked discarded (or restore-pending) forever and
  blocking future discards. Reactivate such panes in place through the
  existing reactivateWithoutNavigation path.

Widen navigationDelegate to internal so the download regression test can
drive the didBecomeDownload callback via @testable import, and raise the
test settle timeout for loaded CI hosts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 2

🤖 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/BrowserDiscardedWebViewRestoreRetryTests.swift`:
- Line 423: The assertion in BrowserDiscardedWebViewRestoreRetryTests should use
a direct Swift type check instead of a cast check to satisfy SwiftLint’s
prefer_type_checking rule. Update the expectation around
payload["discard_blockers"] to verify the value with an is-type check rather
than as? [String], keeping the same test intent while aligning with the lint
rule.

In `@Sources/Panels/BrowserPanel.swift`:
- Line 2994: The BrowserPanel navigation delegate is currently publicly
writable, which lets unrelated code replace or clear it and disrupt
restore/navigation wiring. Update BrowserPanel’s navigationDelegate to keep the
setter private while still allowing tests to read it and invoke callbacks, and
preserve the existing delegate lifecycle behavior without introducing any new
state or test-only seams.
🪄 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: 930d938d-5c42-480d-ab85-eac66a11b6bd

📥 Commits

Reviewing files that changed from the base of the PR and between 1e042d2 and 6394ab3.

📒 Files selected for processing (3)
  • Sources/Panels/BrowserDiscardRestoreHeal.swift
  • Sources/Panels/BrowserPanel.swift
  • cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift

Comment thread cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
…cate

A prewarmed webview is only claimable after its load finished, but the
commit happened under the pool's delegate, so the panel's
hasCommittedDocumentSinceWebViewReplacement stayed false and blank-shell
healing reloaded the adopted page on first reveal. Seed the flag at
adoption.

Move shouldTreatCommitAsDiscardedRestoreCommit next to its sibling
restore-heal predicates in BrowserDiscardRestoreHeal.swift and refresh
the BrowserPanel.swift length budget for the net restore-retry growth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/Panels/BrowserPanel.swift Outdated
austinywang and others added 3 commits July 8, 2026 01:05
A webview replaced after WebContent process termination waits for the
user's explicit Reload (hasRecoverableWebContentTermination). The
blank-shell heal predicate did not know about that gate, so a hidden
crashed pane would auto-navigate on the next reveal, clear the recovery
overlay, and could re-enter the crash loop. Add the recovery flag to
shouldHealBlankShell and cover it in the predicate tests.

Also move the no-restorable-URL restore fallback into
BrowserDiscardRestoreHeal so BrowserPanel.swift stays below its pre-PR
length (the guard job's hard cap forbids any growth of files over 900
lines), and drop the now-unneeded budget bump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
webViewLifecycleTopPayload runs on the polled debug-socket/top path for
every browser panel; allocate the documented-thread-safe formatter once
instead of per timestamp field, matching CmuxEventBus and Workspace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/Panels/BrowserPanel.swift
@austinywang

Copy link
Copy Markdown
Contributor Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 9, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@austinywang I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,631 of the 240,000 allowed lines of code this month. Reviews resume on 1 August 2026 (in 23 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

@austinywang I'll review the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@austinywang

Copy link
Copy Markdown
Contributor Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 9, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@austinywang I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,631 of the 240,000 allowed lines of code this month. Reviews resume on 1 August 2026 (in 23 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

@austinywang I'll review the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@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 3042ee2. Configure here.

Comment thread Sources/Panels/BrowserNavigationDelegate.swift
@austinywang

Copy link
Copy Markdown
Contributor Author

Review comments addressed and branch updated at 74d3507.

@coderabbitai resume
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review comments addressed and branch updated at 74d3507.

@coderabbitai resume
@coderabbitai review
@greptile-apps review
...

@austinywang I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,631 of the 240,000 allowed lines of code this month. Reviews resume on 1 August 2026 (in 23 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

@austinywang Got it — resuming automatic reviews and kicking off a fresh review of the updated branch (74d3507).

✅ Action performed

Reviews resumed.


Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@austinywang
austinywang enabled auto-merge (squash) July 9, 2026 06:41

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift (1)

364-366: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove dead conditional block after assertion.

Line 361 asserts terminalCancellationCount == 0, so the if terminalCancellationCount > 0 block at lines 364-366 is unreachable in the passing case. Since Swift Testing's #expect continues execution after failure, a failed assertion here would cause the if block to execute and fire a terminal cancellation simulation, producing confusing downstream assertion failures that mask the root cause.

🧹 Remove dead conditional
         `#expect`(terminalCancellationCount == 0)
         `#expect`(fallbackRequest?.url == fallbackURL)
 
-        if terminalCancellationCount > 0 {
-            panel.navigationDelegate?.didCancelNavigationPolicy?(panel.webView, .terminal(restoreAttemptID: panel.currentDiscardRestoreAttemptID))
-        }
         panel.navigationDelegate?.didCancelProvisionalNavigation?(panel.webView, nil)
🤖 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/BrowserDiscardedWebViewRestoreRetryTests.swift` around lines 364 -
366, Remove the dead conditional in BrowserDiscardedWebViewRestoreRetryTests
around the terminal cancellation simulation: the preceding assertion on
terminalCancellationCount already makes the `if terminalCancellationCount > 0`
branch unreachable in the passing case, and with `#expect` a failing assertion
should not trigger `panel.navigationDelegate?.didCancelNavigationPolicy` using
`panel.webView` and `panel.currentDiscardRestoreAttemptID`. Delete that block so
the test stops emitting misleading follow-on failures and keeps the focus on the
original assertion.
🤖 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.

Outside diff comments:
In `@cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift`:
- Around line 364-366: Remove the dead conditional in
BrowserDiscardedWebViewRestoreRetryTests around the terminal cancellation
simulation: the preceding assertion on terminalCancellationCount already makes
the `if terminalCancellationCount > 0` branch unreachable in the passing case,
and with `#expect` a failing assertion should not trigger
`panel.navigationDelegate?.didCancelNavigationPolicy` using `panel.webView` and
`panel.currentDiscardRestoreAttemptID`. Delete that block so the test stops
emitting misleading follow-on failures and keeps the focus on the original
assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 03fb95bd-88e4-4f1a-9959-4b468803c407

📥 Commits

Reviewing files that changed from the base of the PR and between 3042ee2 and 74d3507.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • Sources/Panels/BrowserNavigationDelegate.swift
  • Sources/Panels/BrowserPanel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserDiscardRestoreHealPredicateTests.swift
  • cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/Panels/BrowserPanel.swift (1)

3801-3808: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Remove the unused cancellation hook. BrowserNavigationDelegate.didCancelNavigationPolicy is never called, and terminal cancellation already flows through terminalPolicyCancellationReporter. Drop didCancelNavigationPolicy and PolicyCancellationKind so this state has one path.

🤖 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 `@Sources/Panels/BrowserPanel.swift` around lines 3801 - 3808, Remove the
unused navigation cancellation hook by deleting
BrowserNavigationDelegate.didCancelNavigationPolicy and the related
PolicyCancellationKind handling from BrowserPanel and its delegate setup. Keep
the terminal cancellation behavior routed through
terminalPolicyCancellationReporter and
noteDiscardedWebViewRestoreNavigationTerminallyCancelled, so
BrowserNavigationDelegate has a single cancellation path and no dead state
remains.

Source: Path instructions

cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift (1)

442-452: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Test hits a live loopback host/port instead of a local fake or ephemeral server.

mainFrameDownloadCompletesRestoreAndSuppressesBlankShellHeal creates a real (isRemoteWorkspace: false) BrowserPanel with initialURL set to a fixed http://127.0.0.1:1/... address and waits for it to settle. As per path instructions, cmuxTests must "not bind a fixed non-zero port or hit a live network host in tests; use a local fake or an ephemeral server instead."

In practice this is likely reliable (port 1 is essentially never bound), but it's a direct match for the forbidden pattern and could become flaky if the sandbox/CI environment restricts loopback connections differently.

🤖 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/BrowserDiscardedWebViewRestoreRetryTests.swift` around lines 442 -
452, `mainFrameDownloadCompletesRestoreAndSuppressesBlankShellHeal` is using a
fixed loopback URL with a non-ephemeral port, which violates the test networking
rule. Update the test to avoid `URL(string: "http://127.0.0.1:1/...")` and
instead drive `BrowserPanel` through a local fake server or an ephemeral server
fixture, keeping the existing
`BrowserPanel`/`waitForDiscardRestoreRetryWebViewToSettle` flow intact.

Source: Path instructions

🤖 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.

Outside diff comments:
In `@cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift`:
- Around line 442-452:
`mainFrameDownloadCompletesRestoreAndSuppressesBlankShellHeal` is using a fixed
loopback URL with a non-ephemeral port, which violates the test networking rule.
Update the test to avoid `URL(string: "http://127.0.0.1:1/...")` and instead
drive `BrowserPanel` through a local fake server or an ephemeral server fixture,
keeping the existing `BrowserPanel`/`waitForDiscardRestoreRetryWebViewToSettle`
flow intact.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 3801-3808: Remove the unused navigation cancellation hook by
deleting BrowserNavigationDelegate.didCancelNavigationPolicy and the related
PolicyCancellationKind handling from BrowserPanel and its delegate setup. Keep
the terminal cancellation behavior routed through
terminalPolicyCancellationReporter and
noteDiscardedWebViewRestoreNavigationTerminallyCancelled, so
BrowserNavigationDelegate has a single cancellation path and no dead state
remains.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 03fb95bd-88e4-4f1a-9959-4b468803c407

📥 Commits

Reviewing files that changed from the base of the PR and between 3042ee2 and 74d3507.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • Sources/Panels/BrowserNavigationDelegate.swift
  • Sources/Panels/BrowserPanel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserDiscardRestoreHealPredicateTests.swift
  • cmuxTests/BrowserDiscardedWebViewRestoreRetryTests.swift

@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/BrowserInsecureHTTPAlertTestSupport.swift`:
- Around line 20-28: `resetInsecureHTTPAlertHooksForTesting()` is duplicating
the default `insecureHTTPAlertWindowProvider` fallback chain, so update it to
reuse the same production default source instead of hardcoding
`browserInteractiveModalHostWindow` and
`browserFallbackInteractiveModalHostWindow` again. Expose the default alert-hook
closures from `BrowserPanel` as a shared internal factory/constant (or
equivalent), then have `resetInsecureHTTPAlertHooksForTesting()` restore those
defaults directly so test resets always stay in sync with the production
initializer.
🪄 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: 9ec07293-44d2-4e84-91fc-7d912e6ead9d

📥 Commits

Reviewing files that changed from the base of the PR and between 74d3507 and 348e3ee.

📒 Files selected for processing (4)
  • Sources/Panels/BrowserPanel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserDiscardRestoreHealPredicateTests.swift
  • cmuxTests/BrowserInsecureHTTPAlertTestSupport.swift
💤 Files with no reviewable changes (1)
  • cmuxTests/BrowserDiscardRestoreHealPredicateTests.swift

Comment on lines +20 to +28
func resetInsecureHTTPAlertHooksForTesting() {
insecureHTTPAlertFactory = { NSAlert() }
insecureHTTPAlertWindowProvider = { [weak self] in
if let self, let window = browserInteractiveModalHostWindow(for: self.webView) {
return window
}
return browserFallbackInteractiveModalHostWindow()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: default-provider logic duplicated between production and test reset.

resetInsecureHTTPAlertHooksForTesting() re-implements the same window-provider fallback chain (browserInteractiveModalHostWindow → browserFallbackInteractiveModalHostWindow) that presumably already exists as insecureHTTPAlertWindowProvider's production default initializer. If that default changes in BrowserPanel.swift, this copy can silently drift, letting tests reset to a stale "default."

Consider having this function pull the default closures from a small internal factory/constant on BrowserPanel so both sites stay in sync.

🤖 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/BrowserInsecureHTTPAlertTestSupport.swift` around lines 20 - 28,
`resetInsecureHTTPAlertHooksForTesting()` is duplicating the default
`insecureHTTPAlertWindowProvider` fallback chain, so update it to reuse the same
production default source instead of hardcoding
`browserInteractiveModalHostWindow` and
`browserFallbackInteractiveModalHostWindow` again. Expose the default alert-hook
closures from `BrowserPanel` as a shared internal factory/constant (or
equivalent), then have `resetInsecureHTTPAlertHooksForTesting()` restore those
defaults directly so test resets always stay in sync with the production
initializer.

This branch was successfully deployed

1 active deployment
Preview – cmux — 348e3ee2 Deployed Jul 9, 2026 by vercel[bot]
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.

Browser panes stuck permanently black: discarded webview restore no-ops, webview stays about:blank while pane metadata keeps the real URL (0.64.17)

1 participant