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
2 changes: 1 addition & 1 deletion webviews/scripts/design-tokens-baseline.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
10 changes: 9 additions & 1 deletion webviews/src/agent-session/acpmux/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<ShortcutsContext.Provider value={shortcuts}>
<section ref={setShell} className="acpmux-shell" aria-busy="true" />
<section
ref={setShell}
className="acpmux-shell"
data-tab-row={inTabRow ? "" : undefined}
aria-busy="true"
style={inTabRow ? tabRowStyle(tabRow) : undefined}
/>
</ShortcutsContext.Provider>
) : (
<ShortcutsContext.Provider value={shortcuts}>
<section
ref={setShell}
className="acpmux-shell"
data-tab-row={inTabRow ? "" : undefined}
aria-label={composerSnapshot.summary?.title || t("header.agentChat")}
style={inTabRow ? tabRowStyle(tabRow) : undefined}
>
<div
className="acpmux-main"
Expand Down
2 changes: 1 addition & 1 deletion webviews/src/agent-session/acpmux/header/header.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webviews/src/agent-session/acpmux/styles.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions webviews/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions webviews/test/chat-header-tab-row-surface.test.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof papercutPane>>;

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(`<!doctype html><html><head><style>
html, body { width: 100%; height: 100%; margin: 0; background: rgb(10, 20, 30); }
${shellCss}
</style></head><body>
<section class="acpmux-shell" data-tab-row
style="--acpmux-tab-row-height:40px;--agent-page-bg:rgb(200,10,20)">
<header class="acpmux-header" data-tab-row><div id="tools"></div></header>
</section>
</body></html>`);
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}");
});
});
Loading