Repository navigation
cmux-next: smoother dialogs, AA contrast for every dialog button (macOS, web, iOS) - #27638
Conversation
…strip) DEBUG builds only. Reads the active window and its overlay panel (where dialogs draw) back to back while a debug.dialog fixture opens and closes, and reports per-frame card and scrim progress, intermediate frames, settle time, the largest progress step and the main actor's present and close time, with a PNG per frame. Evidence for the dialog motion work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A recorded frame is the window's backing pixels times the scale, so on a 2x display the card rect (points times scale) was half size and off the card. Derive the pixels per point from the first frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n, grow-in and key-window glass Regression tests for the alert polish, red on the base: - a dialog's destructive title and every key hint read at 4.5:1 on their fill over the card (the theme's danger is fit only to 3:1; over the dialog glass it measured 2.1:1 with debug.dialog_strip); - the default button's fill differs from Cancel's at least 3:1 (it was a gray 3 to 4% denser); - the card grows in from Motion.panelOpenScale on the appear spring, only fades under Reduce Motion, and a closed card starts whole again; - the card's glass keeps the key-window look (the close lit it up grey). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…utton Every cmux dialog (CmuxDialogCenter, about 100 call sites) draws through CmuxDialogView on WindowOverlayHost. Measured with debug.dialog_strip (window-server frames, before): - the card only faded, flat; it now grows from Motion.panelOpenScale about its center on the appear spring, as the palette opens (fade only under Reduce Motion), and shrinks toward panelCloseScale on fadeOut; - the scrim eases on the scrim token, the one the inactive-window tint under it uses, so the window darkens in one step, not two; - the close lit the card up grey for the whole fade: its overlay panel gives key back as the fade starts and the glass took its inactive frost. The card's glass keeps the key-window look (keepsActiveLook, as the window backdrop does); - the destructive title read 2.1:1 over the glass (the theme's danger is fit to 3:1). CmuxDialogButtonLook fits every ink to 4.5:1: the default button is the solid accent fill with on-accent ink (the web pages' .primary), a destructive one a solid danger fill (the web's .danger; Debug Settings 'Filled destructive dialog button' off shows the tinted variant), cancel and normal the selection gray. The focus ring moves just outside the button so it shows on a solid fill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A page dialog (ui/Dialog) only faded, and its backdrop shared the card's 120 ms fade. As the native dialog now does: the dialog grows from 0.97 while it fades in and shrinks toward 0.98 as it fades out; the backdrop eases on the standard (scrim) step. Reduce Motion (system or the app's data-cmux-motion=reduced) keeps only the fades; popup-reduce-motion covers both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text-entry alerts (Rename, New Group, Create Team, Join, Send, Set, Open, Save, Create) paired a Cancel button with an unmarked confirm, so iOS drew Cancel bold and Return pressed nothing. The confirm is now the preferred action (.keyboardShortcut(.defaultAction): bold, Return), and Message Not Unsent's Try Again its preferredAction. HIG Alerts: place the button people are most likely to choose as the default. Destructive confirmations keep their role and are never the default. The iOS alerts are system alerts: their animation and colors are the system's, and the root tint is the label color, so their buttons already read at full contrast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…parent Red on the base: in about half the opens the window server showed the whole dialog and scrim for one frame before the fade (debug.dialog_strip, base build too), because attaching the panel displays its layers at full opacity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Attaching the overlay panel (syncPanel) displays and commits its layers at once, so in about half the opens the window server showed the whole dialog and scrim for one frame, then the fade began from zero: a flash. The card and scrim go transparent, with no implicit animation, before the panel attaches; a dialog shown again mid-close keeps what is on screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e its fill Red at the previous head: the solid default button showed no focus ring in the tagged build (debug.window_snapshot). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ring layer, added in init, did not survive AppKit giving the button its backing layer, and the button could clip it. It joins the current layer on layout and the button no longer clips, so keyboard focus shows on the solid default and destructive buttons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Motion.set wrote the model value and added the animation in two transactions. With none already open (a socket, timer or task turn), each commits to the render server on its own, so a frame could show the new model value before the animation that starts from the old one. A cmux dialog flashed in at full opacity for one frame, then faded in from zero: 1 open in 5 after the panel-attach fix (debug.dialog_strip, window-server frames). Every spring, fade and timed set now runs in one outer transaction, which fixes the class for every gated layer animation, not only dialogs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…drew Red at the previous head: ordering the panel in showed its old surface (the last dialog at full opacity) for a frame before the new dialog's fade (debug.dialog_strip: 2 in 10 opens with a fresh attach, 0 in 20 with the panel already attached). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ordering the overlay panel in shows its window surface at once, and that surface still holds what the panel last drew on screen, often a dialog at full opacity, before this turn's commit reaches it: one frame of an old dialog and scrim, then the new one fading in from zero (debug.dialog_strip: 2 opens in 10 when the panel attached fresh, 0 in 20 while it stayed attached). The panel orders in transparent and shows after two display frames (FrameClient on its display link, no timer); a window on no screen shows it at once. An attached panel is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… timer From the correctness review: - Cancel and normal buttons keep the selection fill's hue on hover and press (an accent theme's selection is tinted; pressedFill is gray). - The focus ring leaves a 2 pt gap, so it reads apart from the accent default button's fill. - readable() repeats until the ink reads on every card surface (a move for one surface could lower another). - debug.dialog_strip keeps its pause timer alive across the await. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not cut From the correctness review of the dialog motion: - the grow-in is the scale property, so a dialog placed with a transform (Home palette and forms: translateX(-50%)) keeps its place, and gets the grow-in too; under reduced motion it no longer slides sideways; - a sheet (also .ui-dialog) only fades again; - the backdrop's close matches the dialog's: Base UI unmounts both when the dialog's transitions end, which cut the longer backdrop fade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…x-next-alert-polish # Conflicts: # cmux-next-ios/CmuxMobileKit/Sources/CmuxMobileWorkspaces/TabMoveAndGroupSheets.swift
…pending reveal on teardown After merging feat-cmux-next: - Rename Pane, Rename Screen, Rename Tab Group and Set Tab Group Icon (new in #27084) make their confirm the preferred action, as the other text-entry alerts do here (HIG Alerts). The old Rename Tab Group alert in TabMoveAndGroupSheets is gone on the base. - A window that closes while its overlay panel waits for its first frames stops that reveal (the base now closes the panel on teardown). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot needs on-demand usage enabledBugbot 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. |
Correctness review (subagent, correctness-first)Reviewer verdict on 7d5567c3e734: REVISE. Every finding was fixed afterwards:
The reviewer checked these and found them fine: fade-out cleanup ( After the fixes, all 18 dialog, motion and popup suites pass at the head. The base merge (4892940) brought in teardown changes to |
|
Found 5 test failures on Blacksmith runners: Failures
|
…ach cancels the detach Replaces the fresh-attach reveal test. Red at the previous head: the panel attached transparent (alpha 0) for two frames and left in the turn its content did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de it on attach Review of c646e47: hiding a freshly attached panel for two frames also hid the page planes (focus ring, inactive-pane dim) and the agent cursor it carries when the first browser page appears, and delayed every first overlay in a window by two frames. The stale surface comes from the detach, so the fix goes there: the panel leaves its window only after AfterFrameCommit, once the frame that draws it empty has committed (a re-attach in between cancels the detach). Any later attach shows at once, with no hidden frames, no display-link client and no timer. Teardown stays synchronous. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Gallery before/after for
States
Pick other states with a Probably yours: CI failed on
Failures:
Matched log linesNot re-run automatically: Written by red: web / Web status; a job it gathers is red on this change (its line here); do not merge until green; see lane-rules Shipped in:
|
Default changesNo defaults change at |
…s outside it From the visual review of #27638: the focus gap was a band inside the button's edge, so a focused solid button (Save, Don't Save) looked 3 px smaller on every side and its edges no longer lined up with its neighbors'. The gap is now a 1 pt band just outside the fill, then a 1.5 pt ring: 2.5 pt past the edge, still 1.5 pt clear of a button 4 pt away (the neighbor test holds). A translucent fill shows no gap band; its ring sits in the same place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review (codex gpt-6-astra high fast, drain lane): SHIP at c45470c No blocking findings. |
…ext-alert-polish # Conflicts: # Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tunables/ChromeTunables.swift
Visual review of #27638 (real-app capture at c45470c): the light focus ring was the theme's focus ring as is, rgb(134,134,134) on the card's rgb(223,223,223), 2.73:1, under WCAG's 3:1 for a focus indicator. On a gray Cancel it is the only sign of focus. CmuxDialogButtonLook now fits it to 3:1 on both card surfaces, as it fits titles to 4.5:1; dark (3.81:1) and an accent ring that already reads keep their color. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ally renders Real-app capture at 1dcfb3f: the light focus ring still read 2.66:1. Inks were fit to an estimate of the glass card (the veil over the scrimmed window, about rgb 241), but Liquid Glass shows little of its veil and the light card renders as the scrimmed window, rgb(223,223,223). cardSurfaces now includes the scrimmed window, so every title, hint and the focus ring are fit on it too, and the focus gap takes its color. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shipped style The defaults guard flagged the new Debug Tunable (dialog.filledDestructive, on) and, through a local variable named `color` (also a Tunable helper's name), CmuxDialogButtonLook.readable and minimumContrast. The toggle only compared two styles for the design call; filled was already what ships. The tinted alternative stays as CmuxDialogButtonLook's `filledDestructive: false`, covered by its contrast test, for #13742. The local is renamed. Nothing anyone gets changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Correctness review 3 (subagent): commits after 61ef179, through the base mergesVerdict: SHIP, with two low findings, both fixed in ed9cff0:
Checked and fine: pending-detach re-adoption and reorder; Visual review (real-app captures)
Defaults guardThe Debug toggle |
…keups fast gates on 59993c3: CmuxNextDesignTests imported CmuxNextWakeups (for AfterFrameCommit) without declaring it, which the base's package dependency rule now refuses. The test waits on the same signal with a one-shot CoreFoundation before-waiting observer instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot needs on-demand usage enabledBugbot 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. |
Dialog latency bench (after merge)
The merge-base run failed during checkout ( |
…(build skew of #28266 and #27638) #28266 turned GlassPanelView(keepsActiveLook:) into GlassPanelView(activeLook:) while #27638 added Glass.makePanel/makeOverlayPanel(keepsActiveLook:) for the dialog card. Both merged, and feat-cmux-next stopped compiling (Glass.swift:79 extra argument 'keepsActiveLook'). Glass.makePanel, Glass.makeOverlayPanel and OverlaySurfaceView now take activeLook: ActiveLookScope?. CmuxDialogView takes it from CmuxDialogCenter, which passes the dialog window's backdropActiveLook. An app-scoped dialog (no window) follows the key state. Compiled: 9b4cea495b0bc379ed0794d1
|
Base red from this PR: |
…5592) and dialog grow-in (#27638) (#28718) * webviews: fix the react-apps-check base red from Settings > Menus (#25592) and dialog grow-in (#27638) feat-cmux-next's react-apps-check (1) and (3) went red again after #28601, from two newer landings: #25592 (Settings > Menus, 451c25b) added CSS that fails four checks: - check:design-tokens / check-design-tokens: 12 new off-scale spacing and size literals. Spacing is now on the scale (--ds-space-1/2/4, or 0 for the 1px row gaps). The three icon and dot sizes (16px icon, 12px twisty, 6px edited dot) are not control heights; they go in the baseline. - check:layers / check-layers.test "this checkout passes": the dragged row's raw z-index: 2 is var(--layer-raised). - popup-one-hairline: the dragged row cast var(--ui-popup-drop), a popup token, with no hairline. A lifted row is not a popup, so it casts var(--elevation-2), the same drop that --ui-popup-drop resolves to. - gallery-control-surfaces (pages.settings-menus): hovering a row's icon button lit the whole li.menu-row. The row's hover wash now skips while one of its own icon buttons is hovered; the button lights alone. #27638 (smoother dialogs, 0b0968d) gave the dialog's scale and the backdrop's fade the --cmux-motion-standard step directly, so New Tab's no-entry rule (--ui-popup-open: 0s) no longer stopped them (new-tab-no-entry-menus: dialog [opacity, scale]). They now take --ui-dialog-open, which noEntry.css sets to 0s like --ui-popup-open. Exits keep --ui-popup-close. check-design-tokens.ts and check-layers.py: fail at the base, pass here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64 * webviews: Settings > Menus hover exclusion keeps a dragged row's opaque fill Review of 20d7aed (router-review, P2): :not(:has(...)) raised the hover rule to (0,4,0), over .menu-row[data-dragging] (0,2,0), so a row dragged by its grip took the translucent hover fill. The exclusion is in :where(), so the rule weighs (0,1,0) and the dragged row's fill wins again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64 * webviews: ContextMenusCard's two oxlint findings carry reasoned disables (web lint (fast) base red) web lint (fast) fails on every feat-cmux-next push whose range reaches ContextMenusCard.tsx (#25592): jsx-a11y/no-autofocus at the rename field (its suppression was a biome-ignore, which oxlint does not read) and jsx-a11y/prefer-tag-over-role at the separator line. Both now use the oxlint-disable-next-line form with a reason, as AgentHarnesses.tsx and DevicesCard.tsx do: the rename field opens on the person's Rename, and the divider sits inside the draggable row's flex body, where an <hr> would bring its own box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Summary
Polishes every cmux-next confirmation dialog (macOS native, about 100 call sites through
CmuxDialogCenter), the web pages'ui/Dialog, and the iOS alerts: smoother open and close, and every dialog button at WCAG AA.Measured with a new DEBUG tool,
debug.dialog_strip: the window and its overlay panel read back to back from the window server (about 21 ms apart) while a dialog opens and closes, with per-frame card and scrim progress.Found on the base build (5 runs per fixture, dark; 3 per fixture, light):
Motion.setcommitted the model value and its animation in separate transactions.dangeris fit only to 3:1. Key hints were 4.13:1. The default button's fill differed from Cancel's by 1.06:1 (light) and 1.12:1 (dark): Return's button did not stand out. These are the base readings ofCmuxDialogLightContrastTests.Changes
WindowOverlayHost+Present): the card grows fromMotion.panelOpenScaleon theappearspring and shrinks towardpanelCloseScaleonfadeOut, as the palette does. Under Reduce Motion only the fades remain; with animation off the dialog shows in one frame and completions still run. The scrim eases on thescrimtoken, the one the inactive-window tint uses. Card and scrim are transparent before the panel attaches.Motion+AppKit): every spring, fade and timedsetcommits its model value and its animation in one transaction. This fixes the one-frame flash for every gated layer animation, not only dialogs.keepsActiveLook, as the window backdrop does).CmuxDialogButtonLook):.primary..danger. The tinted alternative (red text on a faint red wash, also fit to 4.5:1) stays in code asCmuxDialogButtonLook(filledDestructive: false)for the design call; there is no Debug toggle, so the defaults guard sees no new default.clipsToBounds.ui/popupSurface.css): the dialog grows in and shrinks out with the same scales; the backdrop eases on the standard (scrim) step; Reduce Motion (system or the app'sdata-cmux-motion=reduced) keeps only the fades.WindowOverlayHost): the panel leaves its window only after the frame that draws it empty has committed (AfterFrameCommit; a re-attach in between cancels it). Before, it was ordered out with its last dialog still on its surface, and the next attach showed that for a frame (2 in 10 opens when the panel attached fresh; none while it stayed attached). An attach is never hidden, so page planes, the agent cursor and first overlays show at once..keyboardShortcut(.defaultAction): bold, Return), as is Message Not Unsent's Try Again.debug.dialog_strip {open, close, hold_ms, scale, dir}.Capture
Viewed after image: yes
Visual verdict: Yes, the after stills read as a clean native dialog next to ChatGPT and Claude, with a solid default button, a clearly destructive red button, every label at 4.5:1 or better and a focus ring that clears its neighbors.
No capture:
cmux-ci capturefor this PR built the head (jobs f369165f4e5b and 8fc7292a788f), but no capture mini could be leased. cmux14, cmuxs-mac-mini-3, cmux9s and cmux10s rejected SSH authentication ("verify the fleet key and owner"), the others stayed busy, and the Blacksmith fallback found no runner (manaflow-ai/cmuxterm-hq run 41141384309).Real-app captures from the tagged DEV builds are in the PR comments and the visual reviews: before/after stills of both dialogs in light and dark, the focus ring, and frame-by-frame open/close strips from
debug.dialog_strip. The last real-app stills are from 1dcfb3f. Since then only the focus ring color has changed, now fit to 3:1 on the card as it renders, plus non-visual code.CmuxDialogLightContrastTestschecks that against the light card color measured in that capture, rgb(223,223,223); it is red at 44f77e2 (2.63:1) and green at the head.Performance
debug.dialog_strip, tagged DEBUG buildsalertbasevsalertpolish, same window and display, 10 dark runs per dialogdebug.dialog_strip, tagged DEBUG buildsalertbasevsalertpolish, same window and display, 10 dark runs per dialogdebug.dialog_strip, tagged DEBUG buildsalertbasevsalertpolish, same window and display, 10 dark runs per dialogdebug.dialog_strip, tagged DEBUG buildsalertbasevsalertpolish, same window and display, 10 dark runs per dialogdebug.dialog_strip, tagged DEBUG buildsalertbasevsalertpolish, same window and display, 10 dark runs per dialogThese laptop numbers are not a controlled A/B; read them as unresolved. The branch app's state drifted from the base app's: it gained a browser workspace with a live page and a second window, which the base app never had. Across the branch's own batches the median present time ranged from 8.5 ms (an earlier build) to 26 ms (the later ones, in the drifted app), against 11.5 to 17 ms on base. DEBUG builds with the recorder running are also not budget figures. The controlled measurement is the fleet run of
CmuxDialogLatencyBench(dialog build and layout, where this PR adds CPU: the per-button color fit) at the merge base and at the head; results below. No fleet STRESS bench of the present path was run.Not finished at merge: the
CmuxDialogLatencyBenchruns (CMUX_DIALOG_BENCH=1, merge base vs head) were still running on the fleet when this merged. Their numbers will be added as a PR comment. Until then there is no controlled perf evidence either way.Owners
PL-motion-cef (
Overlay/WindowOverlayHost*.swift) and the Performance Lead (Motion/):Motion.setnow commits its model value and animation in one transaction, and the overlay panel detaches one commit later, after it has drawn empty.Design decision for Lawrence (#13742)
Filled (shipped) vs tinted (in code, not exposed). Posted with a recommendation for filled on #13742.
Changelog
Verification
Regression tests, red then green (
cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg=suite --arg=Packages/macOS/CmuxNext --arg=<suites>, Xcode 26.6):keepsActiveLookfalseclipsToBoundstrueAt the head (a56a777), 22 suites pass: WindowOverlayHostClose, AgentCursorGlassBlur, AgentCursorTagClick, FullScreenTitlebarChild, DialogFade, CmuxDialogLightContrast, FloatingGlassStyle, FloatingSurfacesStayGlass, DialogKeyHint, HoverCardPanelMotion, MotionCategoryPolicy, MotionCommitProbe, MotionMasterSwitchSurface, MotionPathValueKind, MotionPathValues, MotionScale, MotionScreen, PagePopupMotion, PopupGrowMotion, PopupReduceMotion, ThinScrollerFade and CmuxToastSurface.
Motion.setand the panel detach affect every gated layer animation and every overlay, so the motion, popup, hover card, toast, agent cursor and full-screen titlebar suites are included. App overlay suites also pass: FullScreenTitlebarPageMode, PageGeometryPost, StripScrollbarAbovePage and PopupSurface. AgentCursorGlassBlurTests caught a regression in an earlier detach version, fixed in 5007c02.Live measurement,
debug.dialog_stripon two tagged builds (basealertbase, branchalertpolish), same window on the same display:Observed / Not run
Observed:
prepush-gate.pyclean,scripts/web-lint.shclean.alertbaseandalertpolish, driven through the debug socket on the agent display only, never focused. 70 recorded opens and closes, light and dark, plus full-resolution stills.cmux-next-ios-sim.sh buildat the head built, installed and launched in an iPhone 17 Pro simulator (iOS 26.5).transform; sheets snapped to a scale) and 4 minor native ones (debug timer lifetime, Cancel's hover hue in accent themes, the focus-ring gap, multi-surface ink fit), all fixed. The second reviewed those fixes and the base merge. It found that hiding a freshly attached panel also hid page planes and delayed first overlays, so the fix moved to the detach (2ede0f5, 5007c02).Not run:
popup-reduce-motion(updated forscale). Freestyle needs a key this Mac doesn't have, the receipts and gallery hosts don't resolve from here, and the fleet only runs the full webview suite.🤖 Generated with Claude Code