Skip to content

Fix browser downloads from subframes - #6756

Merged
austinywang merged 71 commits into
mainfrom
issue-6754-browser-downloads
Jun 30, 2026
Merged

austinywang merged 71 commits into
mainfrom
issue-6754-browser-downloads

Conversation

@austinywang

@austinywang austinywang commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • inject scripted browser download interception into subframes so iframe-hosted <a download> and blob flows can reach the native bridge
  • classify explicit attachment / force-download MIME responses from subframe navigation responses without forcing ordinary subresources to download
  • save completed browser downloads directly to ~/Downloads by default with collision-safe filenames, with an opt-in setting to ask where to save each file
  • route main browser, popup, context-menu, and scripted-download paths through the shared download completion behavior

Closes #6754

Verification

  • Added failing regression commit first for subframe scripted-download injection parity.
  • Added resolver coverage for subframe download classification and collision-safe destination naming.
  • Parsed touched JSON/localization files and ran git diff --check.
  • Did not run local tests or local/cloud builds per instruction.

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


Summary by cubic

Fixes browser downloads started in subframes and unifies all download paths with trusted, user-activated intents, safer redirect/HTTP gating, and consistent completion/events. Adds a persistent downloads button with a popover and an opt‑in browser.askWhereToSaveDownloads setting.

  • Bug Fixes

    • Subframes: tokenized intent script; restrict scripted downloads to the main frame and validate frame origin; record/transfer intents across redirects (incl. popups); classify only explicit attachment/force‑download MIME and route via response downloads; apply insecure‑HTTP blocking only after a download is chosen; stream large fallbacks; preserve blob names and use collision‑safe filenames.
    • Events: deduplicate prompted download events, keep completion after prompt, bound per‑surface queues and consumed IDs, and sanitize failure events.
    • PDF: scope Print to the rendered document and show toolbar actions only for a main‑frame PDF.
  • New Features

    • Downloads popover: omnibar button uses a monochrome icon with a spinner while downloading and a discrete bounce on completion; shows a red notification bubble (count) that clears when the popover opens; lists recent downloads (progress, Open, Show in Finder) with Clear.
    • Added browser.askWhereToSaveDownloads with settings UI, search anchors/aliases, command palette toggle, JSON schema, and localization. Off by default.
    • PDF toolbar: omnibar Download/Print buttons when a main‑frame PDF is rendered.

Written for commit c6c6b6d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added an Ask Where to Save Downloads browser setting that prompts with a save panel for each download (instead of saving directly to Downloads), available in settings search and the Command Palette.
  • Bug Fixes
    • Improved download handling for subframe-initiated cases, download authorization decisions, collision-safe save destinations, and quarantine application.
    • Refined session download lifecycle handling and per-surface download event deduplication.
  • Tests
    • Expanded coverage for UI/search anchors, settings/schema, filename resolution, save-prompt behavior, quarantine, subframe intent tracking, and download event queue consumption.

@vercel

vercel Bot commented Jun 24, 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 Jun 30, 2026 5:06am
cmux-staging Building Building Preview, Comment Jun 30, 2026 5:06am

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a browser save-location setting, exposes it across settings surfaces, and updates download handling for frame-aware classification, subframe intent tracking, WebKit download routing, and save-dialog or auto-save completion paths.

Changes

Browser download save prompt

Layer / File(s) Summary
Settings discovery
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/BrowserCatalogSection.swift, Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift, Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift, Sources/CmuxSettingsJSONPathSupport.swift, Sources/SettingsNavigation.swift, Sources/SettingsSearchAliases.swift, Sources/CommandPalette/CommandPaletteSettingsToggle.swift, Sources/KeyboardShortcutSettingsFileStore+Template.swift, Resources/Localizable.xcstrings, web/data/cmux.schema.json, web/messages/en.json, web/messages/ja.json, cmuxTests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
askWhereToSaveDownloads is added to the browser settings catalog and exposed through catalog, schema, localization, search, settings UI, and discovery entry points.
Download resolution
Sources/Panels/BrowserDownloadHTTPStatusDecision.swift, Sources/Panels/BrowserDownloadFilenameResolver.swift, cmuxTests/BrowserDownloadFilenameResolverTests.swift
Download classification now depends on frame context, the save-prompt setting is read from the catalog, destination names are deduplicated, and web-download quarantine metadata is applied; tests cover classification, collisions, quarantine, and the HTTP-status decision type.
Subframe intent routing
Sources/Panels/BrowserSubframeDownloadIntentTracker.swift, Sources/Panels/BrowserNavigationDelegate.swift, Sources/Panels/BrowserPopupWindowController.swift, Sources/Panels/CmuxWebView+SubframeDownloadIntentScript.swift, Sources/Panels/CmuxWebView+ScriptedDownloads.swift, cmuxTests/BrowserDownloadFilenameResolverTests.swift, cmux.xcodeproj/project.pbxproj
A new intent tracker, user script, and navigation wiring record recent iframe download intents and feed them into download decisions, with project entries and tests for the new flow.
Browser download delegate
Sources/Panels/BrowserPanel.swift, Sources/TerminalController.swift, cmuxTests/TerminalControllerSocketSecurityTests.swift
WebKit downloads are bound into BrowserPanel, save-panel anchoring and lifecycle callbacks are added, and queued browser download events now skip started entries.
Scripted downloads
Sources/Panels/CmuxWebView.swift, Sources/Panels/CmuxWebView+ScriptedDownloads.swift
CmuxWebView stores the download delegate and centralizes context-menu download completion, while scripted downloads now cover subframes, blob: fallbacks, and WebKit-started downloads.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#6258: Introduced the browser download trigger and scripted download paths that this PR extends for subframes and save prompting.
  • manaflow-ai/cmux#5938: Also changes Sources/Panels/BrowserDownloadFilenameResolver.swift, which this PR extends with frame-aware classification and destination selection.
  • manaflow-ai/cmux#5012: Added settings search and anchor-resolution wiring that this PR reuses for the new browser setting.

Suggested reviewers

  • lawrencecchen

Poem

A bunny hopped through bytes and light,
Found save panels tucked just right.
In frames and blobs, the paths now steer,
To Downloads with a gentle cheer.
🐰


Important

Pre-merge checks failed

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

❌ Failed checks (5 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error BrowserDownloadDelegate adds activeDownloadsLock (NSLock) around shared download state alongside new async work, with no actor/bridge rationale documented. Replace the lock with actor/MainActor-owned state or document why WKDownload callbacks require a tiny lock; avoid manual locking if an explicit signal suffices.
Cmux Swift File And Package Boundaries ❌ Error CmuxWebView.swift already had 2790 lines and gained 316 more; the new download code blends WebKit bridging, UI prompting, and file I/O in the app target. Extract the download-completion/file-write/quarantine path into a small BrowserDownloadCompletion/Writer type (or package) and keep CmuxWebView as thin glue.
Cmux Swift Logging ❌ Error Added production NSLog calls in BrowserNavigationDelegate.swift and BrowserPanel.swift (navigation, download, snapshot failures), violating the swift-logging rule. Remove or #if DEBUG-gate the NSLogs, or replace them with cmuxDebugLog/Logger using redaction for any dynamic values.
Cmux User-Facing Error Privacy ❌ Error BrowserPanel stores error.localizedDescription in download events, and browser.download.wait returns those events verbatim, exposing raw upstream error text. Replace the public failure string with a generic message like “Download failed” and keep detailed NSError text only in debug logs or internal telemetry.
Cmux Full Internationalization ❌ Error New web copy exists only in web/messages/en.json and ja.json, but web/i18n/routing.ts lists 20 locales; the other 18 message files lack the new key. Add the askWhereToSaveDownloads message entry to every locale file in web/messages/ so it matches web/i18n/routing.ts, or remove the locale from routing if unsupported.
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 (19 passed)
Check name Status Explanation
Title check ✅ Passed The title is clear and directly summarizes the main change: fixing browser downloads from subframes.
Linked Issues check ✅ Passed The code matches the issue goals: subframe downloads are handled, attachment/MIME downloads are classified, and downloads now default to ~/Downloads with an optional prompt.
Out of Scope Changes check ✅ Passed The added settings, helpers, tests, and wiring all support the download-fix scope; no unrelated changes stand out.
Cmux Swift Actor Isolation ✅ Passed New mutable state is behind @MainActor delegates or NSLock, and pure helpers are explicitly nonisolated; no new isolation regressions found.
Cmux Browser Automation Off-Main ✅ Passed PASS: browser.download.wait is in socketWorkerMethods, dispatched to v2BrowserDownloadWaitOnSocketWorker, and policy tests cover it; no new main-actor routing regression.
Cmux Expensive Synchronous Load ✅ Passed No touched Swift file adds or moves RestorableAgentSessionIndex.load() or agent-history parsing onto MainActor/interactive paths; changes are download/settings only.
Cmux Cache Substitution Correctness ✅ Passed Only transient, event-driven download caches were added; persistence/history paths still read fresh state and fall back when caches are empty.
Cmux No Hacky Sleeps ✅ Passed The only changed non-Swift files are config/localization/test scaffolding, and diff scans found no added sleeps, timers, polling, or wall-clock waits.
Cmux Algorithmic Complexity ✅ Passed PASS: New scans are bounded (64/100/128) or tiny fixed lists; the only unbounded queue scans were already present before this commit.
Cmux Swift Concurrency ✅ Passed New async work is confined to AppKit/WebKit/URLSession boundaries; I found no new Combine state or uncancelled production Task lifecycle issue.
Cmux Swift @Concurrent ✅ Passed No new nonisolated async helpers were added; heavy download filename/file I/O work is offloaded via Task.detached, and remaining methods are sync/UI-bound.
Cmux Swiftpm Lockfiles ✅ Passed No cmux-owned .gitignore ignores Package.resolved, and the pbxproj change is source-only—no SwiftPM dependency or package-reference edit that would require a lockfile diff.
Cmux Swiftui State Layout ✅ Passed BrowserSection adds @State DefaultsValueModel (already @Observable); no ObservableObject/@published, GeometryReader, lazy/list store refs, or render-time state writes were introduced.
Cmux Architecture Rethink ✅ Passed PASS: the new subframe-intent bridge and download delegate have clear owners/invariants; I found no added sleeps, polling, or split lifecycle state.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed BrowserPopupPanel keeps stable cmux.browser-popup ID and is registered in cmuxAuxiliaryWindowIdentifiers, so the popup window still shares the close-shortcut path.
Cmux Source Artifacts ✅ Passed All touched paths are intentional source/config/test/localization files; the TSV is a CI budget file referenced by scripts, and no temp/log/build artifacts appear.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test/debug-only accessor or ForTesting seam was added in touched production Sources; the changes are production download plumbing and DEBUG logging only.
Cmux No Ambient Global State ✅ Passed No new ambient global state found: new download state lives on instances, and the only new shared object is a private stateless message handler/constant.
Description check ✅ Passed The PR description covers the required Summary and Verification sections and is mostly aligned with the template.
✨ 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-6754-browser-downloads

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.

@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 `@Sources/Panels/CmuxWebView.swift`:
- Around line 1633-1635: Move the download write path off the main thread: the
synchronous Data write inside the writeData helper in CmuxWebView is being
invoked from main-dispatched download closures, which can block the UI for large
downloads. Keep only destination resolution on the main thread, then perform
directory creation and file writing in a background task before returning to the
main queue for any state updates or fallback callbacks. Apply the same pattern
to the related download call sites in the data, file, and response handlers so
they all use the background write path.

In `@Sources/Panels/CmuxWebView`+ScriptedDownloads.swift:
- Around line 212-216: The scripted download handler in
CmuxWebView+ScriptedDownloads should not accept native file: downloads from
injected web content. Update the URL interception logic in the download path
that checks scheme handling so only http/https are allowed for general scripted
content, and keep file: support restricted to trusted file-origin cases with
strict scope checks before reaching downloadURLViaSession or the file branch.
Ensure the same fix is applied anywhere the same scheme allow-list appears in
this scripted download flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e25895dc-cb8b-4b11-8c17-b687ffb864c6

📥 Commits

Reviewing files that changed from the base of the PR and between da4e4bc and 8e36fd9.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (19)
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/BrowserCatalogSection.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift
  • Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Resources/Localizable.xcstrings
  • Sources/CmuxSettingsJSONPathSupport.swift
  • Sources/CommandPalette/CommandPaletteSettingsToggle.swift
  • Sources/KeyboardShortcutSettingsFileStore+Template.swift
  • Sources/Panels/BrowserDownloadFilenameResolver.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPopupWindowController.swift
  • Sources/Panels/CmuxWebView+ScriptedDownloads.swift
  • Sources/Panels/CmuxWebView.swift
  • Sources/SettingsNavigation.swift
  • Sources/SettingsSearchAliases.swift
  • cmuxTests/BrowserDownloadFilenameResolverTests.swift
  • web/data/cmux.schema.json
  • web/messages/en.json
  • web/messages/ja.json

Comment thread Sources/Panels/CmuxWebView.swift Outdated
Comment thread Sources/Panels/CmuxWebView+ScriptedDownloads.swift Outdated
@greptile-apps

greptile-apps Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes browser downloads initiated from subframes by injecting a tokenized, trusted-click intent script into all frames; adds a persistent downloads popover button with progress and file actions; unifies the WKDownload, popup, context-menu, and scripted-download paths through shared completion logic; and adds an opt-in browser.askWhereToSaveDownloads setting.

  • Subframe intent tracking: BrowserSubframeDownloadIntentTracker records user-activated click intents and redirect transfers; BrowserNavigationDelegate consumes them in both the action and response policy handlers, restricting subframe response downloads to explicit Content-Disposition: attachment or force-download MIME types.
  • Download completion paths unified: BrowserSessionDownloadSaver centralises context-menu and scripted-download saves (auto-save to ~/Downloads or NSSavePanel); BrowserDownloadDelegate gains the same auto-save path with a Task.detached write and collision-safe filenames.
  • Terminal automation event queue hardened: "started" events are filtered from the queue; terminal events deduplicate older entries for the same download_id; the consumed-ID set is bounded at 128 entries per surface.

Confidence Score: 4/5

Safe to merge modulo open prior-round comments on the terminal event queue and save-panel file I/O; the new subframe interception and unified completion paths are logically sound.

The core subframe intent tracking, collision-safe auto-save, popover UI, and localization are all well-constructed. Two dead-code items were found in this round (the handledAnchors WeakSet that is never populated, and the allowsSubframeDownload parameter that is passed to navigationResponseDownloadReason but never read), both minor. The lower ceiling reflects that comments from prior review rounds about the ready_to_save stale-event queue in TerminalController and synchronous file I/O in the BrowserDownloadDelegate.presentSavePanel completion handler remain open without a resolution reply.

Sources/TerminalController.swift — prior-round comment on stale terminal event after save-panel dismissal is unresolved. Sources/Panels/BrowserPanel.swift — prior-round comment on synchronous file I/O in the presentSavePanel NSSavePanel completion handler is unresolved.

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanel.swift Large central orchestration file; download delegate callbacks unified, recentDownloads popover state added, and session-download event routing wired correctly. downloadDelegate visibility widened to internal — acceptable as @testable pattern, but no external callers exist.
Sources/TerminalController.swift Download event queue refactored: started events filtered, terminal events deduplicate the queue, consumed-ID set bounded. v2RecordBrowserDownloadEvent and v2PopBrowserDownloadEvent widened to internal for test access. Previously flagged stale-event issue in the ready_to_save → saved sequence remains open in prior review threads.
Sources/Panels/BrowserNavigationDelegate.swift Navigation response handler now covers subframe cases; PDF print/render tracking callbacks added; subframe intent tracker integrated. NSLog calls for didBecome download were pre-existing and not worsened by this PR.
Sources/Panels/CmuxWebView+ScriptedDownloads.swift Blob URL fallback and large-payload path improved; isMainFrame guard added to restrict blob/data intercept to main frame; handledAnchors WeakSet added but never populated — deduplication guard is dead code.
Sources/Panels/BrowserDownloadFilenameResolver.swift Collision-safe filename logic, downloads directory helper, askWhereToSaveDownloads setting, and quarantine helpers added. allowsSubframeDownload parameter added to navigationResponseDownloadReason but never read in function body.
Sources/Panels/BrowserSessionDownloadSaver.swift New MainActor-isolated helper that routes context-menu and scripted downloads to auto-save or save-panel paths. File writes run in Task.detached for the auto-save branch; promptForDestination notifies correct events but the NSSavePanel completion still writes synchronously on the main thread (flagged in prior review round).
Sources/Panels/BrowserSubframeDownloadIntentTracker.swift New MainActor-isolated tracker for subframe click intents, redirect transfers, user-activated PDF re-downloads, and PDF print intents. All collections are bounded to maxIntentCount=64 and pruned on TTL.
Sources/Panels/CmuxWebView+SubframeDownloadIntentScript.swift New minimal trusted-click intent script injected into all frames. Tokenized, HTTP(S)-only, rate-limited; does not intercept blob/data URLs. MutationObserver arm for dynamically inserted download anchors handles the 2 s activation window.
Sources/Panels/BrowserDownloadsToolbarButton.swift New Safari/Chrome-style downloads popover using native SwiftUI. Passes value snapshots below ForEach boundary per CLAUDE.md. unseenCount logic is correct; seenIDs cleared on open.
Sources/Panels/BrowserNavigationPopupPolicy.swift Popup/new-tab policy helpers moved from BrowserPanel.swift into a dedicated file for testability. All functions are pure stateless helpers; pre-existing code relocated, no new ambient state introduced.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant JS as Subframe JS
    participant Bridge as WKScriptMessageHandler
    participant Tracker as BrowserSubframeDownloadIntentTracker
    participant NavDel as BrowserNavigationDelegate
    participant DlDel as BrowserDownloadDelegate
    participant BP as BrowserPanel

    JS->>Bridge: "kind=subframeDownloadIntent {url}"
    Bridge->>Tracker: record(url)

    NavDel->>Tracker: updateIfNeeded(navigationAction)
    NavDel->>Tracker: consume(for: url) [action policy]
    NavDel->>NavDel: decisionHandler(.download)

    DlDel->>DlDel: decideDestination to tempURL
    DlDel->>BP: onDownloadStarted(filename, downloadID)
    BP->>BP: applyBrowserDownloadEvent started

    alt "askWhereToSave = false"
        DlDel->>DlDel: moveTemporaryDownloadToDownloads Task.detached
        DlDel->>BP: onDownloadSaved(filename, destURL, true, downloadID)
    else "askWhereToSave = true"
        DlDel->>DlDel: presentSavePanel MainActor
        DlDel->>BP: onDownloadReadyToSave(filename, downloadID)
        BP->>BP: endDownloadActivity
        DlDel->>BP: onDownloadSaved(filename, destURL, false, downloadID)
    end

    BP->>BP: applyBrowserDownloadEvent saved to recentDownloads
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 JS as Subframe JS
    participant Bridge as WKScriptMessageHandler
    participant Tracker as BrowserSubframeDownloadIntentTracker
    participant NavDel as BrowserNavigationDelegate
    participant DlDel as BrowserDownloadDelegate
    participant BP as BrowserPanel

    JS->>Bridge: "kind=subframeDownloadIntent {url}"
    Bridge->>Tracker: record(url)

    NavDel->>Tracker: updateIfNeeded(navigationAction)
    NavDel->>Tracker: consume(for: url) [action policy]
    NavDel->>NavDel: decisionHandler(.download)

    DlDel->>DlDel: decideDestination to tempURL
    DlDel->>BP: onDownloadStarted(filename, downloadID)
    BP->>BP: applyBrowserDownloadEvent started

    alt "askWhereToSave = false"
        DlDel->>DlDel: moveTemporaryDownloadToDownloads Task.detached
        DlDel->>BP: onDownloadSaved(filename, destURL, true, downloadID)
    else "askWhereToSave = true"
        DlDel->>DlDel: presentSavePanel MainActor
        DlDel->>BP: onDownloadReadyToSave(filename, downloadID)
        BP->>BP: endDownloadActivity
        DlDel->>BP: onDownloadSaved(filename, destURL, false, downloadID)
    end

    BP->>BP: applyBrowserDownloadEvent saved to recentDownloads
Loading

Reviews (45): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread Sources/Panels/CmuxWebView.swift Outdated
Comment on lines +1683 to +1712
do {
let directory = filenameResolver.downloadsDirectory()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
let destinationURL = filenameResolver.uniqueDownloadDestination(
suggestedFilename: saveName,
in: directory
)
debugContextDownload(
"browser.ctxdl.\(logCategory) trace=\(traceID) stage=autoSave path=\(destinationURL.path)"
)
let didWrite = writeData(destinationURL)
notifyContextMenuDownloadState(false)
if !didWrite, failureFallbackReason == nil {
return
}
} catch {
notifyContextMenuDownloadState(false)
debugContextDownload(
"browser.ctxdl.\(logCategory) trace=\(traceID) stage=saveFailure error=\(error.localizedDescription)"
)
if let failureFallbackReason {
runContextMenuFallback(
action: fallbackAction,
target: fallbackTarget,
sender: sender,
traceID: traceID,
reason: failureFallbackReason
)
}
}

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.

P1 Synchronous file I/O on MainActor in auto-save path

finishSessionDownload's auto-save branch calls FileManager.createDirectory, the fileExists loop inside uniqueDownloadDestination, and data.write(to:options:.atomic) all synchronously on @MainActor. CmuxWebView inherits WKWebView's @MainActor isolation, so every call site that hops back to main via DispatchQueue.main.async before calling this (the http/https and file: URLSession paths) will block the run loop for the full duration of the disk write. A large file from the context-menu "Save Image As" or scripted-download path will freeze the UI until the write completes.

BrowserDownloadDelegate.downloadDidFinish handles the same auto-save operation correctly by wrapping it in Task.detached(priority: .utility) and awaiting the result before calling onDownloadSaved. The two paths need to be consistent. The createDirectory + uniqueDownloadDestination + data.write block should be lifted into a detached task with a main-actor hop for the notifyContextMenuDownloadState/fallback work, matching the delegate pattern at BrowserPanel.swift lines 8442–8460.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved in current head: non-WebKit session download writes now run through the detached save path before main-actor state updates; this thread is outdated against 209962a.

— Claude Code

@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 (3)
Sources/Panels/BrowserPanel.swift (2)

8454-8456: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not log raw download destination paths.

destinationURL.path can include usernames and private folder/file names. Keep the DEBUG trace to non-sensitive metadata such as byte counts or a trace id.

Suggested change
-                cmuxDebugLog("download.saved path=\(destinationURL.path)")
+                cmuxDebugLog("download.saved pathBytes=\(destinationURL.path.utf8.count)")
🤖 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 8454 - 8456, The DEBUG trace
in the download save flow is logging a raw destination path, which can expose
sensitive user and folder information. Update the logging in the download
handling code around the cmuxDebugLog call in BrowserPanel to avoid
destinationURL.path and instead record only non-sensitive metadata such as byte
counts, a trace identifier, or other generic status details.

Source: Coding guidelines


8370-8379: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move the save-panel file operation off the main actor.

presentSavePanel is @MainActor, so replacing/moving a completed download here can block the UI for large files or cross-volume saves. Mirror the auto-save path and do the FileManager work in a detached task, then hop back to the main actor for callbacks.

Suggested direction
-            do {
-                if FileManager.default.fileExists(atPath: destURL.path) {
-                    _ = try FileManager.default.replaceItemAt(destURL, withItemAt: tempURL)
-                } else {
-                    try FileManager.default.moveItem(at: tempURL, to: destURL)
-                }
-                self.onDownloadSaved?(suggestedFilename, destURL)
-            } catch {
-                try? FileManager.default.removeItem(at: tempURL)
-                self.onDownloadFailed?(error)
-            }
+            Task {
+                let result = await Task.detached(priority: .utility) {
+                    Result {
+                        if FileManager.default.fileExists(atPath: destURL.path) {
+                            _ = try FileManager.default.replaceItemAt(destURL, withItemAt: tempURL)
+                        } else {
+                            try FileManager.default.moveItem(at: tempURL, to: destURL)
+                        }
+                    }
+                }.value
+
+                switch result {
+                case .success:
+                    self.onDownloadSaved?(suggestedFilename, destURL)
+                case .failure(let error):
+                    try? FileManager.default.removeItem(at: tempURL)
+                    self.onDownloadFailed?(error)
+                }
+            }
🤖 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 8370 - 8379, The file
move/replace work in presentSavePanel is still running on the MainActor and can
block the UI for large or cross-volume saves. Move the FileManager operations in
the save-panel completion path into a detached task, mirroring the auto-save
flow, and only hop back to the MainActor for onDownloadSaved and
onDownloadFailed callbacks; use the presentSavePanel and FileManager
replaceItemAt/moveItem logic to locate the code.

Source: Coding guidelines

Sources/Panels/CmuxWebView+ScriptedDownloads.swift (1)

133-135: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not materialize unknown blob URLs in JavaScript before size checks.

fetch(url).blob() buffers the whole blob before readBlobForDownload can reject payloads over maxPayloadBytes, so large iframe downloads can still spike memory or kill the web content process. Post the blob URL directly and let the native/WebKit download path stream it.

Suggested change
-            fetch(url)
-              .then((response) => response.blob())
-              .then((blob) => readBlobForDownload(blob, suggestedFilename, url))
-              .catch(() => postURLDownload(url, suggestedFilename));
+            postURLDownload(url, suggestedFilename);
             return true;
🤖 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/CmuxWebView`+ScriptedDownloads.swift around lines 133 - 135,
The scripted download flow in CmuxWebView+ScriptedDownloads is materializing
blob URLs in JavaScript via fetch(url).blob() before size validation, which can
spike memory for large unknown payloads. Update the download path in the blob
handling code so the blob URL is passed directly into the native/WebKit download
path and let readBlobForDownload or its caller avoid buffering unknown blob URLs
in JS before maxPayloadBytes checks. Keep the fix localized around the download
promise chain and the readBlobForDownload helper.
🤖 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 8454-8456: The DEBUG trace in the download save flow is logging a
raw destination path, which can expose sensitive user and folder information.
Update the logging in the download handling code around the cmuxDebugLog call in
BrowserPanel to avoid destinationURL.path and instead record only non-sensitive
metadata such as byte counts, a trace identifier, or other generic status
details.
- Around line 8370-8379: The file move/replace work in presentSavePanel is still
running on the MainActor and can block the UI for large or cross-volume saves.
Move the FileManager operations in the save-panel completion path into a
detached task, mirroring the auto-save flow, and only hop back to the MainActor
for onDownloadSaved and onDownloadFailed callbacks; use the presentSavePanel and
FileManager replaceItemAt/moveItem logic to locate the code.

In `@Sources/Panels/CmuxWebView`+ScriptedDownloads.swift:
- Around line 133-135: The scripted download flow in
CmuxWebView+ScriptedDownloads is materializing blob URLs in JavaScript via
fetch(url).blob() before size validation, which can spike memory for large
unknown payloads. Update the download path in the blob handling code so the blob
URL is passed directly into the native/WebKit download path and let
readBlobForDownload or its caller avoid buffering unknown blob URLs in JS before
maxPayloadBytes checks. Keep the fix localized around the download promise chain
and the readBlobForDownload helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1d8a73c1-ef83-476f-bdb8-6a5bfa800b71

📥 Commits

Reviewing files that changed from the base of the PR and between 8e36fd9 and 2ae496e.

📒 Files selected for processing (2)
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/CmuxWebView+ScriptedDownloads.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: 2

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 (2)

9019-9034: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove production raw-URL logging from navigation response/download classification.

After subframe responses started flowing through this path, these NSLog calls can emit full browsing URLs for ordinary iframe/subresource traffic. Use DEBUG-only redacted diagnostics or Logger with private/redacted fields. As per coding guidelines, runtime Swift should not use NSLog, and secrets/customer content/personal data must not be logged without redaction.

🤖 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 9019 - 9034, Remove the
production raw-URL logging from the BrowserPanel navigation/download path.
Replace the two NSLog calls in BrowserPanel’s navigationResponse handling with
DEBUG-only diagnostics or Logger-based messages that redact private values,
especially responseURL and any URL-derived fields. Keep the classification logic
in BrowserDownloadFilenameResolver.navigationResponseDownloadReason unchanged,
but ensure ordinary subframe/subresource traffic cannot emit full browsing URLs
in release builds.

Source: Coding guidelines


8383-8389: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move the prompted save file operation off the main actor.

The auto-save path detaches file work, but the save-panel OK path still does replaceItemAt/moveItem on @MainActor. Large or cross-volume prompted downloads can freeze the UI; perform the move/replace in a detached task and hop back only for onDownloadSaved / onDownloadFailed.

🤖 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 8383 - 8389, The prompted
save flow in BrowserPanel’s save handling still performs
FileManager.replaceItemAt/moveItem on the MainActor, which can block the UI for
large or cross-volume files. Move the file operation out of the `@MainActor`
context by running the save-panel OK path work in a detached task, then switch
back only to call onDownloadSaved or onDownloadFailed. Keep the fix centered
around the save logic near onDownloadSaved, replaceItemAt, and moveItem so the
UI thread only handles callbacks.
🤖 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/BrowserPopupWindowController.swift`:
- Around line 605-609: The subframe download intent check is relying on an
unreliable app event heuristic in browserNavigationHasSimpleUserActivation(),
which can be triggered by unrelated input. Update the intent gate in
BrowserPopupWindowController and the copied helper in BrowserNavigationDelegate
to use a scoped, authoritative signal such as navigationType == .linkActivated
or a structured scripted-download bridge token, and fail closed when that signal
is absent. Keep the existing recentSubframeDownloadIntentKeys tracking, but
ensure only the new trusted navigation/download intent source can authorize the
auto-save path.

In `@Sources/Panels/CmuxWebView.swift`:
- Around line 1637-1644: The diagnostics in CmuxWebView are logging sensitive
download details such as destinationURL.path and saveName, which can expose
private filenames or local paths. Update the affected logging in the
download/save flow (including the saveSuccess/saveFailure paths and the related
sites around the other noted locations) to emit only non-sensitive metadata,
such as byte counts, file extension, or a redacted/private identifier. Keep the
existing debugContextDownload call sites, but replace any raw path/name values
with sanitized fields from the relevant download handling methods and models.

---

Outside diff comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 9019-9034: Remove the production raw-URL logging from the
BrowserPanel navigation/download path. Replace the two NSLog calls in
BrowserPanel’s navigationResponse handling with DEBUG-only diagnostics or
Logger-based messages that redact private values, especially responseURL and any
URL-derived fields. Keep the classification logic in
BrowserDownloadFilenameResolver.navigationResponseDownloadReason unchanged, but
ensure ordinary subframe/subresource traffic cannot emit full browsing URLs in
release builds.
- Around line 8383-8389: The prompted save flow in BrowserPanel’s save handling
still performs FileManager.replaceItemAt/moveItem on the MainActor, which can
block the UI for large or cross-volume files. Move the file operation out of the
`@MainActor` context by running the save-panel OK path work in a detached task,
then switch back only to call onDownloadSaved or onDownloadFailed. Keep the fix
centered around the save logic near onDownloadSaved, replaceItemAt, and moveItem
so the UI thread only handles callbacks.
🪄 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: d00c0a4d-1a79-4374-b02f-19ba28c4c2a4

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae496e and a4e37af.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • Sources/Panels/BrowserDownloadFilenameResolver.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPopupWindowController.swift
  • Sources/Panels/CmuxWebView.swift
  • cmuxTests/BrowserDownloadFilenameResolverTests.swift

Comment thread Sources/Panels/BrowserPopupWindowController.swift Outdated
Comment thread Sources/Panels/CmuxWebView.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment on lines 3744 to 3745
webView.cmuxDownloadDelegate = downloadDelegate
configureMoveTabToNewWorkspaceContextMenu(for: webView); configureNavigationDelegateCallbacks()

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.

P1 cmuxDownloadDelegate set before delegate is created — blob downloads always fail

webView.cmuxDownloadDelegate = downloadDelegate (line 3744) executes before configureNavigationDelegateCallbacks() (line 3745), which is what actually creates the new BrowserDownloadDelegate and assigns it to self.downloadDelegate. On the first bind, self.downloadDelegate is nil, so webView.cmuxDownloadDelegate is set to nil. On a re-bind it is set to the previous stale delegate. Either way, the webview never holds the current delegate.

startScriptedWebKitDownload guards on cmuxDownloadDelegate being non-nil and logs stage=rejectMissingDelegate if it is nil, so blob: scripted downloads — the primary new path added in this PR — will silently fail in every BrowserPanel context.

Move the assignment to after configureNavigationDelegateCallbacks() returns, or assign inside that method just after self.downloadDelegate = dlDelegate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved in current head: the current BrowserPanel binding assigns the web view download delegate after delegate creation, so scripted WebKit downloads have the active delegate; this thread is outdated against 209962a.

— Claude Code

@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 `@Sources/Panels/BrowserPanel.swift`:
- Around line 8323-8336: `suggestedFilenameOverrides` can retain stale entries
when a `WKDownload` is rejected or fails before `decideDestination` is reached.
Update the shared cleanup path in `BrowserPanel` so the override is removed
whenever download state is cleared, not only via
`takeSuggestedFilenameOverride(for:)`; also clear it in the early HTTP-status
reject branch inside `download(_:decideDestinationUsing:...)` and the failure
path in `download(_:didFailWithError:)` or `removeState`. Ensure the cleanup
uses the same `ObjectIdentifier(download)` key so entries for dead downloads
cannot accumulate.
🪄 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: 8c1a465b-7912-465a-b022-0101ea57f895

📥 Commits

Reviewing files that changed from the base of the PR and between a4e37af and db86301.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (4)
  • Sources/Panels/BrowserDownloadFilenameResolver.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/CmuxWebView+ScriptedDownloads.swift
  • cmuxTests/BrowserDownloadFilenameResolverTests.swift

Comment thread Sources/Panels/BrowserPanel.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)

9037-9052: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove production NSLog calls that include raw URLs.

These lines log full navigation/download URLs, including query strings, outside #if DEBUG. Use cmuxDebugLog under DEBUG and scrub via browserNavigationDebugURL(...), or drop the URL entirely.

Suggested fix
-        NSLog("BrowserPanel navigationResponse: url=%@ mime=%@ canShow=%d isMainFrame=%d",
-              responseURL, mime, canShow ? 1 : 0,
-              navigationResponse.isForMainFrame ? 1 : 0)
+        `#if` DEBUG
+        let debugResponseURL = browserNavigationDebugURL(navigationResponse.response.url)
+        cmuxDebugLog(
+            "browser.nav.response url=\(debugResponseURL) mime=\(mime) " +
+            "canShow=\(canShow ? 1 : 0) isMainFrame=\(navigationResponse.isForMainFrame ? 1 : 0)"
+        )
+        `#endif`
...
-            NSLog("BrowserPanel download: %@ mime=%@ url=%@", reason, mime, responseURL)
             `#if` DEBUG
-            cmuxDebugLog("download.policy=download reason=\(reason) mime=\(mime) mainFrame=\(navigationResponse.isForMainFrame ? 1 : 0)")
+            let debugResponseURL = browserNavigationDebugURL(navigationResponse.response.url)
+            cmuxDebugLog(
+                "download.policy=download reason=\(reason) mime=\(mime) " +
+                "mainFrame=\(navigationResponse.isForMainFrame ? 1 : 0) url=\(debugResponseURL)"
+            )
             `#endif`
🤖 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 9037 - 9052, Remove the
production NSLog calls in BrowserPanel navigation/download handling that print
raw URLs, especially the logging around navigationResponse and the download
reason. Update the logic in BrowserPanel’s navigation decision path to use
cmuxDebugLog only under DEBUG, and when a URL is needed, pass it through
browserNavigationDebugURL(...) instead of logging responseURL directly. If the
URL is not essential, drop it from the log message entirely.

Source: Coding guidelines

Sources/Panels/CmuxWebView+ScriptedDownloads.swift (1)

149-161: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not main-frame-gate the hooks needed for subframe scripted downloads.

The script now runs in subframes, but subframe URL.createObjectURL tracking and programmatic anchor.click() interception are disabled here. That misses common iframe blob-download flows.

Suggested fix
-        if (isMainFrame && typeof originalCreateObjectURL === "function") {
+        if (typeof originalCreateObjectURL === "function") {
...
-        if (isMainFrame && typeof originalRevokeObjectURL === "function") {
+        if (typeof originalRevokeObjectURL === "function") {
...
-        if (isMainFrame && typeof originalAnchorClick === "function") {
+        if (typeof originalAnchorClick === "function") {

Also applies to: 232-236

🤖 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/CmuxWebView`+ScriptedDownloads.swift around lines 149 - 161,
The `CmuxWebView+ScriptedDownloads` hooks are still being gated by
`isMainFrame`, which prevents subframe blob-download tracking and programmatic
`anchor.click()` interception from working. Remove the main-frame-only condition
around the `URLCtor.createObjectURL`/`URLCtor.revokeObjectURL` wrappers and the
`anchor.click()` interception path so the scripted-download hooks are installed
for subframes as well, while keeping the existing `originalCreateObjectURL`,
`originalRevokeObjectURL`, and click-hook logic intact.
🤖 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 9037-9052: Remove the production NSLog calls in BrowserPanel
navigation/download handling that print raw URLs, especially the logging around
navigationResponse and the download reason. Update the logic in BrowserPanel’s
navigation decision path to use cmuxDebugLog only under DEBUG, and when a URL is
needed, pass it through browserNavigationDebugURL(...) instead of logging
responseURL directly. If the URL is not essential, drop it from the log message
entirely.

In `@Sources/Panels/CmuxWebView`+ScriptedDownloads.swift:
- Around line 149-161: The `CmuxWebView+ScriptedDownloads` hooks are still being
gated by `isMainFrame`, which prevents subframe blob-download tracking and
programmatic `anchor.click()` interception from working. Remove the
main-frame-only condition around the
`URLCtor.createObjectURL`/`URLCtor.revokeObjectURL` wrappers and the
`anchor.click()` interception path so the scripted-download hooks are installed
for subframes as well, while keeping the existing `originalCreateObjectURL`,
`originalRevokeObjectURL`, and click-hook logic intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 67bbc0ca-1f10-4de2-bd76-689b6e00a71c

📥 Commits

Reviewing files that changed from the base of the PR and between db86301 and 52d149e.

⛔ 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
  • Sources/Panels/BrowserPopupWindowController.swift
  • Sources/Panels/CmuxWebView+ScriptedDownloads.swift

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment on lines +9091 to +9093
guard !recentSubframeDownloadIntentKeys.isEmpty else { return false }
recentSubframeDownloadIntentKeys.removeFirst()
return true

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.

P1 Fallback consumes unrelated subframe intents

When the response URL doesn't match any recorded intent key, this branch removes the oldest intent — regardless of which subframe it belongs to — and returns true. On a page with multiple active subframes, a user click in subframe A (URL X) records an intent; if subframe B then receives a response for URL Y with Content-Disposition: attachment, the URL-mismatch fallback fires, consumes subframe A's intent, and grants allowsSubframeDownload = true to subframe B's response. The user never clicked anything in subframe B, but a download is triggered.

The redirect-following scenario (URL A → redirect → URL B) is the presumed motivation, but an "unreliable but better than nothing" fallback that can produce a wrong authoritative value is not acceptable. The fallback should be dropped; redirects on subframes that carry explicit Content-Disposition: attachment headers are rare, and the user-activation check in recordSubframeDownloadIntentIfNeeded already requires a real gesture, so failing closed is correct here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved in current head: the unrelated fallback consume path was removed; subframe response authorization now requires a recorded matching intent or redirect transfer and fails closed otherwise.

— Claude Code

austinywang and others added 2 commits June 28, 2026 20:04
- Surface Download/Print buttons in the omnibar when a PDF document is
  rendered (main frame or subframe), tracked via BrowserPanel
  .renderedPDFDocumentURL and navigation-delegate render callbacks.
- Force-download subframe navigation responses that carry explicit
  download signals (Content-Disposition: attachment / force-download
  MIME), and apply the insecure-HTTP subframe block only once a download
  is actually chosen. Update resolver tests for the new classification.
- Extract navigation popup policy, debug-URL helper, and the omnibar
  address button style out of BrowserPanel/BrowserPanelView into their
  own files; wire the new files into the Xcode project.
- Localize the new PDF download/print strings (en + ja).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts in registry/list files:
- Resources/Localizable.xcstrings: union of both branches' new keys
  (main's dock/paneMemoryGuardrail/automation strings + this branch's
  browser PDF/download strings); kept main's improved
  mobile.chat.error.transcriptNotReadable text.
- cmux.xcodeproj/project.pbxproj: union of both branches' file/build
  entries (main's BrowserPaneSplitTarget + this branch's PDF toolbar /
  popup-policy extraction files), re-normalized.
- .github/swift-file-length-budget.tsv: regenerated from the merged tree.

vendor/bonsplit advanced to main's published pointer. Swift compiles
clean (verified via tagged Debug build; the only build-phase failure is
the pre-existing command-palette nucleo Rust FFI script, which needs the
aarch64-apple-darwin target in Homebrew's cargo — an environment issue
unrelated to this change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
austinywang and others added 2 commits June 29, 2026 13:23
Resolve conflicts after main advanced (SSL "proceed anyway" #3711, etc.):
- BrowserNavigationDelegate.swift (real code merge):
  * didStartProvisionalNavigation: keep main's lastAttemptedRequest URL
    fallback plus this branch's print-reset + didClearPDFDocument().
  * shouldPerformDownload: keep this branch's subframe download gating
    (main-frame / user-activation / recorded-intent / insecure-HTTP
    block) and add main's clearAttemptedRequest() on the path that
    actually proceeds to .download.
  * keep this branch's WKWebView.cmuxRunPrintOperation() extension.
- cmux.xcodeproj/project.pbxproj: union of both branches' new files
  (main's SSL/error-page files + this branch's PDF/popup-policy files),
  re-normalized; no duplicate entries.
- .github/swift-file-length-budget.tsv: regenerated from merged tree.

Localizable.xcstrings auto-merged. Swift compiles clean (tagged Debug
build SUCCEEDED).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto-review caught that noteRenderedPDFDocument stored
renderedPDFDocumentURL for subframe PDFs too, so an embedded <iframe>
PDF (e.g. a Gmail/Drive-style preview) would show the omnibar
Download/Print buttons even though the visible page is not a PDF — and
Print runs on the main web view, printing the host page instead of the
embedded document. Gate on isMainFrame so only the top-level PDF drives
the toolbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +8852 to +8855
private func v2IsTerminalBrowserDownloadEvent(_ event: [String: Any]) -> Bool {
let type = event["type"] as? String
return type == "saved" || type == "cancelled" || type == "failed"
}

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.

P1 "ready_to_save" consumed key does not block the subsequent terminal event, poisoning the next automation call

When askWhereToSaveDownloads is true, v2WaitForDownloadEvent returns on "ready_to_save" and marks the consumed key as "ready_to_save\u{0}<downloadID>". Because v2IsTerminalBrowserDownloadEvent returns false for "ready_to_save", the bare downloadID is never stored in consumed. After the user dismisses the save panel, the resulting "saved", "cancelled", or "failed" event arrives at v2RecordBrowserDownloadEvent: v2ShouldStoreBrowserDownloadEvent checks consumed.contains(downloadID) (false) and consumed.contains("saved\u{0}<downloadID>") (false) — so the terminal event IS queued. The next browser.download call on the same surface pops it immediately as if a new download completed, with stale path/state from the previous interaction.

Suggested change
private func v2IsTerminalBrowserDownloadEvent(_ event: [String: Any]) -> Bool {
let type = event["type"] as? String
return type == "saved" || type == "cancelled" || type == "failed"
}
private func v2IsTerminalBrowserDownloadEvent(_ event: [String: Any]) -> Bool {
let type = event["type"] as? String
return type == "saved" || type == "cancelled" || type == "failed" || type == "ready_to_save"
}

Downloads already saved correctly to ~/Downloads, but the only feedback
was a transient toolbar spinner that vanished on completion, so finished
downloads were invisible and felt like failures.

Add a persistent downloads affordance:
- BrowserDownloadRecord: immutable per-download snapshot (id/filename/
  url/state/size) passed below the popover's ForEach boundary (no store
  crosses it, per the snapshot-boundary rule).
- BrowserPanel.recentDownloads (+ applyBrowserDownloadEvent, open/reveal/
  clear) fed from both download paths — the WKDownload navigation/response
  handlers and the session/context-menu path — via the existing event
  vocabulary (started/saved/failed/cancelled). Existing
  browserDownloadEventDidArrive posts are unchanged.
- BrowserDownloadsToolbarButton: omnibar button (spinner while active)
  opening a popover that lists recent downloads with Open / Show in
  Finder, an empty state, and Clear.
- Localized new strings (en + ja).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wnloads

# Conflicts:
#	.github/swift-file-length-budget.tsv
applyBrowserDownloadEvent originally dispatched its mutation to main with
the same DispatchQueue.main.async(execute:) dance as begin/endDownloadActivity,
which added a third instance of the budgeted non-sendable-closure concurrency
warning in BrowserPanel.swift and tripped the Swift warning budget gate.

All callers already run on the main thread (the WKDownload callbacks fire
inside a @mainactor Task / notifyOnMain, and the session path hops to main
before delivering), so fold the event synchronously with a main-thread
assert instead of re-dispatching. No new warnings; begin/endDownloadActivity
are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
austinywang and others added 2 commits June 29, 2026 16:39
The button rendered a static icon with a tiny scaled spinner that was
easy to miss, so an in-flight or finished download didn't read as
anything happening.

- Use a plain SF Symbol so `.symbolEffect` applies: a continuous bounce
  while a download is in flight, and a one-shot bounce each time one
  completes.
- Tint the button with the accent color whenever it is downloading or
  has downloads.
- Add a count badge (springs in) so completed downloads are visible at a
  glance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
austinywang and others added 2 commits June 29, 2026 21:30
The accent-colored button lit up automatically and read as too loud.
Make it match the rest of the omnibar instead:
- Monochrome icon; motion carries the state — a spinner while a download
  is in flight (a repeating .bounce would require macOS 15; the
  deployment target is 14), and a discrete bounce each time one lands.
- The count becomes a red notification bubble that clears when the
  popover is opened (tracks unseen download ids).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wnloads

# Conflicts:
#	.github/swift-file-length-budget.tsv
@austinywang
austinywang merged commit efc6a3a into main Jun 30, 2026
50 of 53 checks passed
azooz2003-bit added a commit that referenced this pull request Jul 3, 2026
… error-page URL + subframe download blocking), delete stale inline copy + dup debugURL, add insecure-HTTP helpers, wire pbxproj (round 42-fix)

This branch was successfully deployed

1 active deployment
Preview – cmux — c6c6b6d7 Deployed Jun 30, 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 pane: downloads still silently fail on Gmail/iframes and the PDF viewer Download button (post-#6258); should save to ~/Downloads like Safari

1 participant