From 2b0fe5687d09c3e87b72f751cf8fb77fcd8c4bc2 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 13 Oct 2026 23:06:12 -0700 Subject: [PATCH 1/8] webviews: test that every popup draws one hairline edge Red: --acpmux-popup-elevation stacks a composer-edge hairline on the one already in --ui-popup-shadow. Co-Authored-By: Claude Opus 5.5 --- webviews/test/popup-one-hairline.test.ts | 139 +++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 webviews/test/popup-one-hairline.test.ts diff --git a/webviews/test/popup-one-hairline.test.ts b/webviews/test/popup-one-hairline.test.ts new file mode 100644 index 000000000000..fe55d08de19e --- /dev/null +++ b/webviews/test/popup-one-hairline.test.ts @@ -0,0 +1,139 @@ +// Coherence passes #20188 and #20213 (2026-10-13) gave the pane's menus and popovers +// `--acpmux-popup-elevation`: a 0.5px composer-edge hairline over `--ui-popup-shadow`, which already +// draws a 0.5px hairline of its own. Those popups drew two edges. Every web menu and popover draws +// exactly one: the hairline in `--ui-popup-shadow` (ui/popupSurface.css). +// +// The check reads the stylesheets: each `box-shadow` that uses a popup token is expanded through +// the popup tokens' definitions and must hold one inset hairline layer. +import { describe, expect, test } from "bun:test"; +import fs from "node:fs"; +import path from "node:path"; + +const source = path.resolve(import.meta.dir, "../src"); + +const sheets = (dir: string): string[] => + fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const file = path.join(dir, entry.name); + if (entry.isDirectory()) return sheets(file); + return entry.name.endsWith(".css") ? [file] : []; + }); + +const css = sheets(source).map((file) => ({ + file: path.relative(source, file), + text: fs.readFileSync(file, "utf8").replace(/\/\*[\s\S]*?\*\//g, ""), +})); + +const POPUP_TOKENS = ["--ui-popup-shadow", "--acpmux-popup-elevation"]; + +// Every definition of a popup token, across every sheet. +const definitions = new Map(); +for (const { text } of css) { + for (const token of POPUP_TOKENS) { + for (const match of text.matchAll(new RegExp(`${token}\\s*:\\s*([^;}]+)`, "g"))) { + definitions.set(token, [...(definitions.get(token) ?? []), match[1].trim()]); + } + } +} + +// The argument list of the call that opens at `open` (the index of its "("). +const closing = (text: string, open: number) => { + let depth = 0; + for (let i = open; i < text.length; i++) { + if (text[i] === "(") depth++; + else if (text[i] === ")" && --depth === 0) return i; + } + throw new Error(`unbalanced parentheses in ${text}`); +}; + +// Expands `var(--popup-token[, fallback])` with the token's first definition; other variables stay. +const expand = (value: string, seen: string[] = []): string => { + let out = ""; + let rest = value; + for (;;) { + const at = rest.search(/var\(\s*--/); + if (at < 0) return out + rest; + const end = closing(rest, rest.indexOf("(", at)); + const call = rest.slice(at, end + 1); + const name = call.match(/var\(\s*(--[\w-]+)/)![1]; + const definition = definitions.get(name)?.[0]; + if (POPUP_TOKENS.includes(name) && definition && !seen.includes(name)) { + out += rest.slice(0, at) + expand(definition, [...seen, name]); + } else { + out += rest.slice(0, end + 1); + } + rest = rest.slice(end + 1); + } +}; + +// The top-level comma-separated layers of a box-shadow (commas inside calls do not split). +const layers = (value: string) => { + const parts: string[] = []; + let depth = 0; + let start = 0; + for (let i = 0; i < value.length; i++) { + if (value[i] === "(") depth++; + else if (value[i] === ")") depth--; + else if (value[i] === "," && depth === 0) { + parts.push(value.slice(start, i).trim()); + start = i + 1; + } + } + return [...parts, value.slice(start).trim()]; +}; +const hairlines = (value: string) => layers(expand(value)).filter((layer) => /^inset\s+0\s+0\s+0\s+[\d.]*\d(px)?\b/.test(layer)); + +// Each box-shadow that names a popup token, with the selector of its rule. +const uses = css.flatMap(({ file, text }) => + [...text.matchAll(/box-shadow\s*:\s*([^;}]+)/g)] + .filter((match) => POPUP_TOKENS.some((token) => match[1].includes(`var(${token}`))) + .map((match) => { + const open = text.lastIndexOf("{", match.index); + const start = Math.max(text.lastIndexOf("}", open), text.lastIndexOf(";", open)) + 1; + return { file, selector: text.slice(start, open).trim().replace(/\s+/g, " "), value: match[1].trim() }; + }), +); + +describe("web popups draw one hairline edge", () => { + test("every popup token definition holds exactly one hairline", () => { + for (const token of POPUP_TOKENS) { + for (const definition of definitions.get(token) ?? []) { + expect({ token, definition, hairlines: hairlines(definition).length }).toEqual({ + token, + definition, + hairlines: 1, + }); + } + } + }); + + test("every menu and popover box-shadow resolves to exactly one hairline", () => { + expect(uses.length).toBeGreaterThan(0); + for (const use of uses) { + expect({ ...use, hairlines: hairlines(use.value).length }).toEqual({ ...use, hairlines: 1 }); + } + }); + + // The popups the coherence passes moved onto the shared elevation; each must still be covered. + test("the pane's menus and popovers are all checked", () => { + const selectors = uses.map((use) => use.selector).join("\n"); + for (const popup of [ + ".ui-popup.ui-context-menu", + ":where(.ui-positioner)>.ui-popup", + ".acpmux-context-pop", + ".acpmux-menu", + ".acpmux-slash-menu", + ".acpmux-omni", + ".acpmux-mp", + ".acpmux-access-menu", + ".acpmux-effort-menu", + ".acpmux-chat-menu-popover", + ".acpmux-pr-status", + ".acpmux-summary-popover", + ".acpmux-file-menu-list", + ".nt-popover", + ".acpmux-mp-cascade .acpmux-mp-sub", + ]) { + expect(selectors).toContain(popup); + } + }); +}); From 1cc661608ff24bd021aad5a74836666e23e48316 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 13 Oct 2026 23:08:06 -0700 Subject: [PATCH 2/8] webviews: popups draw one hairline, not two --acpmux-popup-elevation stacked a 0.5px composer-edge hairline on the hairline already in --ui-popup-shadow. It now is the shared popup shadow, so every menu and popover has one edge. Increase Contrast colors that one edge in the popup's ink, which the composer-edge hairline did before. Co-Authored-By: Claude Opus 5.5 --- webviews/src/agent-session/acpmux/styles.css | 4 ++-- webviews/src/pages/shared/desktop.css | 2 ++ webviews/src/ui/popupSurface.css | 6 ++++-- webviews/test/popup-one-hairline.test.ts | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/webviews/src/agent-session/acpmux/styles.css b/webviews/src/agent-session/acpmux/styles.css index 11a8a0bb37f6..29e471599e55 100644 --- a/webviews/src/agent-session/acpmux/styles.css +++ b/webviews/src/agent-session/acpmux/styles.css @@ -18,8 +18,8 @@ /* The shared popup surface (ui/popupSurface.css) takes the pane's ink; every popup, nested submenus included, is the solid popup surface (--surface-popup, the host's --cmux-popup-surface, else the pane's menu color). */ .acpmux-shell,.ui-positioner{--surface-popup:var(--cmux-popup-surface,var(--acpmux-menu-bg))} .ui-positioner{--ui-popup-fg:var(--agent-text)} -/* One elevation for every menu and popover in the pane (Leo 10-13, coherence pass): the composer's hairline edge over the shared popup shadow (ui/popupSurface.css), no border of their own. A popup portaled straight to the body (ui/ContextMenu.tsx) has no composer edge, so it takes the shared hairline. A Base UI popup with no surface class of its own still gets it. */ -:root,.acpmux-shell,.ui-positioner{--acpmux-popup-elevation:inset 0 0 0 .5px var(--acpmux-composer-edge,color-mix(in srgb,currentColor 12%,transparent)),var(--ui-popup-shadow)} +/* One elevation for every menu and popover in the pane (Leo 10-13, coherence pass): the shared popup shadow (ui/popupSurface.css), whose own hairline is the only edge, and no border of their own. A second composer-edge hairline here drew two edges (test/popup-one-hairline.test.ts). A Base UI popup with no surface class of its own still gets it. */ +:root{--acpmux-popup-elevation:var(--ui-popup-shadow)} :where(.ui-positioner)>.ui-popup{border-radius:var(--ui-popup-radius);box-shadow:var(--acpmux-popup-elevation)} /* The point-anchored context menu (ui/ContextMenu.tsx; New Tab All chats rows, cx-j4sd): the pane does not load ui.css, so it draws the menu in its own menu look. */ .ui-popup.ui-context-menu{box-sizing:border-box;padding:var(--ui-popup-padding);border-radius:var(--ui-popup-radius);background:var(--surface-popup);box-shadow:var(--acpmux-popup-elevation);color:var(--agent-text);font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",system-ui,sans-serif;font-size:var(--text-control);line-height:18px;outline:none} diff --git a/webviews/src/pages/shared/desktop.css b/webviews/src/pages/shared/desktop.css index 43b984ca0811..f159fba020a7 100644 --- a/webviews/src/pages/shared/desktop.css +++ b/webviews/src/pages/shared/desktop.css @@ -223,6 +223,8 @@ input[type="submit"] { --agent-border-strong: var(--agent-text, CanvasText) !important; --acpmux-pill-edge: var(--agent-text, CanvasText) !important; --cmux-diff-border: var(--cmux-diff-fg, CanvasText) !important; + /* A popup's one hairline edge (ui/popupSurface.css) in its own ink. */ + --ui-popup-edge: currentColor; } :root:not([data-borders="none"]) :is(.acpmux-shell, .ui-positioner) { diff --git a/webviews/src/ui/popupSurface.css b/webviews/src/ui/popupSurface.css index d65edece91d6..cb6d8622fa11 100644 --- a/webviews/src/ui/popupSurface.css +++ b/webviews/src/ui/popupSurface.css @@ -6,10 +6,12 @@ --ui-popup-radius: 8px; --ui-popup-padding: 4px; /* The native popup shadow (PopupStyle.swift, `0 2px 8px` black at 12%) and a hairline edge: one - elevation in pages and in the app chrome. popup-surface.test.ts holds the two in step. */ + elevation in pages and in the app chrome. popup-surface.test.ts holds the two in step. The + hairline is a popup's only edge (popup-one-hairline.test.ts); Increase Contrast sets its color + (pages/shared/desktop.css). */ --ui-popup-shadow: 0 2px 8px color-mix(in srgb, black 12%, transparent), - inset 0 0 0 0.5px color-mix(in srgb, currentColor 12%, transparent); + inset 0 0 0 0.5px var(--ui-popup-edge, color-mix(in srgb, currentColor 12%, transparent)); --ui-row-height: 28px; --ui-row-radius: 5px; --ui-row-padding-inline: 8px; diff --git a/webviews/test/popup-one-hairline.test.ts b/webviews/test/popup-one-hairline.test.ts index fe55d08de19e..c650a1d0d60f 100644 --- a/webviews/test/popup-one-hairline.test.ts +++ b/webviews/test/popup-one-hairline.test.ts @@ -80,7 +80,8 @@ const layers = (value: string) => { } return [...parts, value.slice(start).trim()]; }; -const hairlines = (value: string) => layers(expand(value)).filter((layer) => /^inset\s+0\s+0\s+0\s+[\d.]*\d(px)?\b/.test(layer)); +const hairlines = (value: string) => + layers(expand(value)).filter((layer) => /^inset\s+0\s+0\s+0\s+[\d.]*\d(px)?\b/.test(layer)); // Each box-shadow that names a popup token, with the selector of its rule. const uses = css.flatMap(({ file, text }) => From 4a8c8d77162147de08f6f052cd9a6be8484654ef Mon Sep 17 00:00:00 2001 From: "cmux-autofix[bot]" Date: Wed, 14 Oct 2026 06:17:12 +0000 Subject: [PATCH 3/8] chore(next): PR autofix at 1cc661608ff2 - vp fmt webviews/src/agent-session/acpmux/modelPickerLayout.ts Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38520503432 --- .../agent-session/acpmux/modelPickerLayout.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/webviews/src/agent-session/acpmux/modelPickerLayout.ts b/webviews/src/agent-session/acpmux/modelPickerLayout.ts index c9f5bed61db2..ae4f1e65f6b0 100644 --- a/webviews/src/agent-session/acpmux/modelPickerLayout.ts +++ b/webviews/src/agent-session/acpmux/modelPickerLayout.ts @@ -95,6 +95,24 @@ export const SUBMENU_ROOM = 250; /// machine lists many harnesses). Less when the pane has less room above or below the chip; the /// harness rail and the model list scroll inside it. export const MODEL_PICKER_MAX_HEIGHT = 480; +/// The shortest the open picker gets: the search field and about six model rows. A chip with less +/// room than this, or a list that loads after the menu opened, never collapses the menu to its +/// search field (Lawrence 2026-10-13); the menu then extends past the room instead. +export const MODEL_PICKER_MIN_HEIGHT = 220; + +/// The open picker's height bounds: at most the anchor's room (`room`, when known) and +/// MODEL_PICKER_MAX_HEIGHT, never below MODEL_PICKER_MIN_HEIGHT; at least its height when it +/// opened (`opened`), so a narrowing search does not shrink it, while a list that loads later can +/// still grow it. +export function pickerHeightBounds( + room: unknown, + opened: number | undefined, +): { maxHeight: number; minHeight: number } { + const roomed = typeof room === "number" && Number.isFinite(room) ? room : MODEL_PICKER_MAX_HEIGHT; + const maxHeight = Math.max(MODEL_PICKER_MIN_HEIGHT, Math.min(roomed, MODEL_PICKER_MAX_HEIGHT)); + const minHeight = Math.min(maxHeight, Math.max(MODEL_PICKER_MIN_HEIGHT, opened ?? 0)); + return { maxHeight, minHeight }; +} export type PickerLayout = "cascade" | "drill"; From 7101cbdab85e1b62f6186eb95242d990dc5d966b Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 13 Oct 2026 23:20:03 -0700 Subject: [PATCH 4/8] webviews: test that popups draw no border over the hairline and clear it under borders none Red: the pages' .ui-popup and the chat find bar draw a border over the --ui-popup-shadow hairline, and appearance.borders none leaves the hairline drawn. Co-Authored-By: Claude Opus 5.5 --- webviews/test/popup-one-hairline.test.ts | 36 ++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/webviews/test/popup-one-hairline.test.ts b/webviews/test/popup-one-hairline.test.ts index c650a1d0d60f..9e9a70a802e6 100644 --- a/webviews/test/popup-one-hairline.test.ts +++ b/webviews/test/popup-one-hairline.test.ts @@ -4,7 +4,8 @@ // exactly one: the hairline in `--ui-popup-shadow` (ui/popupSurface.css). // // The check reads the stylesheets: each `box-shadow` that uses a popup token is expanded through -// the popup tokens' definitions and must hold one inset hairline layer. +// the popup tokens' definitions and must hold one inset hairline layer, with no border in the same +// rule (a page's `.ui-popup` and the chat find bar drew a border over that hairline). import { describe, expect, test } from "bun:test"; import fs from "node:fs"; import path from "node:path"; @@ -83,14 +84,26 @@ const layers = (value: string) => { const hairlines = (value: string) => layers(expand(value)).filter((layer) => /^inset\s+0\s+0\s+0\s+[\d.]*\d(px)?\b/.test(layer)); -// Each box-shadow that names a popup token, with the selector of its rule. +// A border drawn by the rule is an edge too: any `border`/`border-width` that is not 0 or none. +const borders = (body: string) => + [...body.matchAll(/(?:^|[;{\s])border(?:-width)?\s*:\s*([^;}]+)/g)] + .map((match) => match[1].trim()) + .filter((value) => !/^(0(px)?|none)(\s*!important)?$/.test(value)); + +// Each box-shadow that names a popup token, with the selector and the borders of its rule. const uses = css.flatMap(({ file, text }) => [...text.matchAll(/box-shadow\s*:\s*([^;}]+)/g)] .filter((match) => POPUP_TOKENS.some((token) => match[1].includes(`var(${token}`))) .map((match) => { const open = text.lastIndexOf("{", match.index); const start = Math.max(text.lastIndexOf("}", open), text.lastIndexOf(";", open)) + 1; - return { file, selector: text.slice(start, open).trim().replace(/\s+/g, " "), value: match[1].trim() }; + const body = text.slice(open + 1, text.indexOf("}", match.index)); + return { + file, + selector: text.slice(start, open).trim().replace(/\s+/g, " "), + value: match[1].trim(), + borders: borders(body), + }; }), ); @@ -107,13 +120,24 @@ describe("web popups draw one hairline edge", () => { } }); - test("every menu and popover box-shadow resolves to exactly one hairline", () => { + test("every menu and popover draws exactly one edge: the hairline, no border", () => { expect(uses.length).toBeGreaterThan(0); for (const use of uses) { - expect({ ...use, hairlines: hairlines(use.value).length }).toEqual({ ...use, hairlines: 1 }); + const edges = hairlines(use.value).length + use.borders.length; + expect({ ...use, edges }).toEqual({ ...use, edges: 1 }); } }); + // appearance.borders none (data-borders on the root) clears the rim, as it clears the composer's. + test("appearance.borders none clears the popup hairline", () => { + const cleared = css.some(({ text }) => + [...text.matchAll(/:root\[data-borders=("none"|none)\]\s*\{([^}]*)\}/g)].some((match) => + /--ui-popup-edge\s*:\s*transparent/.test(match[2]), + ), + ); + expect(cleared).toBe(true); + }); + // The popups the coherence passes moved onto the shared elevation; each must still be covered. test("the pane's menus and popovers are all checked", () => { const selectors = uses.map((use) => use.selector).join("\n"); @@ -133,6 +157,8 @@ describe("web popups draw one hairline edge", () => { ".acpmux-file-menu-list", ".nt-popover", ".acpmux-mp-cascade .acpmux-mp-sub", + ".acpmux-find__bar", + ".ui-popup", ]) { expect(selectors).toContain(popup); } From 0feaeb3c386462ac6637d0a0e04db3e8b765ddb4 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 13 Oct 2026 23:21:17 -0700 Subject: [PATCH 5/8] webviews: one rim on page popups and the find bar; borders none clears it The pages' .ui-popup drew a 1px border over the --ui-popup-shadow hairline; it now draws no border. The chat find bar keeps its border (its focus cue) and casts --ui-popup-drop, the shadow without the hairline. appearance.borders none sets --ui-popup-edge transparent. Co-Authored-By: Claude Opus 5.5 --- .../acpmux/conversation/conversation.css | 3 ++- webviews/src/ui/popupSurface.css | 12 ++++++++++-- webviews/src/ui/ui.css | 3 ++- webviews/test/popup-one-hairline.test.ts | 8 +++++--- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/webviews/src/agent-session/acpmux/conversation/conversation.css b/webviews/src/agent-session/acpmux/conversation/conversation.css index 54baf9545c8e..c4ab91c69e3c 100644 --- a/webviews/src/agent-session/acpmux/conversation/conversation.css +++ b/webviews/src/agent-session/acpmux/conversation/conversation.css @@ -1855,7 +1855,8 @@ pre.cv-math-source { border: 0.5px solid var(--agent-border); border-radius: 8px; background: var(--acpmux-base, var(--agent-page-bg)); - box-shadow: var(--ui-popup-shadow, 0 4px 14px var(--agent-shadow, rgb(0 0 0 / 18%))); + /* Its border is its one rim (it takes the accent on focus), so the shadow brings no hairline. */ + box-shadow: var(--ui-popup-drop, 0 4px 14px var(--agent-shadow, rgb(0 0 0 / 18%))); font-family: var(--cv-font); font-size: var(--text-body); } diff --git a/webviews/src/ui/popupSurface.css b/webviews/src/ui/popupSurface.css index cb6d8622fa11..37c3e1113088 100644 --- a/webviews/src/ui/popupSurface.css +++ b/webviews/src/ui/popupSurface.css @@ -8,9 +8,11 @@ /* The native popup shadow (PopupStyle.swift, `0 2px 8px` black at 12%) and a hairline edge: one elevation in pages and in the app chrome. popup-surface.test.ts holds the two in step. The hairline is a popup's only edge (popup-one-hairline.test.ts); Increase Contrast sets its color - (pages/shared/desktop.css). */ + (pages/shared/desktop.css). `--ui-popup-drop` is the shadow alone, for a floating bar that + draws its own border. */ + --ui-popup-drop: 0 2px 8px color-mix(in srgb, black 12%, transparent); --ui-popup-shadow: - 0 2px 8px color-mix(in srgb, black 12%, transparent), + var(--ui-popup-drop), inset 0 0 0 0.5px var(--ui-popup-edge, color-mix(in srgb, currentColor 12%, transparent)); --ui-row-height: 28px; --ui-row-radius: 5px; @@ -21,6 +23,12 @@ --ui-popup-close: 80ms; } +/* appearance.borders none (data-borders on the root, applyAgentTheme) clears the rim in place, as it + clears the composer's; the popup's fill keeps it apart. */ +:root[data-borders="none"] { + --ui-popup-edge: transparent; +} + @keyframes ui-popup-open { from { opacity: 0; diff --git a/webviews/src/ui/ui.css b/webviews/src/ui/ui.css index f36d4af3d192..bf0defd72ebc 100644 --- a/webviews/src/ui/ui.css +++ b/webviews/src/ui/ui.css @@ -52,7 +52,8 @@ overflow: auto; overscroll-behavior: contain; padding: var(--ui-popup-padding); - border: 1px solid var(--ui-line); + /* One rim: the hairline in --ui-popup-shadow (ui/popupSurface.css), no border over it. */ + border: 0; border-radius: var(--ui-popup-radius); background: var(--surface-popup); color: var(--ui-fg); diff --git a/webviews/test/popup-one-hairline.test.ts b/webviews/test/popup-one-hairline.test.ts index 9e9a70a802e6..e8b75b299d89 100644 --- a/webviews/test/popup-one-hairline.test.ts +++ b/webviews/test/popup-one-hairline.test.ts @@ -24,7 +24,9 @@ const css = sheets(source).map((file) => ({ text: fs.readFileSync(file, "utf8").replace(/\/\*[\s\S]*?\*\//g, ""), })); -const POPUP_TOKENS = ["--ui-popup-shadow", "--acpmux-popup-elevation"]; +// The elevations that carry the popup hairline, and the drop shadow alone (no hairline). +const RIM_TOKENS = ["--ui-popup-shadow", "--acpmux-popup-elevation"]; +const POPUP_TOKENS = [...RIM_TOKENS, "--ui-popup-drop"]; // Every definition of a popup token, across every sheet. const definitions = new Map(); @@ -108,8 +110,8 @@ const uses = css.flatMap(({ file, text }) => ); describe("web popups draw one hairline edge", () => { - test("every popup token definition holds exactly one hairline", () => { - for (const token of POPUP_TOKENS) { + test("every popup elevation definition holds exactly one hairline", () => { + for (const token of RIM_TOKENS) { for (const definition of definitions.get(token) ?? []) { expect({ token, definition, hairlines: hairlines(definition).length }).toEqual({ token, From 96c0d53eb9bf60e7a426a4efda8cfec9ea665ed7 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 13 Oct 2026 23:27:16 -0700 Subject: [PATCH 6/8] webviews: format modelPickerLayout.ts (base fmt red) Keeps cmux-autofix's vp fmt of the base file, which the base merge replaced with the unformatted copy; react-apps-check fails on it. Co-Authored-By: Claude Opus 5.5 --- webviews/src/agent-session/acpmux/modelPickerLayout.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webviews/src/agent-session/acpmux/modelPickerLayout.ts b/webviews/src/agent-session/acpmux/modelPickerLayout.ts index f27024b74075..ae4f1e65f6b0 100644 --- a/webviews/src/agent-session/acpmux/modelPickerLayout.ts +++ b/webviews/src/agent-session/acpmux/modelPickerLayout.ts @@ -104,7 +104,10 @@ export const MODEL_PICKER_MIN_HEIGHT = 220; /// MODEL_PICKER_MAX_HEIGHT, never below MODEL_PICKER_MIN_HEIGHT; at least its height when it /// opened (`opened`), so a narrowing search does not shrink it, while a list that loads later can /// still grow it. -export function pickerHeightBounds(room: unknown, opened: number | undefined): { maxHeight: number; minHeight: number } { +export function pickerHeightBounds( + room: unknown, + opened: number | undefined, +): { maxHeight: number; minHeight: number } { const roomed = typeof room === "number" && Number.isFinite(room) ? room : MODEL_PICKER_MAX_HEIGHT; const maxHeight = Math.max(MODEL_PICKER_MIN_HEIGHT, Math.min(roomed, MODEL_PICKER_MAX_HEIGHT)); const minHeight = Math.min(maxHeight, Math.max(MODEL_PICKER_MIN_HEIGHT, opened ?? 0)); From 864aba421e8cf6a5f46effcc25bc8fa68d43ffc8 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 02:07:18 -0700 Subject: [PATCH 7/8] webviews: the Channels palette draws one rim; test follows the summary card Red after the base merge: the new Channels palette (.lg-palette, ledger.css) drew a 1px border over the --ui-popup-shadow hairline, and the summary popover became .acpmux-summary-card. The palette now draws no border. Co-Authored-By: Claude Opus 5.5 --- webviews/src/pages/home-channels/ledger.css | 3 ++- webviews/test/popup-one-hairline.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/webviews/src/pages/home-channels/ledger.css b/webviews/src/pages/home-channels/ledger.css index 782d9bd40b0a..f6175d38fe83 100644 --- a/webviews/src/pages/home-channels/ledger.css +++ b/webviews/src/pages/home-channels/ledger.css @@ -836,7 +836,8 @@ /* A fixed height: filtering shrinks the list, never the dialog, so the key hint stays put. */ height: min(520px, 72vh); padding: 0; - border: 1px solid var(--lg-line); + /* One rim: the hairline in --ui-popup-shadow (ui/popupSurface.css), no border over it. */ + border: 0; border-radius: 12px; background: var(--surface-popup); box-shadow: var(--ui-popup-shadow); diff --git a/webviews/test/popup-one-hairline.test.ts b/webviews/test/popup-one-hairline.test.ts index e8b75b299d89..ad49ec54e6a0 100644 --- a/webviews/test/popup-one-hairline.test.ts +++ b/webviews/test/popup-one-hairline.test.ts @@ -155,7 +155,8 @@ describe("web popups draw one hairline edge", () => { ".acpmux-effort-menu", ".acpmux-chat-menu-popover", ".acpmux-pr-status", - ".acpmux-summary-popover", + ".acpmux-summary-card", + ".lg-palette", ".acpmux-file-menu-list", ".nt-popover", ".acpmux-mp-cascade .acpmux-mp-sub", From 2e854eabeb20b272ced2a0a8adcd461110f2ac47 Mon Sep 17 00:00:00 2001 From: "cmux-autofix[bot]" Date: Wed, 14 Oct 2026 09:46:43 +0000 Subject: [PATCH 8/8] chore(next): PR autofix at 54dd59d45f3d - regenerated schemas/defaults/cmux-next.json Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38543983469 --- schemas/defaults/cmux-next.json | 1 + 1 file changed, 1 insertion(+) diff --git a/schemas/defaults/cmux-next.json b/schemas/defaults/cmux-next.json index 9ff51a96c973..ceacdefaa5aa 100644 --- a/schemas/defaults/cmux-next.json +++ b/schemas/defaults/cmux-next.json @@ -359,6 +359,7 @@ "setting.focusRing.showWhenSinglePane": false, "setting.focusRing.style": "ring", "setting.focusRing.width": 1, + "setting.freeModel.enabled": true, "setting.history.closed.maxAgeDays": 0, "setting.history.closed.maxGroups": 0, "setting.history.commandRetentionDays": 30,