Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions webviews/src/agent-session/acpmux/composerLocation.css
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,11 @@

/* The composer is one material surface. The location scope is quiet metadata at its lower edge,
so it cannot read as a second box over wallpaper or add a full-width divider. Its rim is one
device pixel and its focus ring whole device pixels (desktop.css hairline tokens), so both stay
continuous around the rounded corners at every scale; the rim clears with appearance.borders
none, as the composer edge does (styles.css). */
device pixel (desktop.css hairline tokens), so it stays continuous around the rounded corners at
every scale. Focus is quiet, as the New Tab box's (Lawrence, Leo 10-14): the same rim, its tone
one 8% step toward the text, never a wider ring or the accent. The rim clears with
appearance.borders none, focused or not, as the composer edge does (styles.css); Increase
Contrast draws the shared focus ring (composerStates.css). */
.acpmux-composer-box {
--acpmux-composer-rim: color-mix(in srgb, var(--agent-text) 16%, transparent);
background: var(--acpmux-composer-surface);
Expand All @@ -389,9 +391,8 @@
--acpmux-composer-rim: transparent;
}
.acpmux-composer-box:focus-within {
box-shadow:
var(--acpmux-composer-shadow, var(--elevation-1)),
inset 0 0 0 var(--cmux-hairline-ring, 1px) color-mix(in srgb, var(--agent-accent, var(--agent-text)) 38%, transparent);
/* The rest rim (16%) one 8% step toward the text: 8% + 92% of 16%. */
--acpmux-composer-rim: color-mix(in srgb, var(--agent-text) 23%, transparent);
}
.acpmux-composer-context,
.acpmux-composer-box[data-context-first] > .acpmux-composer-context {
Expand Down
6 changes: 3 additions & 3 deletions webviews/src/agent-session/acpmux/composerStates.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,9 @@ button.acpmux-send:disabled:not(.acpmux-cancel):not(.acpmux-send-ready) {
color: var(--acpmux-composer-placeholder, var(--agent-muted));
opacity: 1;
}
/* Focus in the composer: its rounded edge takes one step toward the theme accent, drawn in
composerLocation.css with the rim; shell mode takes its own accent here. The prompt itself draws
no ring (desktop.css, cx-ncc.57). Rings are whole device pixels (desktop.css hairline tokens). */
/* Focus in the composer: its rim lifts a little in tone (composerLocation.css); shell mode takes
its own accent here, the mode's mark. The prompt itself draws no ring (desktop.css, cx-ncc.57).
Rings are whole device pixels (desktop.css hairline tokens). */
.acpmux-composer[data-shell] .acpmux-composer-box:focus-within {
box-shadow: inset 0 0 0 var(--cmux-hairline-ring, 1px) color-mix(in srgb, var(--acpmux-shell-accent) 85%, transparent);
}
Expand Down
4 changes: 2 additions & 2 deletions webviews/src/agent-session/acpmux/newtab/screen.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
.nt-field{flex:1;min-width:0;height:100%;border:0;outline:0;padding:0;background:none;color:var(--agent-text);caret-color:var(--cmux-caret, var(--agent-text));font:inherit;font-size:var(--text-title);line-height:var(--text-title--line-height)}
.nt-field::placeholder{color:var(--acpmux-composer-placeholder);opacity:1}
.nt-hint{flex:none;color:var(--acpmux-composer-dim);font-size:12px;white-space:nowrap}
/* The suggestions' popover (Leo 2026-10-12): the shared Popover under the composer box, as wide as it, the composer's fill over the opaque popup surface (Leo 10-13: the page background is see-through on a wallpaper theme, and the rows under it showed through); it never moves the page or grows its scroll. The list inside scrolls. */
.nt-popover{box-sizing:border-box;width:var(--anchor-width);padding:0;border-radius:var(--ui-popup-radius,8px);background:linear-gradient(var(--acpmux-composer-bg),var(--acpmux-composer-bg)),var(--surface-popup);color:var(--agent-text);font-family:system-ui;-webkit-font-smoothing:antialiased;-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:var(--acpmux-popup-elevation)}
/* The suggestions' popover (Leo 2026-10-12): the shared Popover under the composer box, as wide as it, the opaque popup surface every pane popup paints (ui/popupSurface.css; Leo 10-13: the page background is see-through on a wallpaper theme, and the rows under it showed through; 10-14: no composer tint over it, so it reads as the pane's other menus); it never moves the page or grows its scroll. The list inside scrolls. */
.nt-popover{box-sizing:border-box;width:var(--anchor-width);padding:0;border-radius:var(--ui-popup-radius,8px);background:var(--surface-popup);color:var(--agent-text);font-family:system-ui;-webkit-font-smoothing:antialiased;-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:var(--acpmux-popup-elevation)}
.nt-popover .nt-rows{--popup-scroll-cap:360px;box-sizing:border-box;padding:var(--ui-popup-padding);max-height:min(48vh,360px);overflow:auto;overscroll-behavior:contain}
.nt-row{display:flex;align-items:center;gap:10px;height:var(--ui-row-height);padding:0 var(--ui-row-padding-inline);border-radius:var(--ui-row-radius);color:var(--agent-soft);font-size:var(--text-control);cursor:default;white-space:nowrap}
.nt-row.is-selected{background:color-mix(in srgb,var(--agent-accent,var(--agent-text)) 8%,transparent);color:var(--agent-text)}
Expand Down
1 change: 1 addition & 0 deletions webviews/test/browser/composer-rim.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</div>
<div class="nt-box" id="newtab"><input class="nt-field" id="newtab-field" /></div>
</section>
<div class="ui-positioner"><div class="ui-popup nt-popover" id="popover"></div></div>
<script type="module" src="./composer-rim.harness.ts"></script>
</body>
</html>
7 changes: 4 additions & 3 deletions webviews/test/composer-rim-hairline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,16 @@ await requireBrowserLane("composer-rim-hairline.test.ts", async () => {
await page.focus(`#${field}`);
const ring = await insets(page, id);
expect(ring.length, `${id} draws a focus ring`).toBeGreaterThan(0);
if (id === "composer") expect(ring[0]?.alpha, "composer focus ring alpha").toBeCloseTo(0.38, 2);
if (id === "composer") expect(ring[0]?.alpha, "composer focus rim alpha").toBeCloseTo(0.23, 2);
for (const { spread } of ring) {
const pixels = devicePixels(spread, scale);
expect(Number.isInteger(pixels) && pixels >= 1, `${id} focus ring at ${scale}x is ${pixels} device px`).toBe(
true,
);
// The ring keeps its 1px look: one CSS pixel wherever a CSS pixel is whole device pixels.
// The New Tab box keeps its one-device-pixel rim on focus (newtab-quiet-focus.test.ts).
if (id === "newtab") expect(pixels, `${id} focus rim at ${scale}x`).toBe(1);
// The New Tab box and the chat composer keep their one-device-pixel rim on focus
// (newtab-quiet-focus.test.ts); the shell's mode ring stays one CSS pixel.
if (id !== "shell") expect(pixels, `${id} focus rim at ${scale}x`).toBe(1);
else if (Number.isInteger(scale)) expect(spread, `${id} focus ring at ${scale}x`).toBe(1);
}
}
Expand Down
104 changes: 70 additions & 34 deletions webviews/test/newtab-quiet-focus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ await requireBrowserLane("newtab-quiet-focus.test.ts", async () => {
return page;
}

/** The box's inset rims: spread in CSS px and color as 0...1 channels. */
function rims(page: Page): Promise<Rim[]> {
return page.evaluate(() => {
const shadow = getComputedStyle(document.getElementById("newtab")!).boxShadow;
/** A box's inset rims: spread in CSS px and color as 0...1 channels. */
function rims(page: Page, id = "newtab"): Promise<Rim[]> {
return page.evaluate((id) => {
const shadow = getComputedStyle(document.getElementById(id)!).boxShadow;
const entries: string[] = [];
let depth = 0;
let start = 0;
Expand Down Expand Up @@ -99,7 +99,7 @@ await requireBrowserLane("newtab-quiet-focus.test.ts", async () => {
rgba = [numbers[0] / 255, numbers[1] / 255, numbers[2] / 255, numbers[3] ?? 1];
return { spread: lengths[3] ?? 0, rgba };
});
});
}, id);
}

/** A computed color as 0...1 channels (`rgb()` or `color(srgb ...)`); null when unparsed. */
Expand Down Expand Up @@ -128,37 +128,73 @@ await requireBrowserLane("newtab-quiet-focus.test.ts", async () => {
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

for (const theme of ["dark", "light"] as const) {
for (const scale of [1, 2]) {
test(`${theme} at ${scale}x: focus keeps the one-device-pixel rim, a slight step in tone, no accent`, async () => {
const page = await open(scale, theme);
const rest = await rims(page);
expect(rest.length, "the box draws a rim").toBe(1);
expect(rest[0].spread * scale, "rest rim in device px").toBe(1);
expect(rest[0].rgba[3], "the rim's color parsed and shows").toBeGreaterThan(0);
await page.focus("#newtab-field");
const focused = await rims(page);
expect(focused.length, "focus draws one rim, no extra ring").toBe(1);
expect(focused[0].spread, "focus keeps the rim's width").toBe(rest[0].spread);
const ground = (await fill(page, "newtab")).rgba!;
const [r, g, b] = focused[0].rgba;
expect(Math.max(Math.abs(r - g), Math.abs(g - b)), "the focused rim is neutral, not the accent").toBeLessThan(
0.02,
);
const lift = Math.max(
...ink(focused[0].rgba, ground).map((value, index) => Math.abs(value - ink(rest[0].rgba, ground)[index])),
);
expect(lift, "focus lifts the rim's tone at most slightly").toBeLessThan(0.08);
await page.close();
});
// The New Tab box and the chat composer (Leo 10-14: the chat composer's focus is as quiet).
const boxes = [
["newtab", "newtab-field"],
["composer", "composer-field"],
] as const;

for (const [box, field] of boxes)
for (const theme of ["dark", "light"] as const) {
for (const scale of [1, 2]) {
test(`${box}, ${theme} at ${scale}x: focus keeps the one-device-pixel rim, a slight step in tone, no accent`, async () => {
const page = await open(scale, theme);
const rest = await rims(page, box);
expect(rest.length, "the box draws a rim").toBe(1);
expect(rest[0].spread * scale, "rest rim in device px").toBe(1);
expect(rest[0].rgba[3], "the rim's color parsed and shows").toBeGreaterThan(0);
await page.focus(`#${field}`);
const focused = await rims(page, box);
expect(focused.length, "focus draws one rim, no extra ring").toBe(1);
expect(focused[0].spread, "focus keeps the rim's width").toBe(rest[0].spread);
const ground = (await fill(page, box)).rgba!;
const [r, g, b] = focused[0].rgba;
expect(Math.max(Math.abs(r - g), Math.abs(g - b)), "the focused rim is neutral, not the accent").toBeLessThan(
0.02,
);
const lift = Math.max(
...ink(focused[0].rgba, ground).map((value, index) => Math.abs(value - ink(rest[0].rgba, ground)[index])),
);
expect(lift, "focus lifts the rim's tone at most slightly").toBeLessThan(0.08);
await page.close();
});
}
}
}

test("focused, the rim still clears with appearance.borders none", async () => {
const page = await open(2);
await page.evaluate(() => document.documentElement.setAttribute("data-borders", "none"));
await page.focus("#newtab-field");
for (const { rgba } of await rims(page)) expect(rgba[3], "focused rim alpha with borders none").toBe(0);
for (const [box, field] of boxes)
test(`${box}: focused, the rim still clears with appearance.borders none`, async () => {
const page = await open(2);
await page.evaluate(() => document.documentElement.setAttribute("data-borders", "none"));
await page.focus(`#${field}`);
for (const { rgba } of await rims(page, box))
expect(rgba[3], `${box} focused rim alpha with borders none`).toBe(0);
await page.close();
});

for (const [box, field] of boxes)
test(`${box}: under Increase Contrast focus takes the shared, wider focus ring`, async () => {
const page = await open(2);
await page.emulateMedia({ contrast: "more" });
const rest = await rims(page, box);
await page.focus(`#${field}`);
const focused = await rims(page, box);
const width = await page.evaluate(() =>
parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--cmux-focus-ring-width")),
);
expect(
focused.map(({ spread }) => spread),
`${box} focus ring`,
).toEqual([width]);
expect(width, "wider than the rim").toBeGreaterThan(rest[0]?.spread ?? 0);
await page.close();
});

test("the New Tab suggestions popover is the popup surface, as every pane popup", async () => {
const page = await open(2, "light");
await page.evaluate(() => document.documentElement.style.setProperty("--cmux-popup-surface", "rgb(201, 202, 203)"));
const { rgba, image } = await fill(page, "popover");
expect(image, "no composer tint over it").toBe("none");
expect(rgba, "the popup surface").toEqual([201 / 255, 202 / 255, 203 / 255, 1]);
await page.close();
});

Expand Down
1 change: 0 additions & 1 deletion webviews/test/subtle-elevation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ describe("one subtle elevation scale", () => {
["styles.css", ".comment-card", "--elevation-1"],
// The composer: --elevation-1 in a light theme, none in a dark one (newtab-quiet-focus.test.ts).
["agent-session/acpmux/composerLocation.css", ".acpmux-composer-box", "--elevation-1"],
["agent-session/acpmux/composerLocation.css", ".acpmux-composer-box:focus-within", "--elevation-1"],
[
"agent-session/acpmux/composerLocation.css",
".acpmux-permission-panel .acpmux-permission-card.acpmux-permission-surface",
Expand Down
Loading