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
11 changes: 7 additions & 4 deletions webviews/src/agent-session/acpmux/conversation/conversation.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
19 changes: 15 additions & 4 deletions webviews/src/agent-session/acpmux/inspector/inspector.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 51 additions & 6 deletions webviews/src/agent-session/acpmux/summary/SummaryCard.gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { realWorkflowFixture } from "../realWorkflowFixture";
type Props = Pick<
ComponentProps<typeof SummaryCard>,
"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.
Expand All @@ -38,17 +38,31 @@ const card: SummaryCardControl = {
fold: none,
};

function GallerySummaryCard({ tabRowHeight, preview, ...props }: Props) {
function GallerySummaryCard({ tabRowHeight, preview, inspectorPush, ...props }: Props) {
return (
<div
className="acpmux-shell"
data-tab-row={tabRowHeight ? "" : undefined}
style={{ height: "100%", "--acpmux-tab-row-height": `${tabRowHeight ?? 0}px` } as CSSProperties}
data-inspector-chat-placement={inspectorPush ? "push" : undefined}
style={
{
height: "100%",
"--acpmux-tab-row-height": `${tabRowHeight ?? 0}px`,
...(inspectorPush
? {
"--acpmux-inspector-top": tabRowHeight ? `${tabRowHeight}px` : "28px",
// ChatInspector.tsx writes this as a measured shell px value. The fixture is 1000px
// wide, with Preview's 46vw artifact resolving to 460px at that width.
"--acpmux-header-room": preview ? "540px" : "1000px",
}
: {}),
} as CSSProperties
}
>
<div className="acpmux-main" data-summary="inline">
{tabRowHeight && (
{tabRowHeight !== undefined && (
<div className="acpmux-stage">
<header className="acpmux-header" data-tab-row="">
<header className="acpmux-header" data-tab-row={tabRowHeight ? "" : undefined}>
<div />
<div className="acpmux-header-tools">
<SummaryButton card={card} />
Expand All @@ -57,12 +71,31 @@ function GallerySummaryCard({ tabRowHeight, preview, ...props }: Props) {
</div>
)}
</div>
{inspectorPush && (
<aside
className="ui-glass ui-glass-floats acpmux-chat-inspector"
data-placement="push"
data-presence="shown"
aria-label="Session Info"
style={
{
width: "220px",
"--acpmux-inspector-top": tabRowHeight ? `${tabRowHeight}px` : "28px",
} as CSSProperties
}
>
<div className="acpmux-chat-inspector__head">Session Info</div>
<div className="acpmux-chat-inspector__body">
<div className="acpmux-chat-inspector-section">Pushed Session Info</div>
</div>
</aside>
)}
{preview ? (
<ArtifactPanel
artifact={{ kind: "markdown", lang: "markdown", code: "# Preview\nA document beside the chat." }}
onClose={none}
/>
) : (
) : inspectorPush ? null : (
<SummaryCard
card={card}
subagents={{ views: undefined, current: undefined, asks: new Set(), onOpen: none }}
Expand Down Expand Up @@ -167,6 +200,18 @@ export default componentEntry<Props>({
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: {
Expand Down
4 changes: 2 additions & 2 deletions webviews/src/agent-session/acpmux/summary/summary.css

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

94 changes: 93 additions & 1 deletion webviews/test/summary-header-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<HTMLElement>(".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<HTMLElement>(".acpmux-shell")!;
const shellRect = shell.getBoundingClientRect();
const tools = document.querySelector<HTMLElement>(".acpmux-header-tools")!.getBoundingClientRect();
const panels = [
...document.querySelectorAll<HTMLElement>(
".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();
}
});
}
}
}
});
Loading