webviews: popups draw one hairline, not two
Summary
Every web menu and popover now draws one hairline edge instead of two.
The coherence passes #20188 and #20213 added --acpmux-popup-elevation: a 0.5px --acpmux-composer-edge inset hairline layered over --ui-popup-shadow. --ui-popup-shadow (ui/popupSurface.css) already ends in its own 0.5px inset hairline, so the pane's popups drew two edges. Several of them (the context menu, slash and mention menus, the omni list, the summary, chat-menu, PR status and file menus) already stacked the composer-edge hairline before #20188; #20188 turned that into the shared token and moved the rest onto it.
--acpmux-popup-elevation now is --ui-popup-shadow, so the one remaining edge is the 0.5px hairline in --ui-popup-shadow: one subtle rim. The drop shadow is unchanged.
- The pages'
.ui-popup(ui.css) drew a 1px border over that hairline; it now draws no border. - The chat find bar keeps its 0.5px border (it takes the accent on focus) and casts
--ui-popup-drop, the shadow without the hairline. - appearance.borders none (
data-borders="none") now clears the rim (--ui-popup-edge: transparent), as it clears the composer's. - Under Increase Contrast the composer-edge hairline went to full ink; the single rim does the same (
--ui-popup-edge: currentColorin pages/shared/desktop.css).
Affected popups
All popups that use --acpmux-popup-elevation:
-
Base UI popups in the agent pane with no surface class (
:where(.ui-positioner) > .ui-popup) -
Context menu (
.ui-popup.ui-context-menu; New Tab All chats rows) -
Composer
@/cascade menu (.acpmux-menu), slash menu (.acpmux-slash-menu), context popover (.acpmux-context-pop) -
Model picker (
.acpmux-mp) and its cascade submenus (.acpmux-mp-cascade .acpmux-mp-sub), reasoning menu (.acpmux-effort-menu) -
Composer access/location menu (
.acpmux-access-menu) and the composer controls menu (composerControls.css) -
Agent-pane New Tab omni list (
.acpmux-omni) and New Tab popover (.nt-popover) -
Chat menu (
.acpmux-chat-menu-popover), PR status (.acpmux-pr-status), summary (.acpmux-summary-popover), changes file menu (.acpmux-file-menu-list) -
Tailwind
shadow-menu(no current users) -
Page menus and popovers (ui.css
.ui-popup): border dropped, the hairline stays -
Chat find bar (
.acpmux-find__bar): border stays, hairline dropped from its shadow
Testing
webviews/test/popup-one-hairline.test.ts(new) expands every popupbox-shadowthat uses--ui-popup-shadowor--acpmux-popup-elevationand requires exactly one inset hairline layer, and checks that the popups above are covered.- Red on the test commit:
heavy --mem 4G bun test test/popup-one-hairline.test.ts: 1 pass, 2 fail (--acpmux-popup-elevationand.acpmux-access-menuresolve to 2 hairlines). - Green on the fix commit: 3 pass, 0 fail.
- After review, the test also counts borders in the popup's rule and checks borders none. Red on 7101cbdab85e: 2 pass, 2 fail (ui.css
.ui-popuphas 2 edges; no borders-none rule). Green on 0feaeb3c3864: 4 pass, 0 fail.
- Red on the test commit:
bun test test/ui-rules.test.ts: 4 pass.scripts/cmux-next/check-system-blue.py: ok.bun run check: browser lanes, layers, type scale, system blue, text focus rings and format pass; thevp checklint/typecheck step was killed by earlyoom on big-red (memory pressure), so CI's react-apps-check covers it.test/css-braces.test.tsdoes not exist on this base.
Recording
Base (left: fleet DEV build of feat-cmux-next f16430018f28, after #20213) vs head (right: DEV build of 98e58941f8a8, same CSS as the current head) on a capture-slot mini, dark mode, showcase chat. Opening the model menu, the Mode menu, the slash menu and the chat actions menu; zoomed 2x. The slot display is 1x, so both 0.5px layers land on one pixel: on base the stacked rim reads brighter, on head it is the single faint rim. The model picker height differs because head includes later base changes (picker min height), not this PR.

Full quality:
Model menu corner at 6x (base left, head right):



Base reds on this head, not from this PR (same failures on feat-cmux-next 48d113b6cc47 locally): react-apps-check model-picker-height.test.tsx ("Expected: "148px""), new-tab-picker-clip.test.ts (menuTop -93.6), pane-english.test.ts (workspaceContext.ts:12). This PR also carries cmux-autofix's vp fmt of modelPickerLayout.ts, which is unformatted on base.
Changelog
Changed: Menus and popovers show a single hairline edge.
🤖 Generated with Claude Code