Skip to content

Fix browser pane refreshes on split and resize churn - #1224

Merged
austinywang merged 3 commits into
mainfrom
issue-1223-cmd-d-browser-refresh
Mar 12, 2026
Merged

austinywang merged 3 commits into
mainfrom
issue-1223-cmd-d-browser-refresh

Conversation

@austinywang

@austinywang austinywang commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add regression tests covering portal-hosted browser resize and split-divider churn
  • keep geometry-only browser portal updates on a repaint path instead of the WebKit reattach path
  • preserve full presentation refreshes for reveal, rebind, and transient-recovery cases

Closes #1223.

Verification

  • built and launched with ./scripts/reload.sh --tag fix-1223-browser-refresh
  • did not run tests locally (repo policy)

Summary by cubic

Fixes browser pane flicker during split/resize churn by repainting on geometry-only changes and avoiding unnecessary WebKit reattach. Full refresh is preserved for reveal, sync attach, anchor, and transient recovery. Closes #1223.

  • Bug Fixes

    • Consolidates portal update classification and routes pure frame/bounds/webFrame changes to a repaint path via invalidateHostedWebViewGeometry.
    • Controls WebKit reattach with a reattachRenderingState flag; skips refresh while the inspector divider is dragged.
    • Preserves refresh for reveal, sync attach, anchor, and transient recovery; tests verify reattach on reveal and repaint-only on anchor and external split resizes.
  • Dependencies

    • Update actions/checkout to v6.0.2 and ensure PR head SHA is checked out in macOS compat CI.
    • Replace oven-sh/setup-bun with a pinned Bun install script (bun-v1.3.10) and set PATH.

Written for commit 27e598c. Summary will update on new commits.

Summary by CodeRabbit

  • Improvements

    • WebView updates now distinguish geometry-only vs full refresh, reducing unnecessary reattachments and improving resize performance.
  • Tests

    • Added tests that verify resize paths repaint without forcing full WebView reattachment.
  • Chores

    • CI workflow checkout and tool-install steps updated for build consistency.

@vercel

vercel Bot commented Mar 12, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 12, 2026 4:14am

@coderabbitai

coderabbitai Bot commented Mar 12, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Refactors hosted WebView update flow to classify updates as none, geometryOnly, or refresh; adds a geometry-only invalidation path and a reattachRenderingState flag to refresh passes. Tests add counters/hooks to verify resizes repaint without forcing WebKit reattachment. CI checkout/install steps updated.

Changes

Cohort / File(s) Summary
BrowserWindowPortal core
Sources/BrowserWindowPortal.swift
Add HostedWebViewPresentationUpdateKind (none, geometryOnly, refresh); compute kind from refresh reasons and switch to invalidateHostedWebViewGeometry(...) for geometry-only cases or refreshHostedWebViewPresentation(...) for full refresh. Add invalidateHostedWebViewGeometry helper and reattachRenderingState: Bool parameter to runHostedWebViewRefreshPass(...), propagate flag through immediate/async/delayed phases and include consolidated reason strings.
Tests — tracking & assertions
cmuxTests/CmuxWebViewKeyEquivalentTests.swift, cmuxTests/BrowserWindowPortalLifecycleTests.swift
Add private(set) var reattachRenderingStateCount to TrackingPortalWebView and two @objc test hooks (cmuxUnitTestEnterInWindow, cmuxUnitTestEndDeferringViewInWindowChangesSync) that increment it. Add/extend tests asserting resize paths cause repaint (displayIfNeeded increments) but do not increment reattach counter.
CI workflows
.github/workflows/ci-macos-compat.yml, .github/workflows/ci.yml
Update actions/checkout usage to v6 and set checkout ref; replace Setup Bun step with inline Install Bun shell step (install Bun v1.3.10 via curl and update PATH).

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I hopped through code with careful cheer,
Geometry nudged — no reattach here.
We paint the frame, we skip the swap,
Quiet resizes — the WebView stays on top.
— A Rabbit in the Portal 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing browser pane refreshes during split and resize operations to prevent unnecessary churn.
Description check ✅ Passed The description includes all required sections: clear summary of changes and reasoning, verification method, and a detailed cubic-generated summary explaining the bug fix and dependencies.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-1223-cmd-d-browser-refresh

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 2 files

@greptile-apps

greptile-apps Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes excessive WebKit rendering reattaches that were firing on every browser-pane resize or split-divider drag by introducing a two-tier update path: geometry-only changes (frame, bounds, webFrame) now go through invalidateHostedWebViewGeometry, which flushes display without calling browserPortalReattachRenderingState, while semantically significant changes (reveal, bind, transient-recovery) continue to use the full refreshHostedWebViewPresentation path. Two new regression tests cover the anchor-resize and external-split-resize cases.

  • HostedWebViewPresentationUpdateKind.resolve classifies incoming refreshReasons arrays into .none, .geometryOnly, or .refresh; unknown reasons fall through to .refresh (safe conservative default, preserving correct behaviour for "rebind" and similar callers).
  • runHostedWebViewRefreshPass gains a reattachRenderingState: Bool parameter; the else branch currently duplicates three displayIfNeeded() calls that are identical to the if branch, which could silently diverge in future edits.
  • HostedWebViewPresentationUpdateKind.resolve parameter is named refreshReasons, matching the private static property Self.refreshReasons; Self. is required to disambiguate, which is easy to miss.
  • TrackingPortalWebView.viewDidUnhide increments reattachRenderingStateCount, but viewDidUnhide fires for any un-hide transition — not exclusively for the reattach path — making the counter potentially unreliable in future tests that exercise hide/show cycles independently.

Confidence Score: 4/5

  • This PR is safe to merge; the logic change is well-scoped and backed by regression tests, with only minor style concerns remaining.
  • The core fix correctly gates the expensive WebKit reattach path behind meaningful state-change reasons while preserving the full refresh for all semantic transitions. The fallthrough-to-.refresh default in resolve is appropriately conservative. The two regression tests directly cover the reported churn scenarios. Score is 4 rather than 5 because: the duplicated else block in runHostedWebViewRefreshPass is a latent maintenance hazard, and the viewDidUnhide hook in the test tracking class conflates view lifecycle with reattach tracking in a way that could mask issues in future tests.
  • Pay close attention to Sources/BrowserWindowPortal.swift — specifically the runHostedWebViewRefreshPass else branch and the resolve parameter naming.

Important Files Changed

Filename Overview
Sources/BrowserWindowPortal.swift Introduces HostedWebViewPresentationUpdateKind to distinguish geometry-only repaints from full WebKit reattach refreshes; wires the new path through invalidateHostedWebViewGeometry so split/resize churn avoids the expensive browserPortalReattachRenderingState call. Minor: the else branch in runHostedWebViewRefreshPass duplicates three display calls; parameter name refreshReasons shadows the static property of the same name inside resolve.
cmuxTests/CmuxWebViewKeyEquivalentTests.swift Adds reattachRenderingStateCount tracking to TrackingPortalWebView via two ObjC SPI overrides and viewDidUnhide; adds two regression tests confirming geometry-only updates skip the reattach path. viewDidUnhide could over-count in future tests that exercise hide/show cycles independently of reattach.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[sync detects changes\ncollects refreshReasons] --> B{reasons empty?}
    B -- yes --> Z[.none\nno update]
    B -- no --> C{any reason in\nrefreshReasons set?\nreveal / transientRecovery\nanchor / syncAttach*}
    C -- yes --> D[.refresh]
    C -- no --> E{all reasons subset of\ngeometryOnlyReasons?\nframe / bounds\nwebFrame / webFrameBottomDock}
    E -- yes --> F[.geometryOnly]
    E -- no --> G[.refresh\nfallthrough default]

    D --> H{hostedInspectorAdjusted\n&& !recoveredFromTransient?}
    H -- yes --> Z2[skip update]
    H -- no --> I[refreshHostedWebViewPresentation\nbrowserPortalReattachRenderingState ×3]

    F --> J[invalidateHostedWebViewGeometry\ndisplayIfNeeded only — no reattach]

    G --> H
Loading

Last reviewed commit: 7c29652

Comment on lines +2419 to +2428
if reattachRenderingState {
webView.browserPortalReattachRenderingState(reason: "\(reason):\(phase)")
containerView.displayIfNeeded()
webView.displayIfNeeded()
(webView.window ?? hostView.window)?.displayIfNeeded()
} else {
containerView.displayIfNeeded()
webView.displayIfNeeded()
(webView.window ?? hostView.window)?.displayIfNeeded()
}

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.

Redundant else branch, divergence risk

The else block contains the exact same three displayIfNeeded() calls as the end of the if block. The only meaningful difference between the two branches is the browserPortalReattachRenderingState call, so the display work can be lifted out of the conditional entirely. As written, any future addition to one branch (e.g. an extra flush or guard) must be duplicated in the other or a silent regression is introduced.

Suggested change
if reattachRenderingState {
webView.browserPortalReattachRenderingState(reason: "\(reason):\(phase)")
containerView.displayIfNeeded()
webView.displayIfNeeded()
(webView.window ?? hostView.window)?.displayIfNeeded()
} else {
containerView.displayIfNeeded()
webView.displayIfNeeded()
(webView.window ?? hostView.window)?.displayIfNeeded()
}
if reattachRenderingState {
webView.browserPortalReattachRenderingState(reason: "\(reason):\(phase)")
}
containerView.displayIfNeeded()
webView.displayIfNeeded()
(webView.window ?? hostView.window)?.displayIfNeeded()

Comment thread Sources/BrowserWindowPortal.swift Outdated
Comment on lines +2509 to +2512
static func resolve(refreshReasons: [String]) -> Self {
guard !refreshReasons.isEmpty else { return .none }
let reasonSet = Set(refreshReasons)
if !reasonSet.isDisjoint(with: Self.refreshReasons) {

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.

Parameter name shadows the static property of the same name

The parameter refreshReasons: [String] has the same name as the private static property refreshReasons: Set<String>. Inside this function, bare refreshReasons resolves to the parameter while Self.refreshReasons resolves to the static set, which is correct but makes the code harder to read at a glance — a reader who misses the Self. qualifier on line 2512 may think the set is being compared against itself. Renaming either the parameter or the static property would eliminate the ambiguity.

Suggested change
static func resolve(refreshReasons: [String]) -> Self {
guard !refreshReasons.isEmpty else { return .none }
let reasonSet = Set(refreshReasons)
if !reasonSet.isDisjoint(with: Self.refreshReasons) {
static func resolve(reasons: [String]) -> Self {
guard !reasons.isEmpty else { return .none }
let reasonSet = Set(reasons)
if !reasonSet.isDisjoint(with: Self.refreshReasons) {

Comment on lines +11451 to +11454
override func viewDidUnhide() {
reattachRenderingStateCount += 1
super.viewDidUnhide()
}

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.

viewDidUnhide fires for all un-hide transitions, not only reattach paths

viewDidUnhide is a standard NSView lifecycle callback that fires whenever the view transitions from hidden to visible — it is not exclusive to browserPortalReattachRenderingState. If a future test hides and then re-shows the WKWebView for an unrelated reason, reattachRenderingStateCount will be incremented even though no reattach occurred, producing a misleading failure (or false pass).

The two ObjC SPI hooks (_enterInWindow, _endDeferringViewInWindowChangesSync) are already precise proxies for the reattach path. Keeping viewDidUnhide separate from reattachRenderingStateCount (e.g. with its own unhideCount counter) would make the assertion semantics explicit and resilient against hide/show churn in future test scenarios.

@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

Caution

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

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

2099-2108: ⚠️ Potential issue | 🟠 Major

Avoid a second invalidate on the external-geometry hot path.

Line 2097 already drives each entry through synchronizeWebView(..., source: "externalGeometry"), and that path now calls invalidateHostedWebViewGeometry(...) itself when the reasons resolve to .geometryOnly. This extra loop adds another synchronous repaint/display pass for the same resize tick, which is exactly the churn path this PR is trying to make cheaper. Consider letting synchronizeWebView own the invalidate, or only running this fallback when the sync pass ended up with .none.

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

In `@Sources/BrowserWindowPortal.swift` around lines 2099 - 2108, The loop over
entriesByWebViewId is redundantly calling invalidateHostedWebViewGeometry after
synchronizeWebView already invalidates when the resolved reason is
.geometryOnly; remove the extra invalidate by letting synchronizeWebView(...,
source: "externalGeometry") own invalidation or only call
invalidateHostedWebViewGeometry from this loop when synchronizeWebView returned
.none; update the code around synchronizeWebView and the for-entry loop
(referencing synchronizeWebView, invalidateHostedWebViewGeometry,
entriesByWebViewId, entry.webView, entry.containerView) so a single invalidate
occurs per resize tick (either move invalidate into synchronizeWebView or
conditionalize this fallback to run only when sync reported .none).
🧹 Nitpick comments (1)
Sources/BrowserWindowPortal.swift (1)

2489-2519: Prefer typed presentation reasons over raw strings.

This classifier depends on string literals that are appended from several branches. A typo or a future geometry-only reason that is not added to geometryOnlyReasons will silently fall back to .refresh and reintroduce the WebKit reattach path. Carrying a small enum instead of [String] would make that regression compile-time visible.

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

In `@Sources/BrowserWindowPortal.swift` around lines 2489 - 2519, Replace the
string-based classifier with a typed enum to avoid silent fallbacks: introduce a
PresentationReason enum (cases like frame, bounds, webFrame, webFrameBottomDock,
syncAttachContainer, syncAttachWebView, reveal, transientRecovery, anchor) and
change HostedWebViewPresentationUpdateKind.resolve to accept
[PresentationReason] instead of [String]; update the internal sets
geometryOnlyReasons/refreshReasons to be Set<PresentationReason> and perform the
same subset/disjoint logic on the enum values; adjust all call sites that
currently append string reasons to convert to PresentationReason at the API
boundary (e.g., where strings are collected from WebKit/other branches) so the
rest of the code uses the typed enum and compilation will catch missing/typoed
reasons.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxTests/CmuxWebViewKeyEquivalentTests.swift`:
- Around line 11444-11464: The reattach probe lacks a positive-control: capture
the initial value of reattachRenderingStateCount on the test helper instance,
trigger the known full-refresh scenario by calling
testPortalRevealRefreshesHostedWebViewWithoutFrameDelta (or invoking the same
portal-reveal flow used there), and assert that reattachRenderingStateCount has
increased afterwards; update the test to reference the helper's
reattachRenderingStateCount and ensure the increment is observed (also apply the
same positive-control assertion pattern to the other similar tests around the
reattach probes that you noted).

---

Outside diff comments:
In `@Sources/BrowserWindowPortal.swift`:
- Around line 2099-2108: The loop over entriesByWebViewId is redundantly calling
invalidateHostedWebViewGeometry after synchronizeWebView already invalidates
when the resolved reason is .geometryOnly; remove the extra invalidate by
letting synchronizeWebView(..., source: "externalGeometry") own invalidation or
only call invalidateHostedWebViewGeometry from this loop when synchronizeWebView
returned .none; update the code around synchronizeWebView and the for-entry loop
(referencing synchronizeWebView, invalidateHostedWebViewGeometry,
entriesByWebViewId, entry.webView, entry.containerView) so a single invalidate
occurs per resize tick (either move invalidate into synchronizeWebView or
conditionalize this fallback to run only when sync reported .none).

---

Nitpick comments:
In `@Sources/BrowserWindowPortal.swift`:
- Around line 2489-2519: Replace the string-based classifier with a typed enum
to avoid silent fallbacks: introduce a PresentationReason enum (cases like
frame, bounds, webFrame, webFrameBottomDock, syncAttachContainer,
syncAttachWebView, reveal, transientRecovery, anchor) and change
HostedWebViewPresentationUpdateKind.resolve to accept [PresentationReason]
instead of [String]; update the internal sets geometryOnlyReasons/refreshReasons
to be Set<PresentationReason> and perform the same subset/disjoint logic on the
enum values; adjust all call sites that currently append string reasons to
convert to PresentationReason at the API boundary (e.g., where strings are
collected from WebKit/other branches) so the rest of the code uses the typed
enum and compilation will catch missing/typoed reasons.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0eaaecfb-24f2-48a9-9170-245d2722f5eb

📥 Commits

Reviewing files that changed from the base of the PR and between 89def5e and 7c29652.

📒 Files selected for processing (2)
  • Sources/BrowserWindowPortal.swift
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/ci.yml">

<violation number="1" location=".github/workflows/ci.yml:40">
P1: Avoid `curl | bash` for tool installation in CI. This runs an unverified remote script and weakens supply-chain safety compared with a pinned setup action.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread .github/workflows/ci.yml
Comment on lines +40 to +44
- name: Install Bun
run: |
set -euo pipefail
curl -fsSL https://bun.sh/install | bash -s -- bun-v1.3.10
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"

@cubic-dev-ai cubic-dev-ai Bot Mar 12, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Avoid curl | bash for tool installation in CI. This runs an unverified remote script and weakens supply-chain safety compared with a pinned setup action.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 40:

<comment>Avoid `curl | bash` for tool installation in CI. This runs an unverified remote script and weakens supply-chain safety compared with a pinned setup action.</comment>

<file context>
@@ -35,10 +35,13 @@ jobs:
 
-      - name: Setup Bun
-        uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2
+      - name: Install Bun
+        run: |
+          set -euo pipefail
</file context>
Suggested change
- name: Install Bun
run: |
set -euo pipefail
curl -fsSL https://bun.sh/install | bash -s -- bun-v1.3.10
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
- name: Setup Bun
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2
with:
bun-version: 1.3.10
Fix with Cubic

@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/BrowserWindowPortal.swift (1)

2091-2109: ⚠️ Potential issue | 🟠 Major

Avoid the second invalidate pass on external geometry changes.

Line 2097 already re-synchronizes every entry. For any entry that picks up frame, bounds, webFrame, or webFrameBottomDock, synchronizeWebView(...) now takes the .geometryOnly path and calls invalidateHostedWebViewGeometry(...) itself. The unconditional loop on Lines 2099-2108 then invalidates the same visible entry again, so live split/window resize does two repaint passes instead of one. Consider limiting this fallback loop to entries whose sync stayed .none, or have synchronizeWebView(...) report whether it already invalidated/refreshed.

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

In `@Sources/BrowserWindowPortal.swift` around lines 2091 - 2109,
synchronizeAllEntriesFromExternalGeometryChange is doing a second unconditional
invalidate pass over entries after calling
synchronizeAllWebViews(excluding:source:), causing duplicate repaints; change
the flow so the fallback loop only invalidates entries that were not already
refreshed by synchronizeWebView(...). Update synchronizeAllWebViews/existing
synchronizeWebView(...) to either return a Set of webView ids (or mark a
property on entriesByWebViewId entries) for which it performed a geometry
invalidation/refresh (or whose sync result != .none), then in
synchronizeAllEntriesFromExternalGeometryChange iterate entriesByWebViewId and
call invalidateHostedWebViewGeometry(...) only for entries not present in that
returned Set (or whose marked sync result is .none), referencing the functions
synchronizeAllEntriesFromExternalGeometryChange,
synchronizeAllWebViews(excluding:source:), synchronizeWebView(...),
invalidateHostedWebViewGeometry(...), and entriesByWebViewId to locate and
implement the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 40-44: Replace the current "Install Bun" step that pipes
https://bun.sh/install into bash with the official pinned GitHub Action; stop
using the curl | bash approach and instead use the oven-sh/setup-bun action
(e.g., oven-sh/setup-bun@v2.1.3) and pass the bun-version input set to "1.3.10"
so the workflow is immutable and auditable; update the step name if needed and
remove the echo to GITHUB_PATH since the action manages installation paths.

In `@cmuxTests/CmuxWebViewKeyEquivalentTests.swift`:
- Around line 11444-11459: The test currently overrides WKWebView lifecycle
selectors (displayIfNeeded, cmuxUnitTestEnterInWindow,
cmuxUnitTestEndDeferringViewInWindowChangesSync) and increments
reattachRenderingStateCount but does not forward to the original implementation;
change these hooks to wrap the real implementations instead of replacing them by
invoking super (or forwarding to the original IMP) after or before incrementing
reattachRenderingStateCount so the real WebKit lifecycle work still runs while
preserving the probe; locate the overrides in
cmuxTests/CmuxWebViewKeyEquivalentTests.swift (functions displayIfNeeded,
cmuxUnitTestEnterInWindow, cmuxUnitTestEndDeferringViewInWindowChangesSync) and
ensure each wrapper calls the original implementation and then increments the
counter (or increments then forwards if timing matters).

---

Outside diff comments:
In `@Sources/BrowserWindowPortal.swift`:
- Around line 2091-2109: synchronizeAllEntriesFromExternalGeometryChange is
doing a second unconditional invalidate pass over entries after calling
synchronizeAllWebViews(excluding:source:), causing duplicate repaints; change
the flow so the fallback loop only invalidates entries that were not already
refreshed by synchronizeWebView(...). Update synchronizeAllWebViews/existing
synchronizeWebView(...) to either return a Set of webView ids (or mark a
property on entriesByWebViewId entries) for which it performed a geometry
invalidation/refresh (or whose sync result != .none), then in
synchronizeAllEntriesFromExternalGeometryChange iterate entriesByWebViewId and
call invalidateHostedWebViewGeometry(...) only for entries not present in that
returned Set (or whose marked sync result is .none), referencing the functions
synchronizeAllEntriesFromExternalGeometryChange,
synchronizeAllWebViews(excluding:source:), synchronizeWebView(...),
invalidateHostedWebViewGeometry(...), and entriesByWebViewId to locate and
implement the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8398e3d2-63ea-4734-a746-f3b9c15e7137

📥 Commits

Reviewing files that changed from the base of the PR and between 7c29652 and 27e598c.

📒 Files selected for processing (4)
  • .github/workflows/ci-macos-compat.yml
  • .github/workflows/ci.yml
  • Sources/BrowserWindowPortal.swift
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift

Comment thread .github/workflows/ci.yml
Comment on lines +40 to +44
- name: Install Bun
run: |
set -euo pipefail
curl -fsSL https://bun.sh/install | bash -s -- bun-v1.3.10
echo "$HOME/.bun/bin" >> "$GITHUB_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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the workflow file to see current implementation and context
cat -n .github/workflows/ci.yml | head -60

Repository: manaflow-ai/cmux

Length of output: 2011


🏁 Script executed:

# Check if there's git history showing what was previously used for Bun setup
git log --oneline -20 .github/workflows/ci.yml

Repository: manaflow-ai/cmux

Length of output: 109


🏁 Script executed:

# Search for any Bun-related setup actions or references in the repo
rg -i "bun" --type yaml

Repository: manaflow-ai/cmux

Length of output: 122


🌐 Web query:

official Bun GitHub Action setup-bun version pinning

💡 Result:

To pin Bun’s version with the official GitHub Action (oven-sh/setup-bun), set bun-version (or point to a version file):

- uses: oven-sh/setup-bun@v2
  with:
    bun-version: "1.3.3"   # also supports "latest", "canary", "1.3.x"

[2]

Or:

- uses: oven-sh/setup-bun@v2
  with:
    bun-version-file: ".bun-version"  # also supports package.json / .tool-versions

[1]

If you don’t specify a version, the action will auto-detect from package.json (packageManager first, then engines.bun), otherwise it installs latest. [1]

To pin the action itself (recommended for reproducibility), use a specific tag (example latest shown in the repo UI as v2.1.3, released Mar 4, 2026):

- uses: oven-sh/setup-bun@v2.1.3
  with:
    bun-version: "1.3.3"

[1][3]

Sources: official action README [1], Bun docs CI/CD guide [2], action releases/tags [3].


Switch to the official pinned Bun action instead of piping an installer into bash.

The current implementation executes a mutable remote script at job time (https://bun.sh/install), which loses the immutability and auditability of a pinned action. Use the official oven-sh/setup-bun@v2.1.3 action with an explicit version instead:

- uses: oven-sh/setup-bun@v2.1.3
  with:
    bun-version: "1.3.10"

This provides commit-level immutability, reviewed/auditable code, and proper version pinning for both the action and the Bun runtime.

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

In @.github/workflows/ci.yml around lines 40 - 44, Replace the current "Install
Bun" step that pipes https://bun.sh/install into bash with the official pinned
GitHub Action; stop using the curl | bash approach and instead use the
oven-sh/setup-bun action (e.g., oven-sh/setup-bun@v2.1.3) and pass the
bun-version input set to "1.3.10" so the workflow is immutable and auditable;
update the step name if needed and remove the echo to GITHUB_PATH since the
action manages installation paths.

Comment on lines +11444 to +11459
private(set) var reattachRenderingStateCount = 0

override func displayIfNeeded() {
displayIfNeededCount += 1
super.displayIfNeeded()
}

@objc(_enterInWindow)
func cmuxUnitTestEnterInWindow() {
reattachRenderingStateCount += 1
}

@objc(_endDeferringViewInWindowChangesSync)
func cmuxUnitTestEndDeferringViewInWindowChangesSync() {
reattachRenderingStateCount += 1
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "TrackingPortalWebView hook definitions:"
sed -n '11442,11460p' cmuxTests/CmuxWebViewKeyEquivalentTests.swift

echo
echo "Search for any wrapper/swizzle that forwards these selectors:"
rg -n -C2 '_enterInWindow|_endDeferringViewInWindowChangesSync|method_exchangeImplementations|class_replaceMethod|class_getInstanceMethod|forwardInvocation|objc_msgSend' cmuxTests/CmuxWebViewKeyEquivalentTests.swift

Repository: manaflow-ai/cmux

Length of output: 1348


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Usage of reattachRenderingStateCount in tests ==="
rg -n 'reattachRenderingStateCount' cmuxTests/CmuxWebViewKeyEquivalentTests.swift

echo
echo "=== Context around the hook usage (test assertions) ==="
# Look for tests that use these counters
rg -n -B5 -A5 'reattachRenderingStateCount' cmuxTests/CmuxWebViewKeyEquivalentTests.swift | head -60

echo
echo "=== Check if there are comments explaining the hook design ==="
sed -n '11440,11465p' cmuxTests/CmuxWebViewKeyEquivalentTests.swift

Repository: manaflow-ai/cmux

Length of output: 3913


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full test showing the assertion context ==="
sed -n '11815,11850p' cmuxTests/CmuxWebViewKeyEquivalentTests.swift

echo
echo "=== Second test using reattachRenderingStateCount ==="
sed -n '11895,11930p' cmuxTests/CmuxWebViewKeyEquivalentTests.swift

echo
echo "=== Third test using reattachRenderingStateCount ==="
sed -n '12212,12245p' cmuxTests/CmuxWebViewKeyEquivalentTests.swift

echo
echo "=== Search for any tests that EXPECT the counter to increment ==="
rg -n -B3 'reattachRenderingStateCount.*+|XCTAssertGreater.*reattachRenderingStateCount|reattachRenderingStateCount.*initialReattachCount' cmuxTests/CmuxWebViewKeyEquivalentTests.swift

Repository: manaflow-ai/cmux

Length of output: 6828


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for _enterInWindow and _endDeferringViewInWindowChangesSync in production code ==="
rg -t swift '_enterInWindow|_endDeferringViewInWindowChangesSync' --type-list | head -20
rg '_enterInWindow|_endDeferringViewInWindowChangesSync' Sources/ 2>/dev/null || echo "No matches in Sources/"

echo
echo "=== Check if these are documented WebKit private methods ==="
echo "Checking WebKit documentation references or patterns..."
rg -n 'enterInWindow|endDeferringViewInWindowChangesSync' cmuxTests/ | grep -v 'cmuxUnitTest' || echo "Only test-specific references found"

echo
echo "=== Look for any other WKWebView subclasses in the codebase ==="
rg -n 'class.*WKWebView' Sources/ | head -20

Repository: manaflow-ai/cmux

Length of output: 560


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for _enterInWindow or _endDeferringViewInWindowChangesSync in production code ==="
rg -t swift '_enterInWindow|_endDeferringViewInWindowChangesSync' Sources/ 2>&1 || echo "No matches in Sources/"

echo
echo "=== Check if these selectors appear anywhere outside the test file ==="
rg '_enterInWindow|_endDeferringViewInWindowChangesSync' --glob '!CmuxWebViewKeyEquivalentTests.swift' 2>&1 || echo "Only in test file"

echo
echo "=== List all WKWebView subclasses in production ==="
rg -n 'class.*:\s*WKWebView' Sources/ -A 5 | head -40

echo
echo "=== Search for any method swizzling or ObjC runtime manipulation in tests ==="
rg -n 'method_setImplementation|method_exchangeImplementations|class_replaceMethod|class_addMethod|forwardingTargetForSelector' cmuxTests/ || echo "No swizzling found"

Repository: manaflow-ai/cmux

Length of output: 1290


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Context around _enterInWindow and _endDeferringViewInWindowChangesSync in BrowserWindowPortal.swift ==="
rg -n -B10 -A10 '_enterInWindow|_endDeferringViewInWindowChangesSync' Sources/BrowserWindowPortal.swift | head -100

Repository: manaflow-ai/cmux

Length of output: 902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Implementation of browserPortalCallVoidIfAvailable ==="
rg -n -B3 -A15 'func browserPortalCallVoidIfAvailable' Sources/BrowserWindowPortal.swift

echo
echo "=== Full context of browserPortalReattachRenderingState ==="
rg -n -B5 -A25 'func browserPortalReattachRenderingState' Sources/BrowserWindowPortal.swift

Repository: manaflow-ai/cmux

Length of output: 1837


Wrap these hooks instead of replacing the WebKit lifecycle methods.

These @objc methods override private WKWebView selectors without forwarding to the original implementation. When production code invokes browserPortalReattachRenderingState(), the test's hooks increment the counter but skip the real WebKit lifecycle work. The test measures invocation without validating behavior, which weakens regression coverage for the reattach path.

The hooks should call super or forward to the original IMP to preserve the real behavior while maintaining the probe.

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

In `@cmuxTests/CmuxWebViewKeyEquivalentTests.swift` around lines 11444 - 11459,
The test currently overrides WKWebView lifecycle selectors (displayIfNeeded,
cmuxUnitTestEnterInWindow, cmuxUnitTestEndDeferringViewInWindowChangesSync) and
increments reattachRenderingStateCount but does not forward to the original
implementation; change these hooks to wrap the real implementations instead of
replacing them by invoking super (or forwarding to the original IMP) after or
before incrementing reattachRenderingStateCount so the real WebKit lifecycle
work still runs while preserving the probe; locate the overrides in
cmuxTests/CmuxWebViewKeyEquivalentTests.swift (functions displayIfNeeded,
cmuxUnitTestEnterInWindow, cmuxUnitTestEndDeferringViewInWindowChangesSync) and
ensure each wrapper calls the original implementation and then increments the
counter (or increments then forwards if timing matters).

@austinywang
austinywang merged commit eef853c into main Mar 12, 2026
17 checks passed
austinywang added a commit that referenced this pull request Mar 12, 2026
Roll back actions/checkout v6.0.2 to v4, restore oven-sh/setup-bun
action, and remove explicit ref parameter from compat workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…-browser-refresh

Fix browser pane refreshes on split and resize churn
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
Roll back actions/checkout v6.0.2 to v4, restore oven-sh/setup-bun
action, and remove explicit ref parameter from compat workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 27e598ca Deployed Mar 12, 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.

Cmd+D / Cmd+Shift+D in terminal refreshes browser pane

1 participant