Repository navigation
Fix browser pane refreshes on split and resize churn #1224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ jobs: | |
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
|
|
||
| - name: Validate Depot runner guards | ||
| run: ./tests/test_ci_self_hosted_guard.sh | ||
|
|
@@ -35,10 +35,13 @@ jobs: | |
| working-directory: web | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
|
|
||
| - name: Setup Bun | ||
| uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2 | ||
| - 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" | ||
|
Comment on lines
+40
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Read the workflow file to see current implementation and context
cat -n .github/workflows/ci.yml | head -60Repository: 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.ymlRepository: 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 yamlRepository: manaflow-ai/cmux Length of output: 122 🌐 Web query:
💡 Result: To pin Bun’s version with the official GitHub Action ( - 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 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 ( - 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 |
||
|
|
||
| - name: Install dependencies | ||
| run: bun install --frozen-lockfile | ||
|
|
@@ -50,7 +53,7 @@ jobs: | |
| runs-on: macos-15 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| submodules: recursive | ||
|
|
||
|
|
@@ -157,7 +160,7 @@ jobs: | |
| runs-on: depot-macos-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| submodules: recursive | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2101,7 +2101,7 @@ final class WindowBrowserPortal: NSObject { | |||||||||||||||||||||||||||||||||
| let containerView = entry.containerView, | ||||||||||||||||||||||||||||||||||
| !containerView.isHidden else { continue } | ||||||||||||||||||||||||||||||||||
| guard webView.superview === containerView else { continue } | ||||||||||||||||||||||||||||||||||
| refreshHostedWebViewPresentation( | ||||||||||||||||||||||||||||||||||
| invalidateHostedWebViewGeometry( | ||||||||||||||||||||||||||||||||||
| webView, | ||||||||||||||||||||||||||||||||||
| in: containerView, | ||||||||||||||||||||||||||||||||||
| reason: "externalGeometry" | ||||||||||||||||||||||||||||||||||
|
|
@@ -2378,14 +2378,16 @@ final class WindowBrowserPortal: NSObject { | |||||||||||||||||||||||||||||||||
| _ webView: WKWebView, | ||||||||||||||||||||||||||||||||||
| in containerView: WindowBrowserSlotView, | ||||||||||||||||||||||||||||||||||
| reason: String, | ||||||||||||||||||||||||||||||||||
| phase: String | ||||||||||||||||||||||||||||||||||
| phase: String, | ||||||||||||||||||||||||||||||||||
| reattachRenderingState: Bool | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| guard !containerView.isHidden else { return } | ||||||||||||||||||||||||||||||||||
| guard !containerView.isHostedInspectorDividerDragActive else { | ||||||||||||||||||||||||||||||||||
| #if DEBUG | ||||||||||||||||||||||||||||||||||
| dlog( | ||||||||||||||||||||||||||||||||||
| "browser.portal.refresh.skip web=\(browserPortalDebugToken(webView)) " + | ||||||||||||||||||||||||||||||||||
| "container=\(browserPortalDebugToken(containerView)) reason=\(reason) phase=\(phase) drag=1" | ||||||||||||||||||||||||||||||||||
| "container=\(browserPortalDebugToken(containerView)) reason=\(reason) phase=\(phase) " + | ||||||||||||||||||||||||||||||||||
| "drag=1 reattach=\(reattachRenderingState ? 1 : 0)" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
|
|
@@ -2414,34 +2416,58 @@ final class WindowBrowserPortal: NSObject { | |||||||||||||||||||||||||||||||||
| scrollView.displayIfNeeded() | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| webView.layoutSubtreeIfNeeded() | ||||||||||||||||||||||||||||||||||
| webView.browserPortalReattachRenderingState(reason: "\(reason):\(phase)") | ||||||||||||||||||||||||||||||||||
| if reattachRenderingState { | ||||||||||||||||||||||||||||||||||
| webView.browserPortalReattachRenderingState(reason: "\(reason):\(phase)") | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+2419
to
+2421
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant else branch, divergence risk The
Suggested change
|
||||||||||||||||||||||||||||||||||
| containerView.displayIfNeeded() | ||||||||||||||||||||||||||||||||||
| webView.displayIfNeeded() | ||||||||||||||||||||||||||||||||||
| (webView.window ?? hostView.window)?.displayIfNeeded() | ||||||||||||||||||||||||||||||||||
| #if DEBUG | ||||||||||||||||||||||||||||||||||
| dlog( | ||||||||||||||||||||||||||||||||||
| "browser.portal.refresh web=\(browserPortalDebugToken(webView)) " + | ||||||||||||||||||||||||||||||||||
| "\(reattachRenderingState ? "browser.portal.refresh" : "browser.portal.invalidate") " + | ||||||||||||||||||||||||||||||||||
| "web=\(browserPortalDebugToken(webView)) " + | ||||||||||||||||||||||||||||||||||
| "container=\(browserPortalDebugToken(containerView)) reason=\(reason) " + | ||||||||||||||||||||||||||||||||||
| "phase=\(phase) frame=\(browserPortalDebugFrame(containerView.frame))" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private func invalidateHostedWebViewGeometry( | ||||||||||||||||||||||||||||||||||
| _ webView: WKWebView, | ||||||||||||||||||||||||||||||||||
| in containerView: WindowBrowserSlotView, | ||||||||||||||||||||||||||||||||||
| reason: String | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| runHostedWebViewRefreshPass( | ||||||||||||||||||||||||||||||||||
| webView, | ||||||||||||||||||||||||||||||||||
| in: containerView, | ||||||||||||||||||||||||||||||||||
| reason: reason, | ||||||||||||||||||||||||||||||||||
| phase: "geometry", | ||||||||||||||||||||||||||||||||||
| reattachRenderingState: false | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private func refreshHostedWebViewPresentation( | ||||||||||||||||||||||||||||||||||
| _ webView: WKWebView, | ||||||||||||||||||||||||||||||||||
| in containerView: WindowBrowserSlotView, | ||||||||||||||||||||||||||||||||||
| reason: String | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| guard !containerView.isHidden else { return } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| runHostedWebViewRefreshPass(webView, in: containerView, reason: reason, phase: "immediate") | ||||||||||||||||||||||||||||||||||
| runHostedWebViewRefreshPass( | ||||||||||||||||||||||||||||||||||
| webView, | ||||||||||||||||||||||||||||||||||
| in: containerView, | ||||||||||||||||||||||||||||||||||
| reason: reason, | ||||||||||||||||||||||||||||||||||
| phase: "immediate", | ||||||||||||||||||||||||||||||||||
| reattachRenderingState: true | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| DispatchQueue.main.async { [weak self, weak webView, weak containerView] in | ||||||||||||||||||||||||||||||||||
| guard let self, let webView, let containerView else { return } | ||||||||||||||||||||||||||||||||||
| self.runHostedWebViewRefreshPass( | ||||||||||||||||||||||||||||||||||
| webView, | ||||||||||||||||||||||||||||||||||
| in: containerView, | ||||||||||||||||||||||||||||||||||
| reason: reason, | ||||||||||||||||||||||||||||||||||
| phase: "async" | ||||||||||||||||||||||||||||||||||
| phase: "async", | ||||||||||||||||||||||||||||||||||
| reattachRenderingState: true | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { [weak self, weak webView, weak containerView] in | ||||||||||||||||||||||||||||||||||
|
|
@@ -2450,11 +2476,45 @@ final class WindowBrowserPortal: NSObject { | |||||||||||||||||||||||||||||||||
| webView, | ||||||||||||||||||||||||||||||||||
| in: containerView, | ||||||||||||||||||||||||||||||||||
| reason: reason, | ||||||||||||||||||||||||||||||||||
| phase: "delayed" | ||||||||||||||||||||||||||||||||||
| phase: "delayed", | ||||||||||||||||||||||||||||||||||
| reattachRenderingState: true | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private enum HostedWebViewPresentationUpdateKind { | ||||||||||||||||||||||||||||||||||
| case none | ||||||||||||||||||||||||||||||||||
| case geometryOnly | ||||||||||||||||||||||||||||||||||
| case refresh | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private static let geometryOnlyReasons: Set<String> = [ | ||||||||||||||||||||||||||||||||||
| "frame", | ||||||||||||||||||||||||||||||||||
| "bounds", | ||||||||||||||||||||||||||||||||||
| "webFrame", | ||||||||||||||||||||||||||||||||||
| "webFrameBottomDock", | ||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private static let refreshReasons: Set<String> = [ | ||||||||||||||||||||||||||||||||||
| "syncAttachContainer", | ||||||||||||||||||||||||||||||||||
| "syncAttachWebView", | ||||||||||||||||||||||||||||||||||
| "reveal", | ||||||||||||||||||||||||||||||||||
| "transientRecovery", | ||||||||||||||||||||||||||||||||||
| "anchor", | ||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| static func resolve(reasons: [String]) -> Self { | ||||||||||||||||||||||||||||||||||
| guard !reasons.isEmpty else { return .none } | ||||||||||||||||||||||||||||||||||
| let reasonSet = Set(reasons) | ||||||||||||||||||||||||||||||||||
| if !reasonSet.isDisjoint(with: Self.refreshReasons) { | ||||||||||||||||||||||||||||||||||
| return .refresh | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if reasonSet.isSubset(of: Self.geometryOnlyReasons) { | ||||||||||||||||||||||||||||||||||
| return .geometryOnly | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return .refresh | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private func moveWebKitRelatedSubviewsIfNeeded( | ||||||||||||||||||||||||||||||||||
| from sourceSuperview: NSView, | ||||||||||||||||||||||||||||||||||
| to containerView: WindowBrowserSlotView, | ||||||||||||||||||||||||||||||||||
|
|
@@ -3272,8 +3332,13 @@ final class WindowBrowserPortal: NSObject { | |||||||||||||||||||||||||||||||||
| let hostedInspectorAdjustedDuringSync = | ||||||||||||||||||||||||||||||||||
| containerOwnsWebView && | ||||||||||||||||||||||||||||||||||
| hostView.reapplyHostedInspectorDividerIfNeeded(in: containerView, reason: "portal.sync") | ||||||||||||||||||||||||||||||||||
| if !shouldHide, containerOwnsWebView, !refreshReasons.isEmpty { | ||||||||||||||||||||||||||||||||||
| if hostedInspectorAdjustedDuringSync && !recoveredFromTransientGeometry { | ||||||||||||||||||||||||||||||||||
| let presentationUpdateKind = HostedWebViewPresentationUpdateKind.resolve( | ||||||||||||||||||||||||||||||||||
| reasons: refreshReasons | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| if !shouldHide, containerOwnsWebView, presentationUpdateKind != .none { | ||||||||||||||||||||||||||||||||||
| if presentationUpdateKind == .refresh && | ||||||||||||||||||||||||||||||||||
| hostedInspectorAdjustedDuringSync && | ||||||||||||||||||||||||||||||||||
| !recoveredFromTransientGeometry { | ||||||||||||||||||||||||||||||||||
| #if DEBUG | ||||||||||||||||||||||||||||||||||
| dlog( | ||||||||||||||||||||||||||||||||||
| "browser.portal.refresh.skip web=\(browserPortalDebugToken(webView)) " + | ||||||||||||||||||||||||||||||||||
|
|
@@ -3282,11 +3347,23 @@ final class WindowBrowserPortal: NSObject { | |||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| refreshHostedWebViewPresentation( | ||||||||||||||||||||||||||||||||||
| webView, | ||||||||||||||||||||||||||||||||||
| in: containerView, | ||||||||||||||||||||||||||||||||||
| reason: "\(source):" + refreshReasons.joined(separator: ",") | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| let refreshReason = "\(source):" + refreshReasons.joined(separator: ",") | ||||||||||||||||||||||||||||||||||
| switch presentationUpdateKind { | ||||||||||||||||||||||||||||||||||
| case .none: | ||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||
| case .geometryOnly: | ||||||||||||||||||||||||||||||||||
| invalidateHostedWebViewGeometry( | ||||||||||||||||||||||||||||||||||
| webView, | ||||||||||||||||||||||||||||||||||
| in: containerView, | ||||||||||||||||||||||||||||||||||
| reason: refreshReason | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| case .refresh: | ||||||||||||||||||||||||||||||||||
| refreshHostedWebViewPresentation( | ||||||||||||||||||||||||||||||||||
| webView, | ||||||||||||||||||||||||||||||||||
| in: containerView, | ||||||||||||||||||||||||||||||||||
| reason: refreshReason | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if containerOwnsWebView, !hostedInspectorAdjustedDuringSync { | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Avoid
curl | bashfor 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