diff --git a/webviews/scripts/design-tokens-baseline.jsonl b/webviews/scripts/design-tokens-baseline.jsonl index 342f0f65c254..7c1c99555a78 100644 --- a/webviews/scripts/design-tokens-baseline.jsonl +++ b/webviews/scripts/design-tokens-baseline.jsonl @@ -79,7 +79,7 @@ {"file":"agent-session/acpmux/harnesses/harnesses.css","kind":"height","property":"min-height","values":{"40px":1}} {"file":"agent-session/acpmux/harnesses/harnesses.css","kind":"spacing","property":"gap","values":{"10px":1,"1px":1}} {"file":"agent-session/acpmux/harnesses/harnesses.css","kind":"spacing","property":"padding","values":{"20px max(18px,calc((100% - 560px)/2))":1,"4px 6px":1,"0 10px":1,"8px 6px":1}} -{"file":"agent-session/acpmux/header/header.css","kind":"height","property":"height","values":{".5px":1,"6px":3,"26px":2,"1px":1,"12px":1}} +{"file":"agent-session/acpmux/header/header.css","kind":"height","property":"height","values":{".5px":1,"6px":3,"26px":2,"1px":1}} {"file":"agent-session/acpmux/header/header.css","kind":"height","property":"min-height","values":{"26px":1}} {"file":"agent-session/acpmux/header/header.css","kind":"spacing","property":"gap","values":{"2px":3,"5px":2,"3px":1,"6px":4}} {"file":"agent-session/acpmux/header/header.css","kind":"spacing","property":"padding","values":{"0 6px":5,"8px 6px 2px":1,"0 10px":2,"8px 6px 6px":1,"2px 6px 6px":1,"4px 6px":1,"8px 6px 8px":1}} diff --git a/webviews/src/agent-session/acpmux/App.tsx b/webviews/src/agent-session/acpmux/App.tsx index 6eedf3de113c..a0acf70f9e55 100644 --- a/webviews/src/agent-session/acpmux/App.tsx +++ b/webviews/src/agent-session/acpmux/App.tsx @@ -9311,14 +9311,22 @@ function AcpmuxPane() { const pane = !knowsWhatItIs ? ( // Clear until it knows: the pane's background shows, never a chat screen a New Tab replaces. -
+
) : (
*{margin:8px 0 0}.acpmux-markdown>:first-child{margin-top:0}.acpmux-markdown code{font:11.5px var(--font-mono)}.acpmux-markdown blockquote{padding-left:12px;border-left:2px solid var(--agent-border);color:var(--agent-soft)}.acpmux-markdown pre,.acpmux-activity-item pre{padding:9px;border-radius:8px;overflow:auto;background:var(--agent-input-bg);font:12px var(--font-mono)}.acpmux-summary,.acpmux-muted{color:var(--agent-muted);font-size:11px;padding:8px 0}.acpmux-activity-toggle{border:0;background:none;color:var(--agent-muted);padding:5px 0;cursor:pointer}.acpmux-activity-items{border-left:1px solid var(--agent-border);padding-left:12px;color:var(--agent-soft)}.acpmux-activity-item{padding:3px 0}.acpmux-glyph{display:inline-block;width:18px;color:var(--agent-accent)}.acpmux-permission{padding:0 var(--cv-inset,max(18px,calc((100% - 760px)/2)));min-width:0;min-height:0;flex-shrink:0}.acpmux-asks-stack{display:flex;flex-direction:column;gap:8px;padding-bottom:10px}.acpmux-asks-stack .acpmux-trust-ask{margin-bottom:0}.acpmux-permission-card{min-inline-size:0;margin:0;padding:11px;border:1px solid var(--agent-accent);border-radius:12px;background:var(--agent-surface-elevated)}.acpmux-permission-buttons{display:flex;gap:7px;margin-top:9px}.acpmux-permission-buttons button{border:0;border-radius:6px;padding:7px 12px;background:var(--agent-accent);color:var(--agent-accent-text,#fff)} /* The chat header's height (.acpmux-header); the Inspector over the chat starts under it (inspector/inspector.css). */ .acpmux-shell{--acpmux-header-height:44px} +.acpmux-shell[data-tab-row]{background:linear-gradient(to bottom,transparent 0 var(--acpmux-tab-row-height),var(--agent-page-bg) var(--acpmux-tab-row-height));} .acpmux-stage{position:relative;flex:1;display:flex;flex-direction:column;min-height:0}.acpmux-reviewing>.acpmux-header,.acpmux-reviewing>.acpmux-scroll{visibility:hidden} .acpmux-transcript-after-latest{flex:none;min-width:0} diff --git a/webviews/src/styles.css b/webviews/src/styles.css index 742d0bc090eb..df0787a6f14d 100644 --- a/webviews/src/styles.css +++ b/webviews/src/styles.css @@ -107,6 +107,11 @@ html { background: var(--cmux-diff-viewer-bg); } +/* A chat that shares the native tab row leaves that row's background visible behind its tools. + The page still owns the background below the row; only the offered strip is transparent. */ +html:has(.acpmux-shell[data-tab-row]) { + background: transparent !important; +} /* Glass over an image background or a see-through window (data-cmux-see-through, desktop.ts), so the backdrop shows through as it does through every other surface. Off under Reduce Transparency (the system's, by the media query, or the host's data-reduce-transparency): every variable below diff --git a/webviews/test/chat-header-tab-row-surface.test.ts b/webviews/test/chat-header-tab-row-surface.test.ts new file mode 100644 index 000000000000..743bf793e226 --- /dev/null +++ b/webviews/test/chat-header-tab-row-surface.test.ts @@ -0,0 +1,86 @@ +import { afterAll, beforeAll, expect, setDefaultTimeout, test } from "bun:test"; +import fs from "node:fs"; +import path from "node:path"; +import { PNG } from "pngjs"; +import { chromium, type Browser } from "playwright"; +import { papercutPane } from "./support/papercutChat"; +import { requireBrowserLane } from "./support/requireBrowserLane"; + +await requireBrowserLane("chat-header-tab-row-surface.test.ts", async () => { + setDefaultTimeout(120_000); + const source = path.resolve(import.meta.dir, "../src"); + const read = (file: string) => fs.readFileSync(path.join(source, file), "utf8"); + let browser: Browser; + let pane: Awaited>; + + beforeAll(async () => { + browser = await chromium.launch({ headless: true }); + pane = await papercutPane(() => browser, { turns: 1, running: false }); + }); + afterAll(async () => { + await pane?.close(); + await browser?.close(); + }); + + test("the normal App shell carries the offered row state", async () => { + const page = await pane.open({ width: 900, height: 640 }); + try { + await page.evaluate(async () => { + const url = "/header/tabRow.ts"; + const module = (await import(/* @vite-ignore */ url)) as { + setTabRow(value: { height: number; trailingInset: number }): void; + }; + module.setTabRow({ height: 40, trailingInset: 12 }); + }); + const shell = page.locator("section.acpmux-shell").first(); + await page.waitForFunction(() => document.querySelector("section.acpmux-shell")?.hasAttribute("data-tab-row")); + const rendered = await shell.evaluate((node) => ({ + tabRow: node.hasAttribute("data-tab-row"), + height: getComputedStyle(node).getPropertyValue("--acpmux-tab-row-height"), + inset: getComputedStyle(node).getPropertyValue("--acpmux-tab-row-inset"), + })); + expect(rendered).toEqual({ tabRow: true, height: "40px", inset: "12px" }); + } finally { + await page.close(); + } + }); + + test("the shipped tab-row shell leaves the native strip transparent", async () => { + const page = await browser.newPage({ viewport: { width: 240, height: 100 } }); + // This is the stylesheet that build-agent-pane-web.sh inlines into the shipped agent pane. + // webviews/src/styles.css belongs to the page host and is deliberately not part of this fixture. + const shellCss = [read("agent-session/acpmux/styles.css"), read("agent-session/acpmux/header/header.css")].join( + "\n", + ); + await page.setContent(` +
+
+
+ `); + const shot = PNG.sync.read(await page.screenshot()); + const pixel = (x: number, y: number) => { + const offset = (y * shot.width + x) * 4; + return Array.from(shot.data.subarray(offset, offset + 4)); + }; + // Every pixel in the offered row, including its bottom edge, is the native strip beneath it. + expect(pixel(20, 8)).toEqual([10, 20, 30, 255]); + expect(pixel(20, 39)).toEqual([10, 20, 30, 255]); + // The page fill starts immediately after the offered row. + expect(pixel(20, 50)).toEqual([200, 10, 20, 255]); + expect(await page.locator(".acpmux-header").evaluate((node) => getComputedStyle(node, "::after").content)).toBe( + "none", + ); + await page.close(); + }); + + test("the loading shell carries the offered row state before the handshake", () => { + const app = read("agent-session/acpmux/App.tsx"); + const loading = app.match(/const pane = !knowsWhatItIs \? \(([\s\S]*?)\) : \(/)?.[1] ?? ""; + expect(loading).toContain('data-tab-row={inTabRow ? "" : undefined}'); + expect(loading).toContain("style={inTabRow ? tabRowStyle(tabRow) : undefined}"); + }); +});