Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
19265 Sources/ContentView.swift
18118 Sources/AppDelegate.swift
16674 Sources/GhosttyTerminalView.swift
14622 Sources/TerminalController.swift
13606 Sources/Panels/BrowserPanel.swift
14629 Sources/TerminalController.swift
13640 Sources/Panels/BrowserPanel.swift
12044 cmuxTests/AppDelegateShortcutRoutingTests.swift
10020 Sources/TabManager.swift
9345 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
Expand All @@ -23,7 +23,7 @@
5462 Sources/cmuxApp.swift
4801 Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
4460 Sources/Panels/FilePreviewPanel.swift
4400 cmuxTests/BrowserPanelTests.swift
4385 cmuxTests/BrowserPanelTests.swift
4227 Sources/BrowserWindowPortal.swift
4009 cmuxTests/WindowAndDragTests.swift
3937 Sources/Feed/FeedPanelView.swift
Expand Down Expand Up @@ -95,7 +95,7 @@
905 Sources/CmuxSSHURLRequest.swift
896 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
878 Sources/WorkspaceContentView.swift
868 Sources/Panels/BrowserScreenshotSnapshotter.swift
863 Sources/Panels/BrowserScreenshotSnapshotter.swift
864 Sources/Panels/TerminalPanel.swift
856 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
852 Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Workspace/ControlCommandCoordinator+Workspace.swift
Expand Down
6 changes: 5 additions & 1 deletion Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Foundation
protocol BrowserHiddenWebViewDiscardManagerDelegate: AnyObject {
var hiddenWebViewDiscardSnapshot: BrowserHiddenWebViewDiscardManager.BlockerSnapshot { get }
var hiddenWebViewDiscardHiddenAt: Date? { get }
var hiddenWebViewDiscardLastAutomationActivityAt: Date? { get }
var hiddenWebViewDiscardWebViewInstanceID: UUID { get }

func hiddenWebViewDiscardManagerDidRequestDiscard(
Expand Down Expand Up @@ -101,11 +102,14 @@ final class BrowserHiddenWebViewDiscardManager {
let observedWebViewInstanceID = delegate.hiddenWebViewDiscardWebViewInstanceID
let generation = scheduleGeneration
let hiddenAt = delegate.hiddenWebViewDiscardHiddenAt ?? Date()
let lastAutomationActivityAt = delegate.hiddenWebViewDiscardLastAutomationActivityAt
// Restart the countdown from the latest wake: WebKit pages reconnect and
// re-navigate right after wake, and replacing/releasing a WKWebView in
// that window crashed in WebPageProxy::updateActivityState
// (https://github.com/manaflow-ai/cmux/issues/5261).
let effectiveHiddenAt = lastSystemWakeAt.map { max(hiddenAt, $0) } ?? hiddenAt
let effectiveHiddenAt = [hiddenAt, lastSystemWakeAt, lastAutomationActivityAt]
.compactMap { $0 }
.max() ?? hiddenAt
let elapsed = Date().timeIntervalSince(effectiveHiddenAt)
let remaining = max(0, BrowserHiddenWebViewDiscardPolicy.hiddenDelay - elapsed)
if remaining <= 0 {
Expand Down
34 changes: 34 additions & 0 deletions Sources/Panels/BrowserPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3397,6 +3397,7 @@ final class BrowserPanel: Panel, ObservableObject {
private var backgroundPreloadWindow: NSWindow?
private let visualAutomationCaptureGate = BrowserScreenshotCaptureGate()
private var activeVisualAutomationCaptureCount: Int = 0
private var webViewLastAutomationActivityAt: Date?
private struct PendingInteractiveBrowserPrompt {
let present: (NSWindow, @escaping () -> Void) -> Void
let cancel: () -> Void
Expand Down Expand Up @@ -3788,6 +3789,7 @@ final class BrowserPanel: Panel, ObservableObject {
webViewLastHiddenAt = nil
webViewLastVisibilityChangeAt = nil
webViewLastVisibilityChangeReason = nil
webViewLastAutomationActivityAt = nil
isWebViewVisibleInUI = false
}
hiddenWebViewDiscardManager.resetMetadata()
Expand Down Expand Up @@ -6395,6 +6397,10 @@ extension BrowserPanel: BrowserHiddenWebViewDiscardManagerDelegate {
webViewLastHiddenAt
}

var hiddenWebViewDiscardLastAutomationActivityAt: Date? {
webViewLastAutomationActivityAt
}

var hiddenWebViewDiscardWebViewInstanceID: UUID {
webViewInstanceID
}
Expand Down Expand Up @@ -7580,6 +7586,34 @@ extension BrowserPanel {
}
}

@discardableResult
func beginAutomationCommandLease(reason: String) -> BrowserScreenshotWebViewSnapshotter.OffscreenRenderHostLease? {
webViewLastAutomationActivityAt = Date()
activeVisualAutomationCaptureCount += 1
cancelHiddenWebViewDiscard()
restoreDiscardedWebViewIfNeeded(reason: "\(reason).restore")
refreshWebViewLifecycleState()

guard shouldUseOffscreenRenderHostForVisualAutomation else { return nil }
return BrowserScreenshotWebViewSnapshotter.OffscreenRenderHostLease(
webView: webView,
viewportSize: visualAutomationViewportSize()
)
}
Comment thread
cursor[bot] marked this conversation as resolved.

func endAutomationCommandLease(
_ lease: BrowserScreenshotWebViewSnapshotter.OffscreenRenderHostLease?,
reason: String
) {
lease?.end()
webViewLastAutomationActivityAt = Date()
activeVisualAutomationCaptureCount = max(0, activeVisualAutomationCaptureCount - 1)
refreshWebViewLifecycleState()
if activeVisualAutomationCaptureCount == 0, !isWebViewVisibleInUI {
scheduleHiddenWebViewDiscardIfNeeded(reason: "\(reason).finished")
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lease ends during active capture

Medium Severity

endAutomationCommandLease always tears down the automation offscreen host and restores the WKWebView when a socket command finishes, even if activeVisualAutomationCaptureCount is still above zero after decrement. Overlapping browser.screenshot (main actor) with a socket-worker command that held the lease can move the webview back to a hidden portal while PNG capture is still in progress, causing failed or incorrect screenshots.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e3fd55b. Configure here.


@discardableResult
func ensureVisualAutomationRestoreHostIfNeeded(reason: String) -> Bool {
guard shouldUseOffscreenRenderHostForVisualAutomation else { return false }
Expand Down
Loading
Loading