Skip to content

Fix browser image download filenames - #5938

Merged
austinywang merged 9 commits into
mainfrom
issue-5924-browser-download-image-wrong-extension
Jun 12, 2026
Merged

austinywang merged 9 commits into
mainfrom
issue-5924-browser-download-image-wrong-extension

Conversation

@austinywang

@austinywang austinywang commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5924

Summary

  • add a shared browser download filename/status resolver covered by regression tests
  • reject non-2xx browser download responses before showing a save panel
  • derive image download extensions from decoded image bytes or image URL paths instead of server MIME alone
  • reuse the resolver for native WKDownload save-panel filenames

Verification

  • reproduced the current logic with a local HTTP fixture: 403 application/xml was treated as saveable and Foundation MIME-derived filenames append .txt/.xml
  • swiftc -typecheck Sources/Panels/BrowserDownloadFilenameResolver.swift
  • ./scripts/lint-pbxproj-test-wiring.sh
  • ./scripts/check-pbxproj.sh

Note: local xcodebuild was not run per the task instruction to never run bare xcodebuild.


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


Summary by cubic

Fixes wrong image filenames when downloading from the in‑app browser. Adds byte/file type sniffing, preserves explicit names, rejects non‑2xx before the save panel, localizes the fallback name, uses Swift concurrency for a responsive save prompt, and replaces existing files atomically.

  • Bug Fixes
    • Added a shared filename resolver used by context‑menu downloads and WKDownload.
    • Derive image type via bytes/file/URL; preserve explicit base names; strip wrong extensions and apply the correct one; sniff native download images off the main thread and drive the save prompt with Swift concurrency.
    • Reject non‑2xx HTTP responses early; no save panel on errors; added resolver tests and safer filename sanitization.
    • Localized the fallback download name via browser.download.defaultFilename.
    • Replace existing files atomically when saving to avoid partial writes.

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

Review in cubic

Summary by CodeRabbit

  • New Features

    • Rejects non-successful HTTP responses before proceeding with downloads.
    • Improved filename selection: detects image types from data or downloaded file, normalizes/sanitizes names and extensions, and provides robust fallbacks for empty names.
  • Tests

    • Added tests for HTTP-status rejection, image-type detection, and filename-suggestion behavior (including extension handling).
  • Localization

    • Added a localized default download filename resource.

@vercel

vercel Bot commented Jun 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 Jun 12, 2026 3:28am
cmux-staging Building Building Preview, Comment Jun 12, 2026 3:28am

@coderabbitai

coderabbitai Bot commented Jun 12, 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 BrowserDownloadFilenameResolver to validate HTTP responses, detect image UTType from bytes or files, sanitize candidate filenames, and ensure image extensions match detected content; integrates into WKDownload and context-menu download flows, and adds tests, localization, and project wiring.

Changes

Download Filename Resolver

Layer / File(s) Summary
HTTP status validation and resolver contract
Sources/Panels/BrowserDownloadFilenameResolver.swift
BrowserDownloadHTTPStatusDecision enum and httpStatusDecision(for:) allow 2xx and non-HTTP responses, reject other HTTP status codes.
Image type detection and core filename API
Sources/Panels/BrowserDownloadFilenameResolver.swift
Detect image UTType from Data or downloaded URL using CGImageSource, and implement suggestedFilename(...) core overloads that pick a candidate, sanitize it, and call into extension reconciliation when an image type is available.
Filename sanitization and extension reconciliation
Sources/Panels/BrowserDownloadFilenameResolver.swift
imageFilename, stripping of trailing non-image extensions, base-name extraction, hasImageExtension(_:matching:), preferredFilenameExtension(for:) (maps JPEG→jpg, fallback img), and sanitized-filename helpers.
WKDownload (BrowserPanel) integration
Sources/Panels/BrowserPanel.swift
Adds DownloadState.sourceURL; decideDestinationUsing: uses resolver to validate HTTP status and pick a safe temp filename; downloadDidFinish(_:) re-resolves final save-panel filename using the downloaded temp file and stored sourceURL.
Context-menu download flow integration
Sources/Panels/CmuxWebView.swift
downloadURLViaSession gates on httpStatusDecision(for:) and computes saveName via the resolver using suggestedFilename, response, sourceURL, and downloaded data, replacing prior naive selection.
Resolver test coverage
cmuxTests/BrowserDownloadFilenameResolverTests.swift
Tests HTTP-status rejection, image type detection from PNG bytes, and suggested-filename behavior (preserving .png, stripping extra .txt).
Xcode build configuration & localization
cmux.xcodeproj/project.pbxproj, Resources/Localizable.xcstrings
Adds resolver and test file references to the project and a new localized browser.download.defaultFilename entry.
sequenceDiagram
  participant Client as CmuxWebView / BrowserPanel
  participant URLSession as NSURLSession
  participant Resolver as BrowserDownloadFilenameResolver
  participant SavePanel

  Client->>URLSession: fetch URL / network download
  URLSession-->>Client: (response, data)
  Client->>Resolver: httpStatusDecision(for: response)
  alt allow (2xx or non-HTTP)
    Resolver-->>Client: allow
    Client->>Resolver: suggestedFilename(..., imageData or imageFileURL)
    Resolver-->>Client: sanitized filename (correct image ext)
    Client-->>SavePanel: present save with filename
  else reject (non-2xx)
    Resolver-->>Client: reject(statusCode)
    Client-->>Client: fallback/cancel download action
  end
Loading

🎯 3 (Moderate) | ⏱️ ~18 minutes

🐰 I nibble bytes to see what's true,
No more .txt when PNGs pass through.
I trim and fix each filename's end,
I swap bad suffixes for what they intend,
Then hop away — your images defend. 🥕


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift @Concurrent ❌ Error BrowserPanel.swift defines @Sendable private nonisolated static func removeItemIfExists(...) async doing file I/O, called from @MainActor; it lacks @concurrent per swift-concurrent-annotation r... Annotate the nonisolated async helper with @concurrent (or restructure so file deletion happens without running the nonisolated async on the caller/main actor).
Cmux Swift File And Package Boundaries ❌ Error New 159-line BrowserDownloadFilenameResolver.swift (imports only Foundation/ImageIO/UTType) is reusable domain logic used by BrowserPanel & CmuxWebView but lives in app Sources/Panels, not a SwiftP... Extract BrowserDownloadHTTPStatusDecision/BrowserDownloadFilenameResolver into a small SwiftPM package under Packages/, add tests there, and have Sources/Panels depend on it; keep only UI/AppKit glue in the app target.
Cmux Swift Logging ❌ Error BrowserPanel.swift adds multiple runtime NSLog(...) calls in PR head commit ca592d0 (git log -L shows +NSLog lines from /dev/null), violating swift-logging rules. Remove those NSLog calls or wrap them in #if DEBUG (or replace with existing cmuxDebugLog), so no NSLog/debugPrint/dump reaches production runtime.
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.
Description check ⚠️ Warning PR description covers summary of changes and verification steps, but is missing required template sections for testing methodology and checklist. Add 'Testing' section describing manual verification and test cases; complete the 'Checklist' section with test status and bot review requests.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing image download filenames in the browser, which is the primary focus of the changeset.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #5924: HTTP status validation before save panel, image type detection from bytes/URLs, extension correction, and consistent filename logic.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue: the new resolver, HTTP status checks, image type detection, filename sanitization, tests, and localization strings stay within scope.
Cmux Swift Actor Isolation ✅ Passed New BrowserDownloadFilenameResolver types are marked nonisolated/Sendable; BrowserDownloadDelegate UI (NSSavePanel) is confined to Task {@MainActor}, while image sniffing runs in Task.detached util...
Cmux Swift Blocking Runtime ✅ Passed Resolver + updated save/download flow use Task.detached/@mainactor with no DispatchSemaphore/wait, Task.sleep, timers, polling, or main-queue sync in the resolver-related blocks.
Cmux Expensive Synchronous Load ✅ Passed Reviewed PR-touched Swift files for forbidden heavy sync loads (RestorableAgentSessionIndex.load / SharedLiveAgentIndex.shared); none found. Image sniffing runs in Task.detached off-main.
Cmux Cache Substitution Correctness ✅ Passed PR changes only filename/HTTP-decision logic and stores per-download in-memory DownloadState (tempURL/sourceURL) for save-panel naming; no persistence/history/undo/snapshot cached-vs-fresh substitu...
Cmux No Hacky Sleeps ✅ Passed PR changes only Swift files + pbxproj/xcstrings; no JS/TS/shell/build scripts modified. Evidence: PR shows no sleep/setTimeout/poll/timer usage and pbxproj has no “sleep”.
Cmux Algorithmic Complexity ✅ Passed New resolver only checks HTTP status and image type, with bounded filename-extension stripping (while removing final extensions); no scalable collection scans/sorts/filter/map in production hot paths.
Cmux Swift Concurrency ✅ Passed In BrowserPanel downloadDidFinish, PR uses Task/@mainactor plus awaited Task.detached for background image-type sniffing; BrowserDownloadFilenameResolver is synchronous and has no legacy DispatchQu...
Cmux User-Facing Error Privacy ✅ Passed CmuxWebView.swift/BrowserPanel.swift changes reject non-2xx via httpStatusDecision and use only fallback/cancel; no new NSAlert/recovery copy exposing upstream/vendor/provider details. Default file...
Cmux Full Internationalization ✅ Passed Uses String(localized:defaultValue:) for default filename key browser.download.defaultFilename; Localizable.xcstrings entry covers all 20 locales; PR hunks removed hard-coded save-panel "download"...
Cmux Swiftui State Layout ✅ Passed No SwiftUI state constructs found in PR files (no import SwiftUI, ObservableObject, @Published, @Observable, or GeometryReader); changes are AppKit/WKDownload filename/status handling.
Cmux Architecture Rethink ✅ Passed BrowserDownloadFilenameResolver is synchronous/pure (no sleep/poll/asyncAfter); BrowserDownloadDelegate changes use Task.detached for sniffing and an internal NSLock to guard activeDownloads, witho...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Scanned Swift for .identifier = NSUserInterfaceItemIdentifier("cmux.*"); 0 cmux.* assignments missing from cmuxAuxiliaryWindowIdentifiers (and BrowserDownloadFilenameResolver adds no NSWindow/N...
Cmux Source Artifacts ✅ Passed PR’s changed paths are only Swift source/test files, pbxproj, and xcstrings. None match forbidden artifact patterns (logs, temp dirs, DerivedData, caches, build/package-manager outputs).
✨ 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-5924-browser-download-image-wrong-extension

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Introduces BrowserDownloadFilenameResolver, a shared nonisolated struct that centralises HTTP-status gating, ImageIO-based image-type sniffing, and filename sanitisation for both the context-menu download path (CmuxWebView) and the WKDownload delegate (BrowserPanel). The WKDownload path correctly moves temp-file ImageIO work off the main thread via Task.detached(priority: .utility), removes legacy NSLog calls, and uses atomic replaceItemAt for destination overwrites.

  • New resolver (BrowserDownloadFilenameResolver.swift): rejects non-2xx responses, derives the correct image extension from bytes or a downloaded file, strips server-appended MIME extensions (e.g. .png.txt → .png), sanitises colons, and falls back to a fully-localised default filename with translations for all 20 supported locales.
  • WKDownload path (BrowserPanel.swift): moves ImageIO type-detection off-main, stores sourceURL in DownloadState (now Sendable), and replaces the old raw try?/NSLog move pattern with replaceItemAt.
  • Context-menu path (CmuxWebView.swift): rejects non-2xx before the save panel and delegates name resolution to the shared resolver using the already-in-memory image bytes.

Confidence Score: 5/5

Safe to merge — the core filename resolution logic is correct and both download paths behave properly; the single note is a minor edge-case in the extension-stripping loop that only surfaces for unusual double-extension filenames.

The resolver's sanitisation and image-type detection are correct for all common web image formats. The WKDownload path properly moves disk I/O off the main thread, and the context-menu path correctly gates on HTTP status before showing the save panel. The only gap is that strippingNonImageExtensions falls back to stripping only the outermost extension when an inner wrong-image extension blocks the loop, producing e.g. avatar.webp.png instead of avatar.png for an unusual double-suffix filename — a cosmetic output issue that does not affect file integrity or data safety.

Sources/Panels/BrowserDownloadFilenameResolver.swift — the strippingNonImageExtensions fallback path in imageFilename.

Important Files Changed

Filename Overview
Sources/Panels/BrowserDownloadFilenameResolver.swift New shared filename resolver: correctly sanitizes names, detects image types via ImageIO, strips server-appended extensions, appends the correct extension. One untested edge case in strippingNonImageExtensions produces oddly-named output when a wrong image extension appears beneath a non-image suffix.
Sources/Panels/BrowserPanel.swift WKDownload path updated: non-2xx responses rejected early, image-type detection correctly moved off-main via Task.detached(priority:.utility), replaceItemAt used for atomic destination overwrite, stale NSLog calls removed.
Sources/Panels/CmuxWebView.swift Context menu download path updated: rejects non-2xx before the save panel, delegates filename/type resolution to the shared resolver using the already-in-memory imageData variant.
Resources/Localizable.xcstrings Adds browser.download.defaultFilename with translations for all 20 supported locales (en, ja, zh-Hans, zh-Hant, ko, de, es, fr, it, da, pl, ru, bs, ar, nb, pt-BR, th, tr, uk, km) — complete coverage.
cmuxTests/BrowserDownloadFilenameResolverTests.swift Regression tests cover HTTP-status rejection, PNG sniffing, extension preservation, server-suffix stripping (logo.png.txt → logo.png), explicit base preservation, and non-image extension replacement; missing coverage for wrong-image-extension-under-suffix case.
cmux.xcodeproj/project.pbxproj Wires BrowserDownloadFilenameResolver.swift into the app target and BrowserDownloadFilenameResolverTests.swift into the test target; UUIDs follow the project's synthetic-key convention.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[URL session / WKDownload response] --> B{HTTPStatus 2xx?}
    B -- No --> C[reject / cancel download\nno save panel]
    B -- Yes --> D[BrowserDownloadFilenameResolver\nsuggestedFilename]

    D --> E{imageType available?}
    E -- No --> F[sanitizedFilename only\ncolon replaced, fallback localized]
    E -- Yes --> G[imageFilename]

    G --> H{filename already has\nmatching image ext?}
    H -- Yes --> I[return as-is]
    H -- No --> J[strippingNonImageExtensions\nstrip until matching ext found]
    J --> K{matching ext found\nafter stripping?}
    K -- Yes --> I
    K -- No --> L[strip final ext from original\nappend correct ext]

    F --> M[NSSavePanel.nameFieldStringValue]
    I --> M
    L --> M
Loading

Reviews (4): Last reviewed commit: "fix: replace browser downloads atomicall..." | Re-trigger Greptile

Comment thread Sources/Panels/BrowserPanel.swift Outdated
completionHandler(nil)
return
}
let sourceURL = response.url ?? URL(fileURLWithPath: suggestedFilename)

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.

P2 Semantic mismatch: filename string used as a file path

When response.url is nil, URL(fileURLWithPath: suggestedFilename) creates a file:/// URL from what is actually a bare filename string (e.g. "photo.jpg"). The resolver only uses this for lastPathComponent, so it works today, but the file:// scheme means any future caller that inspects .scheme or .host on sourceURL will get unexpected values. A URL(string:) or a direct URL(fileURLWithPath:) on FileManager.default.temporaryDirectory.appendingPathComponent(_:) would be more semantically accurate.

Suggested change
let sourceURL = response.url ?? URL(fileURLWithPath: suggestedFilename)
let sourceURL = response.url ?? URL(string: suggestedFilename.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? suggestedFilename) ?? URL(fileURLWithPath: suggestedFilename)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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

11-83: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Optional: Consider additional test coverage for edge cases.

The current tests validate the key regression scenarios from the PR objectives. For more comprehensive coverage, consider adding tests for:

  • .allow case for 2xx HTTP status codes
  • .allow case for non-HTTP responses (per upstream contract in Context snippet 1)
  • JPEG type detection and the special "jpg" extension mapping
  • Multiple-layer extension stripping (e.g., "logo.png.txt.xml" → "logo.png")
  • The "download" fallback filename when all candidates are empty

These are not required for the regression fix but would strengthen the test suite against future changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/BrowserDownloadFilenameResolverTests.swift` around lines 11 - 83,
Add unit tests to BrowserDownloadFilenameResolverTests to cover the suggested
edge cases: add a test that asserts resolver.httpStatusDecision(for:) returns
.allow for a 2xx HTTPURLResponse, a test that passes a non-HTTP URLResponse and
asserts .allow behavior per upstream contract, tests that validate
resolver.imageType(forImageData:) detects JPEG bytes and that
suggestedFilename(...) maps JPEG imageType to "jpg" extension, a test that
verifies multi-layer extension stripping (e.g., suggestedFilename
"logo.png.txt.xml" → "logo.png"), and a test that when all filename candidates
are empty the fallback filename is "download"; use the existing resolver
instance and the functions httpStatusDecision(for:), imageType(forImageData:),
and suggestedFilename(suggestedFilename:response:sourceURL:imageType:) to
implement these assertions.
🤖 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/BrowserDownloadFilenameResolver.swift`:
- Around line 93-101: The hardcoded fallback filename "download" in
sanitizedFilename(_:, fallbackURL:) must be localized; replace the literal with
a localized lookup (e.g. use NSLocalizedString with the key
"browser.download.defaultFilename" or your project's localization helper) in the
return path(s) where the function currently returns "download", and add matching
entries for browser.download.defaultFilename to Resources/Localizable.xcstrings
for every supported locale so the UI displays the translated default filename.

---

Outside diff comments:
In `@cmuxTests/BrowserDownloadFilenameResolverTests.swift`:
- Around line 11-83: Add unit tests to BrowserDownloadFilenameResolverTests to
cover the suggested edge cases: add a test that asserts
resolver.httpStatusDecision(for:) returns .allow for a 2xx HTTPURLResponse, a
test that passes a non-HTTP URLResponse and asserts .allow behavior per upstream
contract, tests that validate resolver.imageType(forImageData:) detects JPEG
bytes and that suggestedFilename(...) maps JPEG imageType to "jpg" extension, a
test that verifies multi-layer extension stripping (e.g., suggestedFilename
"logo.png.txt.xml" → "logo.png"), and a test that when all filename candidates
are empty the fallback filename is "download"; use the existing resolver
instance and the functions httpStatusDecision(for:), imageType(forImageData:),
and suggestedFilename(suggestedFilename:response:sourceURL:imageType:) to
implement these assertions.
🪄 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: f4611d20-8a22-4cbc-add9-635c5ca240d2

📥 Commits

Reviewing files that changed from the base of the PR and between dcab2aa and 5fde78b.

📒 Files selected for processing (5)
  • Sources/Panels/BrowserDownloadFilenameResolver.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/CmuxWebView.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserDownloadFilenameResolverTests.swift

Comment thread Sources/Panels/BrowserDownloadFilenameResolver.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 8863-8868: The code currently synthesizes sourceURL from
suggestedFilename when response.url is nil, which reintroduces wrong-extension
behavior; instead, leave sourceURL nil when response.url is nil and pass that
nil into filenameResolver.suggestedFilename (i.e. use response.url directly
rather than URL(fileURLWithPath: suggestedFilename)), and store the
DownloadState with the actual optional sourceURL (ensure DownloadState and
storeState accept an optional URL if needed) so URL-path fallback is skipped
when no real response URL was provided.
🪄 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: a56d3463-d1bb-4db0-9762-a035f211e54f

📥 Commits

Reviewing files that changed from the base of the PR and between 5fde78b and ea804d8.

📒 Files selected for processing (4)
  • Sources/Panels/BrowserDownloadFilenameResolver.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/CmuxWebView.swift
  • cmux.xcodeproj/project.pbxproj

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.

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 8906-8912: The current flow deletes destURL then moves
info.tempURL into place, risking data loss if the move fails; instead, check
FileManager.default.fileExists(atPath: destURL.path) and when it exists use
FileManager.default.replaceItemAt(destURL, withItemAt: info.tempURL) to
atomically replace the file, or alternatively move/copy info.tempURL to a
sibling temp destination and only replace/rename to destURL after the write
succeeds; update the code around the removeItem/moveItem calls (references:
FileManager.default.removeItem, FileManager.default.moveItem,
FileManager.default.replaceItemAt, destURL, info.tempURL) to follow this safer
replacement pattern.
🪄 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: df59165c-b135-491c-a29f-5a25c8e49fbc

📥 Commits

Reviewing files that changed from the base of the PR and between ea804d8 and 8c5d011.

📒 Files selected for processing (3)
  • Resources/Localizable.xcstrings
  • Sources/Panels/BrowserDownloadFilenameResolver.swift
  • Sources/Panels/BrowserPanel.swift

Comment thread Sources/Panels/BrowserPanel.swift Outdated
@austinywang
austinywang merged commit 6c1c475 into main Jun 12, 2026
26 checks passed
austinywang added a commit that referenced this pull request Jun 12, 2026
Aziz concurrency policy (autoreview): main-thread hops in new files
should use @mainactor structured concurrency, not DispatchQueue.main
.async — same modernization as the BrowserPanel download path (#5938).
The Task { @mainactor } hop preserves the load-bearing ordering: it is
enqueued synchronously and runs after the current runloop callout, i.e.
after AppKit's own per-scroll-view scroller-style reset.

The test drain switches from a main-queue round trip to a bounded
main-actor yield loop so it makes no cross-mechanism FIFO assumptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jun 12, 2026
… changes (#3241 sidebar scope) (#5955)

* Add failing test: sidebar scroller config must re-assert after preferred-style change

AppKit resets every NSScrollView's scrollerStyle to the new system
preference when NSScroller.preferredScrollerStyleDidChangeNotification
fires (mouse connect/disconnect, System Settings "Show scroll bars").
That clobbers the sidebar's forced overlay configuration with a legacy,
space-reserving scrollbar until some unrelated SwiftUI re-render happens
to re-run the resolver (#3241 reopen, sidebar scope; browser-pane scope
is PR #5847).

Moves SidebarScrollViewResolverView from ContentView.swift into
SidebarScrollViewConfigurator.swift unchanged (internal visibility) so
the test can exercise it; no behavior change in this commit, so the new
test fails: posting the style-change notification does not re-resolve.

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

* Re-assert sidebar overlay scrollers when the preferred scroller style changes

SidebarScrollViewResolverView now observes
NSScroller.preferredScrollerStyleDidChangeNotification and re-resolves,
re-applying SidebarScrollViewConfigurator's overlay configuration. The
async main hop in resolveScrollView() guarantees the re-apply runs after
AppKit's own synchronous per-scroll-view style reset, regardless of
observer registration order — mirroring the terminal's existing
handlePreferredScrollerStyleChange treatment of the same notification.

Both sidebar resolver call sites share this path, and the configurator's
guarded writes keep the re-apply a no-op when nothing changed, so the
knob-fade contract from the #3241 follow-up is preserved.

Fixes the sidebar scope of the #3241 reopen (browser panes: PR #5847).

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

* Move SidebarScrollViewResolverView to its own file

Autoreview: the extraction from ContentView.swift should follow the
one-major-type-per-file architecture rule rather than sharing
SidebarScrollViewConfigurator.swift with the configurator enum. No
behavior change; wires the new file into the app target.

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

* Observe scroller-style changes on the main queue

Greptile: queue: nil runs the observer block on the posting thread, so a
background-thread post would call the NSView method off-main. Use .main
— a main-thread post (AppKit's case) still executes the block
synchronously in place, so the re-apply ordering after AppKit's own
per-scroll-view reset is unchanged.

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

* Express the resolver's deferred hop with structured concurrency

Aziz concurrency policy (autoreview): main-thread hops in new files
should use @mainactor structured concurrency, not DispatchQueue.main
.async — same modernization as the BrowserPanel download path (#5938).
The Task { @mainactor } hop preserves the load-bearing ordering: it is
enqueued synchronously and runs after the current runloop callout, i.e.
after AppKit's own per-scroll-view scroller-style reset.

The test drain switches from a main-queue round trip to a bounded
main-actor yield loop so it makes no cross-mechanism FIFO assumptions.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jun 12, 2026
* test: cover browser download image filenames

* fix: validate browser image downloads

* fix: keep browser download adapters within budget

* fix: sniff native download images off main

* fix: localize browser download fallback name

* fix: preserve explicit browser download names

* fix: use Swift concurrency for download save prompt

* fix: replace browser downloads atomically

This branch was successfully deployed

1 active deployment
Preview – cmux — ca592d08 Deployed Jun 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.

Browser Download Image saves .txt/.xml instead of the image: HTTP errors unchecked and server MIME trusted for extension

1 participant