diff --git a/webviews/src/agent-session/acpmux/conversation/conversation.css b/webviews/src/agent-session/acpmux/conversation/conversation.css index e4ed9fbc4f62..55c9b3f5ef0d 100644 --- a/webviews/src/agent-session/acpmux/conversation/conversation.css +++ b/webviews/src/agent-session/acpmux/conversation/conversation.css @@ -503,6 +503,9 @@ color: var(--agent-text); font-family: var(--cv-font); } +.acpmux-shell { + --acpmux-artifact-push: 0px; +} /* A shell sibling must leave the offered native tab row clear, including when the narrow preview covers the chat. Its background belongs below the row, never in its tools hole. */ .acpmux-shell[data-tab-row] > .acpmux-artifact { @@ -511,8 +514,8 @@ } /* The header lives in the chat column while Preview is a shell sibling. Keep its tools at the pane edge, across the Preview column, just as the inline Summary does. */ -.acpmux-shell[data-tab-row]:has(> .acpmux-artifact) .acpmux-header[data-tab-row] { - width: calc(100% + var(--acpmux-artifact-width, clamp(320px, 46vw, 760px))); +.acpmux-shell[data-tab-row]:has(> .acpmux-artifact) { + --acpmux-artifact-push: var(--acpmux-artifact-width, clamp(320px, 46vw, 760px)); } @media (max-width: 720px) { .acpmux-artifact { @@ -521,8 +524,8 @@ z-index: var(--layer-overlay); box-shadow: none; } - .acpmux-shell[data-tab-row]:has(> .acpmux-artifact) .acpmux-header[data-tab-row] { - width: 100%; + .acpmux-shell[data-tab-row]:has(> .acpmux-artifact) { + --acpmux-artifact-push: 0px; } } .acpmux-artifact-bar { diff --git a/webviews/src/agent-session/acpmux/inspector/inspector.css b/webviews/src/agent-session/acpmux/inspector/inspector.css index b16196394d81..f0fc1ef7e0b7 100644 --- a/webviews/src/agent-session/acpmux/inspector/inspector.css +++ b/webviews/src/agent-session/acpmux/inspector/inspector.css @@ -51,10 +51,21 @@ align-self: flex-start; margin: var(--acpmux-inspector-top, 0px) var(--acpmux-inspector-inset) 0 0; } -/* The native tab strip's reserved tool area stays at the pane's trailing edge. Push always - narrows the conversation below it; the header keeps the room measured before that column. */ -.acpmux-shell[data-inspector-chat-placement="push"] .acpmux-header { - width: var(--acpmux-header-room, 100%); +/* Every shell-side panel contributes one term to the native tab row's width. The Inspector's + room is measured before the artifact column by ChatInspector.tsx; subtracting the main column's + 100% leaves the pushed Inspector width and its inset. Keeping each panel as a zero-valued shell + variable makes the shared rule additive as new side panels arrive. */ +.acpmux-shell { + --acpmux-inspector-push: 0px; +} +.acpmux-shell[data-inspector-chat-placement="push"] { + --acpmux-inspector-push: calc(var(--acpmux-header-room, 100%) - 100%); +} +.acpmux-shell .acpmux-header { + width: calc( + 100% + var(--acpmux-summary-push, 0px) + var(--acpmux-inspector-push, 0px) + + var(--acpmux-artifact-push, 0px) + ); } /* Floating: the same card over the gutter. With the side panel open beside the chat, its gutter ends at that panel's edge (--acpmux-inspector-side, its width), never over the document. The inset is its diff --git a/webviews/src/agent-session/acpmux/summary/SummaryCard.gallery.tsx b/webviews/src/agent-session/acpmux/summary/SummaryCard.gallery.tsx index 744a804123dd..5865f11de214 100644 --- a/webviews/src/agent-session/acpmux/summary/SummaryCard.gallery.tsx +++ b/webviews/src/agent-session/acpmux/summary/SummaryCard.gallery.tsx @@ -23,7 +23,7 @@ import { realWorkflowFixture } from "../realWorkflowFixture"; type Props = Pick< ComponentProps, "rows" | "background" | "sessions" | "onAttach" | "workflow" | "workflowActions" -> & { tabRowHeight?: number; preview?: boolean }; +> & { tabRowHeight?: number; preview?: boolean; inspectorPush?: boolean }; const none = () => undefined; /// The inline column, shown and open. @@ -38,17 +38,31 @@ const card: SummaryCardControl = { fold: none, }; -function GallerySummaryCard({ tabRowHeight, preview, ...props }: Props) { +function GallerySummaryCard({ tabRowHeight, preview, inspectorPush, ...props }: Props) { return (
- {tabRowHeight && ( + {tabRowHeight !== undefined && (
-
+
@@ -57,12 +71,31 @@ function GallerySummaryCard({ tabRowHeight, preview, ...props }: Props) {
)}
+ {inspectorPush && ( + + )} {preview ? ( - ) : ( + ) : inspectorPush ? null : ( ({ note: "The Preview sibling obeys the same native tab-row clearance, both beside the chat and covering a narrow pane.", props: { tabRowHeight: 32, preview: true, rows: [] }, }, + "preview-pushed-inspector-tab-row": { + note: "Preview and a pushed Session Info card share the pane; the native tools span both side-panel terms.", + props: { tabRowHeight: 32, preview: true, inspectorPush: true, rows: [] }, + }, + "inspector-pushed-tab-row": { + note: "A pushed Session Info card keeps the native tools at the pane edge.", + props: { tabRowHeight: 32, inspectorPush: true, rows: [] }, + }, + "inspector-pushed-no-tab-row": { + note: "A pushed Session Info card keeps ordinary header tools in place when no native row is active.", + props: { tabRowHeight: 0, inspectorPush: true, rows: [] }, + }, "shared-tab-row": { note: "The inline Summary column beside the chat, under a 32px native tab row. Two commands and a failed subagent keep long counts visible with a right ellipsis, outside the native header holes.", props: { diff --git a/webviews/src/agent-session/acpmux/summary/summary.css b/webviews/src/agent-session/acpmux/summary/summary.css index 9b8204806b26..f96485b168da 100644 --- a/webviews/src/agent-session/acpmux/summary/summary.css +++ b/webviews/src/agent-session/acpmux/summary/summary.css @@ -9,9 +9,9 @@ .acpmux-main[data-summary=inline]{min-width:0} /* The inline Summary is beside the chat, not inside its stage: it must leave the native shared tab row empty too. The same column width keeps the tools at the pane edge while the chat narrows. */ -.acpmux-shell{--acpmux-summary-column-width:300px} +.acpmux-shell{--acpmux-summary-column-width:300px;--acpmux-summary-push:0px} .acpmux-shell[data-tab-row]>.acpmux-summary-card[data-placement=inline]{margin-top:var(--acpmux-tab-row-height);max-height:calc(100% - var(--acpmux-tab-row-height))} -.acpmux-shell[data-tab-row]:has(>.acpmux-summary-card[data-placement=inline]) .acpmux-header[data-tab-row]{width:calc(100% + var(--acpmux-summary-column-width))} +.acpmux-shell[data-tab-row]:has(>.acpmux-summary-card[data-placement=inline]){--acpmux-summary-push:var(--acpmux-summary-column-width)} /* Under the tab row when the header's tools sit in it (header/tabRow.ts). */ .acpmux-header[data-tab-row]~.acpmux-summary-card{top:calc(var(--acpmux-tab-row-height) + 8px)} .acpmux-reviewing>.acpmux-summary-card,.acpmux-inspecting>.acpmux-summary-card,.acpmux-stage:has(.acpmux-find)>.acpmux-summary-card{visibility:hidden} diff --git a/webviews/test/summary-header-surface.test.ts b/webviews/test/summary-header-surface.test.ts index f0340de878fb..84ec192711d9 100644 --- a/webviews/test/summary-header-surface.test.ts +++ b/webviews/test/summary-header-surface.test.ts @@ -120,7 +120,10 @@ await requireBrowserLane("summary-header-surface.test.ts", async () => { ); await page.waitForFunction(() => document.documentElement.dataset.galleryReady === "1"); await page.locator(".acpmux-shell").evaluate((node, pixels) => { - (node as HTMLElement).style.setProperty("--acpmux-tab-row-height", `${pixels}px`); + const shell = node as HTMLElement; + shell.style.setProperty("--acpmux-tab-row-height", `${pixels}px`); + if (shell.dataset.inspectorChatPlacement === "push") + shell.style.setProperty("--acpmux-inspector-top", pixels > 0 ? `${pixels}px` : "28px"); }, height); await page.locator(".acpmux-artifact").waitFor(); if (process.env.CMUX_SUMMARY_CAPTURE_DIR) { @@ -157,4 +160,93 @@ await requireBrowserLane("summary-header-surface.test.ts", async () => { } } } + for (const variant of [ + "preview-tab-row", + "preview-pushed-inspector-tab-row", + "inspector-pushed-tab-row", + "inspector-pushed-no-tab-row", + ] as const) { + for (const height of variant === "inspector-pushed-no-tab-row" ? [0] : [28, 40]) { + for (const theme of ["Apple System Colors", "Apple System Colors Light"]) { + test(`${variant} keeps tools at the shell edge at ${height}px (${theme})`, async () => { + const page = await browser.newPage({ + viewport: { width: 1000, height: 560 }, + reducedMotion: "reduce", + }); + try { + const port = (server.httpServer!.address() as { port: number }).port; + await page.goto(`http://127.0.0.1:${port}${galleryFrame("agent-pane.summary-card", { variant, theme })}`); + await page.waitForFunction(() => document.documentElement.dataset.galleryReady === "1"); + await page.locator(".acpmux-shell").evaluate((node, pixels) => { + const shell = node as HTMLElement; + shell.style.setProperty("--acpmux-tab-row-height", `${pixels}px`); + if (shell.dataset.inspectorChatPlacement === "push") + shell.style.setProperty("--acpmux-inspector-top", `${pixels}px`); + }, height); + await page.evaluate((pixels) => { + const inspector = document.querySelector(".acpmux-chat-inspector[data-placement=push]"); + inspector?.style.setProperty("--acpmux-inspector-top", pixels > 0 ? `${pixels}px` : "28px"); + }, height); + if ( + process.env.CMUX_SUMMARY_CAPTURE_DIR && + variant === "preview-pushed-inspector-tab-row" && + height === 28 + ) { + await mkdir(process.env.CMUX_SUMMARY_CAPTURE_DIR, { recursive: true }); + await page.screenshot({ + path: path.join( + process.env.CMUX_SUMMARY_CAPTURE_DIR, + `preview-pushed-inspector-${theme.endsWith("Light") ? "light" : "dark"}.png`, + ), + }); + } + const actual = await page.evaluate(() => { + const shell = document.querySelector(".acpmux-shell")!; + const shellRect = shell.getBoundingClientRect(); + const tools = document.querySelector(".acpmux-header-tools")!.getBoundingClientRect(); + const panels = [ + ...document.querySelectorAll( + ".acpmux-artifact, .acpmux-chat-inspector[data-placement=push]", + ), + ].map((panel) => { + const rect = panel.getBoundingClientRect(); + return { + top: rect.top - shellRect.top, + intersectsTools: + rect.left < tools.right && + rect.right > tools.left && + rect.top < tools.bottom && + rect.bottom > tools.top, + }; + }); + return { + toolsWidth: tools.width, + toolsRight: tools.right, + shellRight: shellRect.right, + panels, + }; + }); + expect(actual.toolsWidth).toBeGreaterThan(0); + if (variant === "inspector-pushed-no-tab-row") { + // Ordinary headers keep their existing 8px trailing inset; the regression is the + // Inspector narrowing the header, which would move this edge left by the panel width. + expect(actual.toolsRight).toBe(actual.shellRight - 8); + await page.locator(".acpmux-header-tools button").evaluate((button) => { + button.addEventListener("click", () => button.setAttribute("data-test-clicked", "true"), { + once: true, + }); + }); + await page.locator(".acpmux-header-tools button").click(); + expect(await page.locator(".acpmux-header-tools button").getAttribute("data-test-clicked")).toBe("true"); + } else { + expect(actual.toolsRight).toBe(actual.shellRight); + } + expect(actual.panels.every((panel) => panel.top >= height && !panel.intersectsTools)).toBe(true); + } finally { + await page.close(); + } + }); + } + } + } });