*{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}");
+ });
+});