cmux-next Settings: sliders rest on the live value; floating Undo, Redo and Reset on Appearance
Leo's report on Settings > Appearance: dragging Opacity "just reverts to 'ghostty'", the surface sliders "jump the moment you click", and a wrong pick can't be taken back. This PR fixes the shared slider row and adds a floating Undo, Redo and Reset bar to Appearance and Theme.
Why the Opacity slider "reverts to Ghostty"
Nothing changed the theme or the picture. The slider's resting value was wrong, so the control lied about the window:
- Thumb and field resting on the wrong value. With a picture behind the window and Opacity unset, the window paints the work's own tint (about 35%). The page's slider sat on the host's "derived" value, which was the theme's opacity (Ghostty's 100%), and the field showed the label "Ghostty". The first click jumped from 100% to wherever you clicked.
- The top of the range snapped back.
WindowBackdropreads 100% over art as "use the work's own tint", so dragging to the top dropped the window back to about 35%. - The released value didn't stick. On release the editor cleared its drag value before the optimistic write landed, so the thumb and field went back to the unset value ("Ghostty"). A release over another element sent no write at all, because only
pointerupon the slider committed.
The surface rows had the same problem in a worse form. Their schema placeholder is 0 and the field showed "Window opacity", so the thumb sat at 0 while the surface showed the window's opacity.
What changes
- Unset sliders rest on the live value.
editors/NumberEditor.tsxand the newinheritance.ts:- A surface opacity rests on the window's opacity. The window Opacity rests on what the host derives, which
SettingsWindowService.shownOpacitynow computes from theWindowBackdropthe window paints, so it's the art's tint over a picture. - The value is dimmed, with a small badge under the control: "Inherits window" or "Automatic". The badge sits out of the layout flow, so setting a value moves nothing.
- Rows whose live value the page doesn't know keep their label as before. Those are the density metrics,
terminal.fontSizeandeditor.fontSize.
- A surface opacity rests on the window's opacity. The window Opacity rests on what the host derives, which
- Releases always commit. The native
changeevent commits a release wherever the pointer is, and the slider takes pointer capture. The thumb keeps the released value until the write answers. - The Opacity slider stops at 95% over a picture, because 100% there means "the work's own tint".
- Surface colors show the window color in the swatch with the same badge, instead of a truncated "Same as winc…".
- Popups in rows have one fixed width (180px) and ellipsize, so a selection never resizes the control. The Reset slot was already reserved.
- Floating bar at the bottom right of Appearance and Theme:
- Undo and Redo name their step, for example "Undo: Background → Shuffle Daily". Both are disabled when there's nothing to undo or redo.
- Reset is a menu with Restore How It Was (as Appearance was when it first showed in this visit) and Reset to Defaults. Each asks for confirmation inline and can itself be undone.
- Every row write, theme level, chosen image, link and museum artwork on those two pages is one undo step. The opacity and material keys are captured together, because the host writes them together.
- API for the Theme pane rework (leo/theme-pane-unify):
store.trackAppearance(keys, work, { label, themes })wraps a change as one step, and nested calls fold into the outer step.store.recordAppearanceChange(entry)records a step its caller already applied.
Audit of slider rows (kind: number, rendered as a slider):
- The 14
appearance.surfaces.*.opacityrows rested at 0 and now rest on the window's opacity. appearance.backgroundOpacitynow shows its derived number.- The unset density metrics (
appearance.metrics.*,layout.panePadding,layout.paneCornerRadius,layout.paneBorderWidth,sidebar.borderWidth),terminal.fontSizeandeditor.fontSizestill rest on their schema placeholder, because the page has no live value for them. They need host-derived values, left for a follow-up. - Every other slider has a default and always shows a number. All sliders get the release and no-snap-back fixes.
Not in this PR (follow-up, per the coordinator's order): the wallpaper tile grid and source row, presets, the Fine-tune collapse, Cmd-Z, the toast, recents, and the automatic fallback or safe mode.
Tests
bun test src/pages/settings/sliderRows.test.tsx(committed first, in ede78c62e8). Against the base source 5 of 6 fail: the thumb at 0, the "Ghostty" label, no write on a release off the slider, the snap-back, and no 95% cap. On this head 6 of 6 pass. The sixth, "an opacity write changes neither the theme nor the background", passes on both and stays as a guard.bun test src/pages/settings/appearanceUndo.test.tsx: the undo model, plus page tests for background pick, undo and redo, theme levels, Reset to Defaults, Restore How It Was and nestedtrackAppearance. 10 of 10 pass.bun test src/pages/settings: 145 pass and 2 fail. The 2 failures ("every page row has exactly one home" and "every schema row renders the editor for its kind") fail the same way at the base ce6670c4e6.- Swift:
WindowBackgroundOpacityTestsgaineddraggingOpacityKeepsTheThemeAndTheBackground(preview steps and the write keepappearance.theme,appearance.backgroundandGhosttyRuntime.themeOverride) andoverArtTheUnsetSliderSitsOnTheShownTint.
Observed / Not run
- Ran: the bun tests above on cmux16, red at the base and green on the head.
vp check(typecheck) shows one error, intest/markdown-reading-settings.test.ts, and the base has it too.vp fmtis clean,scripts/cmux-next/check-l10n.shreports 0 errors, andscripts/localization_catalog.py checkshows no new parity errors beyond the catalog's existingneeds_reviewstate. - Not run: the Swift tests and any Swift compile. cmux16's disk is full, so they run in CI (
cmux-next.ymldispatch). No live app run; the screenshots come from the gallery run.
Proof
Gallery variants pages.settings/appearance-undo-bar and pages.settings/surfaces-inherit-window; captures are linked below once the gallery run finishes.
Localization: 23 new settingsPage.* keys in CmuxNextSettingsWindow/Localizable.xcstrings with all 21 locales (machine translated, needs_review).
Changelog
- Fixed: Settings sliders rest on the value the window shows (a surface's opacity inherits the window's, the window's follows the background picture), never jump on the first click, and keep the value on release.
- Added: Undo, Redo and Reset (Restore How It Was, Reset to Defaults) floating at the bottom right of Settings > Appearance and Theme.
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes Settings > Appearance slider rows that rested on wrong values, jumped on the first click, and snapped back to "Ghostty" on release, and adds a floating Undo, Redo, and Reset bar to Appearance and Theme.
- Unset sliders now rest on the live value: a surface opacity rests on the window's, and window Opacity shows the value the host derives, dimmed with an "Inherits window" or "Automatic" badge.
- A release anywhere commits via the native
changeevent with pointer capture, and the thumb keeps the released value while the write is in flight. - Over a background picture the Opacity slider caps at 95%, because 100% there means "use the work's own tint."
- Surface colors show the window color in the swatch instead of a truncated default label.
- Appearance and Theme get a floating bar: Undo and Redo name their step ("Undo: Background → Shuffle Daily"), and Reset offers Restore How It Was or Reset to Defaults, confirmed inline and themselves undoable.
store.trackAppearanceandstore.recordAppearanceChangelet other parts of Appearance record their own undo steps.- The shown window opacity moved to
ThemeScope.shownWindowOpacityinCmuxNextDesignto keepCmuxNextAppunder its size cap. - The default binding for
key.agentPane.searchFiles(cmd+p) is removed from the defaults schema.