Skip to content

cmux-next: place a background picture by hand (drag, zoom, reset) - #21743

Closed
teamleaderleo wants to merge 30 commits into
feat-cmux-nextfrom
leo/next-place-background
Closed

teamleaderleo wants to merge 30 commits into
feat-cmux-nextfrom
leo/next-place-background

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Phase 6 of the backgrounds plan (briefs/next-backgrounds-plan.md): place a background picture by hand.

Stacked on #21709 and #21731. It needs the placement math from the first and the Settings page provider from the second, so this branch is #21731's head with #21709 merged in. Review only the last commit, 93f4995. Once both merge, this PR shows only that commit.

What changes

  • Adjust Placement… in Settings > Background. It's offered for a still picture, not for None, Desktop or videos.
    • It puts BackdropAdjustOverlay over the frontmost window that shows a picture, and fades the window's content to 12% so the art shows.
    • Drag moves the picture. Scroll or pinch zooms (1×–4×).
    • Reset goes back to the automatic subject framing from phase 1. Done, Escape or Return ends it and brings the content back.
    • With no window showing a still picture, the page says so (cmux.settings.backdrop.not_adjustable).
  • The placement (BackdropUserPlacement) is the image point at the middle of the visible span, plus a zoom past the cover fit.
    • The art always still covers the window: a placement past an edge slides back.
    • It starts from the framing on screen (userPlacement(from:)), so the first drag doesn't jump.
  • Saved per background id in background-placement.json, next to the Backgrounds folder (BackdropPlacementStore), and written off the main actor. Every window showing that picture follows it live, through didChange.
  • CmuxNextApp unchanged. The op is cmux.settings.backdrop.image.adjust on BackdropImagesPageProvider, and the overlay lives in CmuxNextDesign. There's no catalog entry (WINDOW); a palette and right-click "Adjust Background…" comes with phase 5.

Strings

All 21 locales:

  • backdrop.adjust.hint, backdrop.adjust.reset, backdrop.adjust.done (Design)
  • settingsWindow.backdropPicker.adjust, adjustHelp, notAdjustable (Settings page)

Tests

  • CmuxNextDesignTests/BackdropUserPlacementTests:
    • the person's point sits mid-span, the zoom is respected, an edge slides back to cover, and zoom is clamped
    • converting the automatic framing and back gives the same framing
    • the store saves, reloads and announces changes once
  • CmuxNextAppTests/BackdropAdjustOverlayTests:
    • in a real window, the content fades, zoom and drag move the art and save it for that background, Reset restores the automatic framing, and Done restores the content
    • a window with no picture refuses
  • CmuxNextPagesTests/BackdropImagesPageProviderTests: adjust refuses or places.
  • Web hostCards.test.tsx: the button runs the op, and isn't shown for None. All 129 settings tests pass.

Verification

  • vp fmt and vp lint are clean, and tsc has no settings errors.
  • lint-ios-package-conventions.sh is clean. CmuxNextApp source lines are unchanged.
  • Swift: not built locally (no Mac in this lane), so CI is the first compile. Swift tests need a full-ci or next-batch run, which I've asked the coordinator about.
  • Not dogfooded.

Not in this PR

  • Per-workspace placements. A placement is per picture, so a workspace with its own background gets that picture's placement.
  • Placing videos.
  • The menu and palette entry, which comes with phase 5's catalog change.

WINDOW

None.

Changelog

Added: Place a background picture by hand: drag to move it, scroll or pinch to zoom, and reset to the automatic framing.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Settings > Background now lets you place a still background picture by hand and add your own images from a link. Adjust Placement… fades the window's content (including views added while placing) and overlays a drag-to-move, scroll-or-pinch-to-zoom (1×–4×), reset-to-automatic-framing session; Done, Return, or Escape ends it and restores the content. Arrow keys nudge (Shift nudges further), +/− zoom, and 0 resets.

  • The placement is saved per background id in background-placement.json, followed live by every window showing the picture; it starts from the on-screen framing so the first drag doesn't jump, and removing a picture drops its placement.
  • Bundled art now names its subject box, and the picture zooms, slides, or shrinks so the subject stays clear of the sidebar and a docked agent chat.
  • The overlay opens in the workspace window (brought forward from a separate Settings window), one at a time app-wide; the action only shows for still pictures, and a window with none refuses with a page message.
  • Add from a Link downloads an https image or movie under a size cap and uses it as a picked file; your images get remove, Show in Finder, and a disk-usage tally computed off the main actor. File imports check the bytes decode or play before they are copied in.
  • CmuxNextApp is unchanged: the ops ride BackdropImagesPageProvider and the overlay lives in CmuxNextDesign.

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

View guided diff Turn on auto-fix

teamleaderleo and others added 8 commits October 18, 2026 14:55
…t chat

Every wide painting framed the same fixed point (0.5, 0.45), so with the sidebar
and a docked agent chat open the subject often sat under them, and wide or short
windows cut faces off. Each work now names its subject box in art/manifest.json,
and the art zooms, slides or shrinks (over a blurred fill) so that box lies in the
part of the window the columns leave visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out, not the App

CmuxNextApp may not grow (check-app-size). The sidebar and each workspace's
layout root now tell the window's backdrop where they cover the art
(WindowMaterialView.setCover), and the App is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > Background gets Add from a Link (https only, judged by its bytes,
50 MB image / 500 MB video caps), a remove button on each of your images (not
the one in use), what they take on disk, and Show in Finder. A picked, dropped
or linked file must decode or play before it is copied in. Dropping a file on
Settings no longer needs experimental appearance (the page already said to).
The ops live in BackdropImagesPageProvider (CmuxNextPages); CmuxNextApp shrinks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r edge correctly

Review fixes for the subject crop:
- keepsSubject is measured on the final placement, never assumed.
- A shrunk or slid image keeps the subject in the span first (the cover clamp
  overrode it), and the shrink fit counts the subject's height too.
- No vertical fallback that cannot fail; portrait subjects are the head and
  upper body (top 70%%), which fit a 21:9 window.
- The catalog test asserts the subject frame lies in the visible span.
- Only a column that comes or goes springs the art; resizes and live window
  resizes move it at once. Fill filters are made per view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eir name in their folder

Review fixes for Add from a Link:
- The link's name is sanitized (no folders, colons or leading dots), and the
  renamed file must stay in the download's own folder (an encoded ../ climbed out).
- A delegate-driven data task replaces download(from:delegate:), whose task
  delegate never saw progress: the declared length and the running count are
  checked against the cap for the declared type, so a huge body never lands.
- Redirects off https are refused; timeouts are a minute idle, ten in all.
- The temporary folder is removed on every failure.
- Remove refuses the background in use natively, not just on the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > Background > Adjust Placement… fades the window's content and puts
an overlay over it: drag moves the picture, scroll or pinch zooms (1x-4x),
Reset goes back to the automatic subject framing, Done or Escape ends. The
placement (the point at the middle of the visible span, and the zoom) is kept
per background in background-placement.json, and every window follows it.
It starts from the framing on screen, so the first drag does not jump.
CmuxNextApp is unchanged: the op rides BackdropImagesPageProvider.

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

cursor Bot commented Oct 18, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 9a11f133-d533-4164-85b9-1337de07cb1e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review cacfd74: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr21743 -f ref=leo/next-place-background -f platform=macos -f backend_mode=local

Probably yours: menu-focus.test.ts fails here but not on main's latest full suite. Seen on other PRs too (likely flaky): categories.test.ts, agent-pane-history-paging.test.ts, agent-pane-no-capsules.test.ts and 25 more.

CI failed on cacfd741f5 (run 39373729814 attempt 1): 4 code, 1 unknown.
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped.

Job Verdict Why
web / react-apps-check (1) code a web test failed
web / react-apps-check (3) code a web test failed
web / react-apps-check (2) code a web test failed
web / react-apps-check (4) code a web test failed
web / react-apps-check unknown no known signature; failed step: Check webview test shards

Failures:

  • seen on other PRs categories.test.ts every page row has exactly one home: failed (job)
  • seen on other PRs agent-pane-history-paging.test.ts a long chat opens on its tail and pages older turns in as the reader scrolls up, holding its place: failed (job)
  • seen on other PRs agent-pane-no-capsules.test.ts agent pane page > rounds its controls as small-radius rects, not capsules: failed (job)
  • seen on other PRs chat-find.test.ts Find in Chat > keeps a Find match in view while what is above it keeps measuring: failed (job)
  • seen on other PRs composer-edges.test.ts (unnamed): failed (job)
  • seen on other PRs composer-mention.test.ts (unnamed): failed (job)
  • seen on other PRs composer-quiet-chips.test.ts (unnamed): failed (job)
  • seen on other PRs narrow-layouts.test.ts the agent pane holds together at 280 px, scale 1: failed (job)
  • seen on other PRs markdown-table-keys.test.ts markdown table keys > Cmd-A selects the cell's text, then the whole table, then lets the document ta...: failed (job)
  • seen on other PRs new-tab-warm-first-frame.test.ts chromium: Cmd-T paints its final chats and project on its first frame: failed (job)
  • ... 19 more in the job logs
Matched log lines
web / react-apps-check (1): (fail) every page row has exactly one home [0.82ms]
web / react-apps-check (3): (fail) markdown table keys > Cmd-A selects the cell's text, then the whole table, then lets the document take it [32.62ms]
web / react-apps-check (2): (fail) New Tab strip, default, 760 px [1510.83ms]
web / react-apps-check (4): (fail) the New Tab composer at 420 px: the strip heads the card, one controls row inside [1719.09ms]

Not re-run automatically: web / react-apps-check (1), web / react-apps-check (3), web / react-apps-check (2), web / react-apps-check (4), web / react-apps-check are not machine failures.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

red: cmux-next CmuxNextApp size cap; no finished base run on 16f36fe to compare; do not merge until green; see lane-rules
red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 16f36fe to compare; do not merge until green; see lane-rules
red: web / Web status; no finished base run on 16f36fe to compare; do not merge until green; see lane-rules
red: web / react-apps-check; no finished base run on 16f36fe to compare; do not merge until green; see lane-rules
red: web / react-apps-check (1); no finished base run on 16f36fe to compare; do not merge until green; see lane-rules
red: web / react-apps-check (2); no finished base run on 16f36fe to compare; do not merge until green; see lane-rules
red: web / react-apps-check (3); no finished base run on 16f36fe to compare; do not merge until green; see lane-rules
red: web / react-apps-check (4); no finished base run on 16f36fe to compare; do not merge until green; see lane-rules

CI fast guards failed on cacfd741f5 (https://github.com/manaflow-ai/cmux/actions/runs/39373729814). It does not block the merge; a red guard merged into main breaks it for every open PR.
The log named no failed step; see the run.

Agents: python3 scripts/ci/guard_attribution.py fix applies the mechanical fixes locally.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-kaq56 created for this PR by the cmux coordinator.

teamleaderleo and others added 3 commits October 18, 2026 15:51
…re they start

Re-review fixes: LinkDownload keeps its state in a Mutex and is plainly
Sendable (no new unchecked conformance for the crash ratchet). A cancel that
comes before the task exists marks the download, and the task is cancelled as
soon as it is made, instead of invalidating the session first (making a task
on an invalidated session raises). A body that starts like a picture gets the
picture cap whatever type the server declared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blacksmith-sh

blacksmith-sh Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Found 34 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpys6e32er/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmpyuxpod8d/summary-fail.json (live):/summary-fail.json (live): View Logs
src/pages/settings/categories.test.ts/categories.test View Logs
src/pages/settings/categories.test.ts/categories.test View Logs
test/agent-pane-history-paging.test.ts/agent-pane-history-paging.test View Logs
test/agent-pane-history-paging.test.ts/agent-pane-history-paging.test View Logs
test/agent-pane-no-capsules.test.ts/agent-pane-no-capsules.test View Logs
test/agent-pane-no-capsules.test.ts/agent-pane-no-capsules.test View Logs
test/chat-find.test.ts/chat-find.test View Logs
test/chat-find.test.ts/chat-find.test View Logs

...and 24 more test failures. View all on Blacksmith: run 39373729814, run 39373729802

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Not verified: macOS jobs did not run: compile and app tests were skipped on 631b4d75f0; ci-status is green without them.

CI passes on 631b4d75f0 (run 39190286890 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

CI fast guards passes on 631b4d75f0 (https://github.com/manaflow-ai/cmux/actions/runs/39190286890). This covers only the fast guards, not CI: CI's result is the ci-status check, and the CI failure attribution comment names any failing test.

…ace window, saved in order

Review fixes for Adjust Placement:
- Compile: the store notification's object is compared by identity (the batch
  Swift run stopped on 'sending store risks causing data races').
- Saves run on one serial queue, in order; an unreadable file is kept beside.
- The overlay opens in the workspace window (WindowManager.active), never the
  Settings window; one at a time app-wide; it ends when its window shows
  another background, and fades views added while it is open.
- Keyboard: arrows nudge (Shift: further), + and - zoom, 0 resets.
- A drag past an edge has no dead travel; windows only re-place for their own
  background's change; removing a picture drops its placement; the page's
  'no picture' note clears when the background changes.
- init(coder:) returns nil instead of fatalError (crash ratchet).

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

cursor Bot commented Oct 18, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

…ace-background

# Conflicts:
#	Packages/macOS/CmuxNext/Sources/CmuxNextDesign/BackdropArtPlacement.swift
#	Packages/macOS/CmuxNext/Sources/CmuxNextDesign/WindowMaterialView.swift
#	Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/BackdropArtPlacementTests.swift
…dow comes forward

Re-review fix: the center's clamp had its bounds swapped, so for art that
covers the window every drag or arrow pinned it to an edge. The bounds are
now [middle / size, (middle - view + size) / size], and the test checks the
exact travel of a drag and the stop at the edge. Placing brings the target
window forward again, so from a separate Settings window keys reach it.

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

cursor Bot commented Oct 18, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

teamleaderleo and others added 4 commits October 18, 2026 22:03
The fleet compile found JSONValue (CmuxNextSettings) and revealInBackground
(CmuxNextCompat) used without their imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A public initializer's default argument may only use publicly imported API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo and others added 8 commits October 18, 2026 22:20
…utes in Double

Fleet compile fixes: BackdropArt has no id (its selection does), and the travel
assertions mixed CGFloat and Double.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gallery-coverage failed: BackdropLink had no entry. The Settings entry now
covers it, with a variant that opens the link form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The batch Swift run crashed (SIGTRAP in dispatch_assert_queue): the import
extension took the module's main-actor default, so usage()'s reduce closure
asserted the main queue when the page called it from a detached task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The batch Swift run failed aLinkNameCannotClimbOutOfTheDownloadFolder:
deletingPathExtension keeps "...png" whole on macOS 26, so the stem was "png".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The batch Swift run failed aDragMovesTheArtAndIsKeptForThatBackground. AppKit
sends no KVO change for subviews, so a view added while placing kept full
opacity; each window update now fades new ones. The test drags from the
middle (the automatic framing can sit at an edge) and expects a drag down
to move the center up the picture, as the code does.

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

cursor Bot commented Oct 20, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 0/5

[Medium impact] The change affects background framing in the Settings and workspace-window workflow.

Do not merge yet: the webview build remains broken, and Chromium panes can block placement.

Findings

  1. P0 Test helper ships with app ▶
  2. P0 Saves use a forbidden queue ▶
  3. P0 Placement data lacks an owner ▶
  4. P1 Browser panes cover placement ▶
  5. P1 Duplicate methods break the build ▶
  6. P1 Placement loses keyboard focus ▶
  7. P1 Workspace pictures cannot be placed ▶
  8. P2 First drag changes the framing ▶
  9. P2 Dragging stalls after zooming out ▶
  10. P2 Closed windows leave observers ▶
  11. P2 Failed saves stay hidden ▶

Summary

Adds manual placement for still background pictures, with drag, zoom, reset, and per-picture saves.

  • People can move and zoom a still background picture in its workspace window.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Settings["Settings: Adjust Placement"] --> Overlay["BackdropAdjustOverlay in parent window"]
  Overlay --> Store["BackdropPlacementStore"]
  Store --> Notification["didChange"]
  Notification --> Windows["WindowMaterialView updates"]
  Browser["Chromium child window"] --> Covers["Covers parent views and takes input"]
  Covers --> Overlay
Loading

Reviews (4) · Last reviewed commit: "Merge branch 'feat-cmux-next' into leo/n..." · Reviewed by Greptile

Comment on lines +366 to +370
async addBackdropLink(url: string): Promise<string | null> {
const reply = await this.request("cmux.settings.backdrop.image.link", { url });
if (!reply.ok) return reply.error.code || "cmux.settings.backdrop.download_failed";
await Promise.all([this.refresh(), this.refreshHost()]);
return null;

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 Duplicate methods break the build

SettingsStore now declares addBackdropLink, removeBackdropImage, revealBackdropImage, and backdropUsage twice. TypeScript rejects these duplicate implementations, so the webview type check and build fail.

Remove the duplicate methods and keep only the new adjustBackdrop method. Preserve the original addBackdropLink, which also records the change for undo.

}
// Brought forward: from a separate Settings window, the keys and pointer come here.
window.makeKeyAndOrderFront(nil)
window.makeFirstResponder(overlay)

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 Placement loses keyboard focus

begin(in:) gives the overlay keyboard focus without telling the window's focus model that an overlay opened. In a real ShellWindow, this reports the new responder. FocusResponderClassifier calls this view windowOrNone, which tells the focus model to put focus back on the current page or terminal immediately.

The overlay therefore loses the keyboard as it opens. Register placement with the existing overlay focus flow, and close that session in done().

Comment on lines +108 to +110
let scale = f.width / (c.width * imageSize.width)
let cover = max(viewSize.width / imageSize.width, viewSize.height / imageSize.height)
return BackdropUserPlacement(centerX: u, centerY: v, zoom: scale / cover)

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 First drag changes the framing

userPlacement(from:) cannot preserve automatic framing that slides beyond the window edge or shrinks below the cover fit. Those are supported results, including the bundled Böcklin case already covered by a test. The returned value clamps zoom to at least 1, and the manual renderer clamps the image back inside the cover bounds.

The first tiny drag therefore changes the framing abruptly instead of starting from what is on screen. Preserve these automatic starting states, or make the transition explicit before accepting a gesture.

Comment on lines +128 to +130
public func zoom(by factor: CGFloat) {
guard factor.isFinite, factor > 0 else { return }
save(BackdropUserPlacement(centerX: placement.centerX, centerY: placement.centerY, zoom: placement.zoom * factor))

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 Dragging stalls after zooming out

zoom(by:) keeps the old center even when zooming out makes that center impossible to show. The renderer slides the image back, but move(dx:dy:) still starts from the old center.

For a matching image and window shape, move to centerX = 0.125 at 4×, zoom out to 2×, then drag left slightly. The visible center is already 0.25, so the first drag only corrects the stored center and does not move the picture. Start each drag from the actual rendered placement, including after zoom or resize.

Comment on lines +68 to +70
/// Waits for every save queued so far (tests).
public func flush() {
Self.writes.sync {}

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.

P0 Test helper ships with app

flush() adds a test-only helper to production Sources/. Its comment names tests as its purpose, and its only callers are in BackdropUserPlacementTests. The repository rule prohibits new test-only helpers in shipping source.

Move this helper into the test target and expose only the internal state needed through @testable import.

Rule Used: Flag Swift files under a production Sources path (matching /Sources/ and not under /Tests/) that add a test-only or debug-only seam: a #if DEBUG (or other test-build-guarded) extension/member exposing internal state only for tests or a debugg... (source)

Comment on lines +62 to +65
Self.writes.async {
try? FileManager.default.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true)
try? data.write(to: file, options: .atomic)
}

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.

P0 Saves use a forbidden queue

BackdropPlacementStore introduces a custom background DispatchQueue for ordinary file saves. The repository's concurrency rule explicitly requires Swift concurrency for this work.

Put the ordered writes on an owned actor and expose an async completion point. Preserve the guarantee that older saves cannot overwrite newer ones.

Rule Used: Flag new legacy async patterns in cmux-owned Swift where Swift concurrency is the correct shape: DispatchQueue.global for ordinary async work, new Combine app state, completion-handler APIs fully under cmux control, or fire-and-forget Tasks with mean... (source)

Comment on lines +41 to +44
overlay.updateObserver = NotificationCenter.default.addObserver(forName: NSWindow.didUpdateNotification, object: window,
queue: .main) { [weak overlay] _ in
MainActor.assumeIsolated { overlay?.fadeAdded() } // main-proof: observer on queue: .main
}

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 Closed windows leave observers

begin(in:) registers a block observer that only done() removes. Closing the window during placement does not call done(), and the overlay has no other cleanup path. The notification center keeps the unused registration after the overlay goes away.

End placement when the window closes, or remove the observer when the overlay is torn down.

Comment on lines +63 to +64
try? FileManager.default.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true)
try? data.write(to: file, options: .atomic)

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 Failed saves stay hidden

BackdropPlacementStore.set silently ignores folder and file write errors. If the disk is full or the folder is not writable, the picture moves and Done appears to keep it, but the placement disappears after relaunch. Report failed saves through the app’s logging or error path instead of dropping both errors with try?.

Comment on lines +26 to +28
public static let shared = BackdropPlacementStore(file: BackdropArtDownloads.defaultFolder.map {
$0.deletingLastPathComponent().appendingPathComponent("background-placement.json")
})

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.

P0 Placement data lacks an owner

BackdropPlacementStore.shared adds a new singleton for mutable placement data. The repository rule requires this state to have an owner and be injected, rather than reached globally. A view can use an injected store while removing a picture still changes the global store.

Construct one store in AppServices and pass it to WindowMaterialView and BackdropImagesPageProvider, including the remove action.

Rule Used: Flag new ambient global state in production Swift: a top-level (file-scope) func used as API, a top-level mutable var or a stub class/struct holding a global flag/once-token, a caseless enum/empty struct used purely as a static func/static let namesp... (source)

Comment on lines +245 to +248
{!own &&
current !== "none" &&
current !== "desktop" &&
!backdrops.find((choice) => choice.id === current)?.video && (

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 Workspace pictures cannot be placed

The !own check hides Adjust Placement whenever This Workspace is selected. If the global background is None and a workspace has its own still picture, neither scope offers the button: All Workspaces checks None, and This Workspace always hides it.

Offer adjustment for the workspace's still picture too. Saving per picture does not require separate per-workspace placements.

Comment on lines +39 to +40
for view in content.subviews where view !== backdrop { overlay.fade(view) }
content.addSubview(overlay, positioned: .above, relativeTo: nil)

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 Browser panes cover placement

If Settings shares a window with a visible Chromium browser pane, that page sits above BackdropAdjustOverlay and takes clicks, drags, and scrolls in its area. The overlay is added to the parent window's view tree, below Chromium child windows. Fading the parent's subviews does not bring it above the page, which can also cover the placement controls.

Present placement through WindowOverlayHost, and hide or mask the browser content while placing.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

CI / web / react-apps-check (2) is red on 13 of 20 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

CI / web / react-apps-check (3) is red on 14 of 19 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

CI / web / react-apps-check (1) is red on 13 of 18 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

CI / web / react-apps-check (4) is red on 13 of 18 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Closing: superseded by #22562. The picture now aspect-fills the window and resizes with it, like a macOS wallpaper, so it needs no hand placement.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Placing a background picture by hand is not landing: BackdropUserPlacement, BackdropPlacementStore, BackdropAdjustOverlay, the Adjust Placement… button and its cmux.settings.backdrop.image.adjust op were dropped from the backgrounds batch, #22033, along with their strings and tests. #22562 (merged into feat-cmux-next) made the background aspect-fill the window, centered, with no framing logic, and deleted BackdropArtPlacement.swift and the visible-span math hand placement builds on, because framing made the picture jump while resizing. Bringing that back would take back #22562.

The rest of the batch (collages, every Apple aerial, video playback and Chief picture motion settings) lands in #22033. I'm leaving this PR open for its owner.

Agent session: cosmic-gray

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.

1 participant