From ea38368b705295ce13a0bf0c3a09bbaf5b2b8950 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 20 Oct 2026 20:16:09 -0700 Subject: [PATCH 1/5] test(next): the folder trust bar takes the composer card's corner Red on base: the bar is 12px under the card's 11px (inline look) or 18px corner. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bf1-fdc7-79b0-b869-8ac78b0edd90&host=cosmic-gray --- .../test/agent-pane-trust-ask-corner.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 webviews/test/agent-pane-trust-ask-corner.test.ts diff --git a/webviews/test/agent-pane-trust-ask-corner.test.ts b/webviews/test/agent-pane-trust-ask-corner.test.ts new file mode 100644 index 000000000000..c434a6fcc47c --- /dev/null +++ b/webviews/test/agent-pane-trust-ask-corner.test.ts @@ -0,0 +1,56 @@ +// The folder trust bar over the composer takes the composer card's corner (a layout pass found the +// bar at 8px under a 16px card): both read --acpmux-composer-radius, so whatever corner the card's +// look sets, the bar matches it. Real gallery frames at a wide and a narrow pane. +// +// Real Chromium from Playwright; CI fails the file when it cannot launch (requireBrowserLane). +import { afterAll, beforeAll, expect, setDefaultTimeout, test } from "bun:test"; +import path from "node:path"; +import { chromium, type Browser } from "playwright"; +import { requireBrowserLane } from "./support/requireBrowserLane"; +import { warmViteServer } from "./support/warmDevServer"; + +await requireBrowserLane("agent-pane-trust-ask-corner.test.ts", async () => { + setDefaultTimeout(240_000); + const webviews = path.resolve(import.meta.dir, ".."); + const { createServer } = await import("vite"); + let vite: Awaited> | undefined; + let browser: Browser | undefined; + + beforeAll(async () => { + vite = await createServer({ + configFile: path.join(webviews, "vite.config.gallery-dev.ts"), + server: { port: 0, host: "127.0.0.1", strictPort: false }, + logLevel: "error", + }); + await vite.listen(); + await warmViteServer(vite, "/gallery/frame.html"); + browser = await chromium.launch({ headless: true }); + }); + afterAll(async () => { + await browser?.close(); + await vite?.close(); + }); + + test("the trust bar's corner is the composer card's", async () => { + const port = (vite!.httpServer!.address() as { port: number }).port; + const found: string[] = []; + for (const width of [1100, 420]) { + const page = await browser!.newPage({ viewport: { width, height: 760 } }); + await page.goto( + `http://127.0.0.1:${port}/gallery/frame.html?entry=agent-pane.bottom-stack&variant=trust-permission-queue`, + ); + // The first page waits on a cold Vite compiling the gallery, past 30 s on a busy runner. + await page.locator(".acpmux-trust-ask").first().waitFor({ timeout: 90_000 }); + await page.waitForFunction(() => document.documentElement.dataset.galleryReady === "1", undefined, { + timeout: 30_000, + }); + const corners = await page.evaluate(() => ({ + bar: getComputedStyle(document.querySelector(".acpmux-trust-ask")!).borderTopLeftRadius, + card: getComputedStyle(document.querySelector(".acpmux-composer-box")!).borderTopLeftRadius, + })); + await page.close(); + if (corners.bar !== corners.card) found.push(`@${width}: bar ${corners.bar}, card ${corners.card}`); + } + expect(found).toEqual([]); + }); +}); From 74003f1df735746abc12064a961917df43c7f424 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 20 Oct 2026 20:16:15 -0700 Subject: [PATCH 2/5] fix(next): the folder trust bar takes the composer card's corner The bar and the card read one --acpmux-composer-radius: the rules that set the card's corner (18px, and 11px in the inline look) also set it on the asks stack, so the bar follows whatever corner the card's look sets. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bf1-fdc7-79b0-b869-8ac78b0edd90&host=cosmic-gray --- .../src/agent-session/acpmux/composerLocation.css | 13 +++++++++++-- webviews/src/agent-session/acpmux/styles.css | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/webviews/src/agent-session/acpmux/composerLocation.css b/webviews/src/agent-session/acpmux/composerLocation.css index 33794134a4cd..7f0faa57ea94 100644 --- a/webviews/src/agent-session/acpmux/composerLocation.css +++ b/webviews/src/agent-session/acpmux/composerLocation.css @@ -338,6 +338,12 @@ .acpmux-access-grant-remove:hover { color: var(--agent-text); background: var(--agent-control-hover); } .acpmux-access-grant-remove:focus-visible { outline: 1.5px solid var(--agent-text); outline-offset: -1px; } +/* The card's corner. A question in the stack over it (the folder trust bar) takes the same token, + so the two always match. */ +.acpmux-composer-box, +.acpmux-asks-stack { + --acpmux-composer-radius: 18px; +} /* The card over a backdrop (a background image or a clear window): the card mixes its tint into the opaque base at 72% instead of into the clear page, so the prompt, the chips and the footer stay legible over a busy painting while the backdrop still shows through. On an opaque page the base is @@ -346,7 +352,6 @@ .acpmux-composer-box { --acpmux-composer-glass: color-mix(in srgb, var(--acpmux-base) 72%, transparent); overflow: visible; - --acpmux-composer-radius: 18px; border-radius: var(--acpmux-composer-radius); background: color-mix(in srgb, var(--agent-text) 13%, var(--acpmux-composer-glass)); } @@ -1096,12 +1101,16 @@ display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: end; - --acpmux-composer-radius: 11px; } /* The strip and the queue over the box take its corners. */ .acpmux-composer:has(.acpmux-composer-box > .acpmux-chrome-area > .acpmux-composer-inner) { --acpmux-strip-radius: 11px; } +/* The box's corner, which the trust bar in the stack over it shares. */ +.acpmux-composer-box:has(> .acpmux-chrome-area > .acpmux-composer-inner), +.acpmux-dock:has(.acpmux-composer-box > .acpmux-chrome-area > .acpmux-composer-inner) > .acpmux-asks-stack { + --acpmux-composer-radius: 11px; +} .acpmux-composer-inner { display: contents; } .acpmux-composer-box > .acpmux-attachments { grid-column: 1 / -1; grid-row: 1; } .acpmux-composer-box:has(> .acpmux-chrome-area > .acpmux-composer-inner) > .acpmux-composer-field-menu > * { diff --git a/webviews/src/agent-session/acpmux/styles.css b/webviews/src/agent-session/acpmux/styles.css index cd01d91e5d03..9ad61ec663a7 100644 --- a/webviews/src/agent-session/acpmux/styles.css +++ b/webviews/src/agent-session/acpmux/styles.css @@ -268,7 +268,7 @@ } /* Context (ComposerContext.tsx): composerLocation.css. */ /* The folder trust ask (TrustAsk.tsx): one quiet line among the chat's permission asks, its actions text buttons that step on hover and press. */ -.acpmux-trust-ask{display:flex;align-items:center;gap:4px;min-height:34px;margin-bottom:8px;padding:4px 4px 4px 12px;box-sizing:border-box;border-radius:12px;background:var(--acpmux-composer-tray);color:var(--acpmux-composer-dim);font-size:13px}.acpmux-trust-ask-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.acpmux-tool-card .acpmux-trust-ask-text{padding:4px 0;white-space:normal;overflow-wrap:anywhere}.acpmux-tool-card-input{flex:0 1 220px;min-width:80px;height:26px;padding:0 8px;box-sizing:border-box;border:1px solid var(--acpmux-composer-hover);border-radius:6px;background:var(--agent-page-bg,var(--acpmux-base));color:var(--agent-text);font:inherit}.acpmux-tool-card-input:focus{outline:none}.acpmux-tool-card:focus-within .acpmux-tool-card-input{border-color:var(--agent-text)}.acpmux-trust-ask-action:disabled{opacity:.5;cursor:default}.acpmux-trust-ask-action{flex:none;height:26px;padding:0 10px;border:0;border-radius:6px;background:none;color:var(--agent-text);font:inherit;cursor:pointer}.acpmux-trust-ask-action:hover{background:var(--acpmux-composer-hover)}.acpmux-trust-ask-action:active{background:color-mix(in srgb,var(--agent-text) 20%,var(--agent-page-bg,var(--acpmux-base)))}.acpmux-trust-ask-action:focus-visible{outline:1.5px solid var(--agent-text);outline-offset:1px}@media (prefers-reduced-motion:no-preference){.acpmux-trust-ask-action{transition:background-color var(--agent-motion-hover,80ms) var(--agent-ease-out,ease-out)}} +.acpmux-trust-ask{display:flex;align-items:center;gap:4px;min-height:34px;margin-bottom:8px;padding:4px 4px 4px 12px;box-sizing:border-box;border-radius:var(--acpmux-composer-radius,12px);background:var(--acpmux-composer-tray);color:var(--acpmux-composer-dim);font-size:13px}.acpmux-trust-ask-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.acpmux-tool-card .acpmux-trust-ask-text{padding:4px 0;white-space:normal;overflow-wrap:anywhere}.acpmux-tool-card-input{flex:0 1 220px;min-width:80px;height:26px;padding:0 8px;box-sizing:border-box;border:1px solid var(--acpmux-composer-hover);border-radius:6px;background:var(--agent-page-bg,var(--acpmux-base));color:var(--agent-text);font:inherit}.acpmux-tool-card-input:focus{outline:none}.acpmux-tool-card:focus-within .acpmux-tool-card-input{border-color:var(--agent-text)}.acpmux-trust-ask-action:disabled{opacity:.5;cursor:default}.acpmux-trust-ask-action{flex:none;height:26px;padding:0 10px;border:0;border-radius:6px;background:none;color:var(--agent-text);font:inherit;cursor:pointer}.acpmux-trust-ask-action:hover{background:var(--acpmux-composer-hover)}.acpmux-trust-ask-action:active{background:color-mix(in srgb,var(--agent-text) 20%,var(--agent-page-bg,var(--acpmux-base)))}.acpmux-trust-ask-action:focus-visible{outline:1.5px solid var(--agent-text);outline-offset:1px}@media (prefers-reduced-motion:no-preference){.acpmux-trust-ask-action{transition:background-color var(--agent-motion-hover,80ms) var(--agent-ease-out,ease-out)}} /* Search files: a quick-open palette, centered over the transcript's upper part (main column). */ .acpmux-file-search{position:absolute;z-index:20;top:max(48px,18%);left:50%;right:auto;margin:0;border:0;width:min(600px,calc(100vw - 32px));transform:translateX(-50%);padding:6px;box-sizing:border-box;border-radius:14px;background:var(--surface-popup);box-shadow:inset 0 0 0 var(--cmux-hairline, .5px) var(--acpmux-composer-edge),var(--elevation-2);color:var(--agent-text);font-size:14px} .acpmux-file-search input{display:block;width:100%;height:34px;padding:0 10px;box-sizing:border-box;border:0;background:none;color:var(--agent-text);font:inherit;outline:none} From 6f3700c56e3cd33ded7d11faa10767147b43f51e Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 20 Oct 2026 20:21:30 -0700 Subject: [PATCH 3/5] test(next): no caption repeats the trust question, and the fade reaches past Jump to latest Red on base: "Answer the trust question first" stands between the trust bar and the card, and the bottom fade stays 18px under the 44px-high Jump to latest button. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bf1-fdc7-79b0-b869-8ac78b0edd90&host=cosmic-gray --- webviews/test/agent-pane-column-edges.test.ts | 1 - .../test/agent-pane-trust-ask-corner.test.ts | 24 ++++++++++++++++--- .../agent-pane-wheel-pass-through.test.ts | 6 +++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/webviews/test/agent-pane-column-edges.test.ts b/webviews/test/agent-pane-column-edges.test.ts index 97e8af03716a..80c9e2e4ae25 100644 --- a/webviews/test/agent-pane-column-edges.test.ts +++ b/webviews/test/agent-pane-column-edges.test.ts @@ -36,7 +36,6 @@ await requireBrowserLane("agent-pane-column-edges.test.ts", async () => { edge: ".acpmux-composer-box", blocks: { "trust ask": ".acpmux-trust-ask", - "trust note": ".acpmux-composer-remote-note", queue: ".acpmux-composer-queue", }, }, diff --git a/webviews/test/agent-pane-trust-ask-corner.test.ts b/webviews/test/agent-pane-trust-ask-corner.test.ts index c434a6fcc47c..5dbae99a7f75 100644 --- a/webviews/test/agent-pane-trust-ask-corner.test.ts +++ b/webviews/test/agent-pane-trust-ask-corner.test.ts @@ -1,6 +1,7 @@ -// The folder trust bar over the composer takes the composer card's corner (a layout pass found the -// bar at 8px under a 16px card): both read --acpmux-composer-radius, so whatever corner the card's -// look sets, the bar matches it. Real gallery frames at a wide and a narrow pane. +// l10n-allow-file: asserts the English tooltip the gallery renders, not shipped UI. +// The folder trust bar over the composer (layout pass, hq#2696): it takes the composer card's corner +// (both read --acpmux-composer-radius, so whatever corner the card's look sets, the bar matches it), +// and no caption repeats its question over the card. Real gallery frames at a wide and a narrow pane. // // Real Chromium from Playwright; CI fails the file when it cannot launch (requireBrowserLane). import { afterAll, beforeAll, expect, setDefaultTimeout, test } from "bun:test"; @@ -53,4 +54,21 @@ await requireBrowserLane("agent-pane-trust-ask-corner.test.ts", async () => { } expect(found).toEqual([]); }); + + // The layout pass's "orphan caption": "Answer the trust question first" stood between the bar + // that asks it and the card. The question says it; Send's tooltip keeps the reason. + test("no caption stands between the open trust question and the card", async () => { + const port = (vite!.httpServer!.address() as { port: number }).port; + const page = await browser!.newPage({ viewport: { width: 1100, height: 760 } }); + await page.goto( + `http://127.0.0.1:${port}/gallery/frame.html?entry=agent-pane.bottom-stack&variant=trust-permission-queue`, + ); + await page.locator(".acpmux-trust-ask").first().waitFor({ timeout: 90_000 }); + await page.waitForFunction(() => document.documentElement.dataset.galleryReady === "1", undefined, { + timeout: 30_000, + }); + expect(await page.locator(".acpmux-composer-remote-note").count()).toBe(0); + expect(await page.locator(".acpmux-send").first().getAttribute("title")).toBe("Answer the trust question first"); + await page.close(); + }); }); diff --git a/webviews/test/agent-pane-wheel-pass-through.test.ts b/webviews/test/agent-pane-wheel-pass-through.test.ts index 02ab68079d8d..df98b237e21b 100644 --- a/webviews/test/agent-pane-wheel-pass-through.test.ts +++ b/webviews/test/agent-pane-wheel-pass-through.test.ts @@ -67,6 +67,12 @@ await requireBrowserLane("agent-pane-wheel-pass-through.test.ts", async () => { // Up from the latest message, so the button shows. expect(await wheelOver(page, ".acpmux-scroll", -1200)).toBeLessThan(0); await page.locator(".acpmux-jump-latest[data-shown]").waitFor(); + // The bottom fade reaches past the button, so rows pass out of sight behind it (layout pass, + // hq#2696: "overlaps the date line, with no fade"). + const fade = await page.evaluate(() => + getComputedStyle(document.querySelector(".acpmux-scroll")!).getPropertyValue("--acpmux-fade-bottom").trim(), + ); + expect(fade).toBe("56px"); expect(await wheelOver(page, ".acpmux-jump-latest", -300)).toBeLessThan(-100); expect(await wheelOver(page, ".acpmux-jump-latest", 200)).toBeGreaterThan(100); await page.locator(".acpmux-jump-latest").click(); From 2091fecbbd40d682c90e89353ada4c0ec9295db9 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 20 Oct 2026 20:21:33 -0700 Subject: [PATCH 4/5] fix(next): drop the orphan trust caption, fade the transcript behind Jump to latest The open trust question's "Answer the trust question first" no longer draws as a caption between the bar and the card (SendBlock.shownAbove); Send's tooltip keeps it. While Jump to latest shows, the transcript's bottom fade runs 56px, past the button, so a date line passes out of sight behind it. From the layout pass (hq#2696). Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bf1-fdc7-79b0-b869-8ac78b0edd90&host=cosmic-gray --- webviews/src/agent-session/acpmux/App.tsx | 3 +++ webviews/src/agent-session/acpmux/Composer.tsx | 2 +- .../src/agent-session/acpmux/conversation/conversation.css | 4 ++++ webviews/src/agent-session/acpmux/useFolderTrustAsk.ts | 7 ++++--- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/webviews/src/agent-session/acpmux/App.tsx b/webviews/src/agent-session/acpmux/App.tsx index 6b4ae04c01e1..01aa908d0fd6 100644 --- a/webviews/src/agent-session/acpmux/App.tsx +++ b/webviews/src/agent-session/acpmux/App.tsx @@ -2586,6 +2586,9 @@ export function VirtualTranscript({ aria-busy={seeking || undefined} data-fade-top={renderScroll.top > 0.5 || undefined} data-fade-bottom={(!renderFollowing && layout.totalHeight - renderScroll.top - height > 0.5) || undefined} + // Jump to latest stands in that band: the fade reaches past it, so rows (a date line) pass + // out of sight behind it instead of reading through its glass. + data-jump-shown={(jumpButton && behind) || undefined} // Under the bottom stack: rows there stay out of sight (conversation.css), and focus or Find // scroll a row into view above it. data-docked={dockSpace > 0 || undefined} diff --git a/webviews/src/agent-session/acpmux/Composer.tsx b/webviews/src/agent-session/acpmux/Composer.tsx index 2b6f337ec96a..fea8109eeb3b 100644 --- a/webviews/src/agent-session/acpmux/Composer.tsx +++ b/webviews/src/agent-session/acpmux/Composer.tsx @@ -1304,7 +1304,7 @@ export function Composer({ {t(remote.note)}

)} - {!remote.note && blocked?.reason && ( + {!remote.note && blocked?.reason && !blocked.shownAbove && (

{t(blocked.reason)}

diff --git a/webviews/src/agent-session/acpmux/conversation/conversation.css b/webviews/src/agent-session/acpmux/conversation/conversation.css index f08e78c8a719..7250d8f2c5e0 100644 --- a/webviews/src/agent-session/acpmux/conversation/conversation.css +++ b/webviews/src/agent-session/acpmux/conversation/conversation.css @@ -2600,6 +2600,10 @@ button.cv-received__name:focus-visible { .acpmux-scroll[data-fade-bottom] { --acpmux-fade-bottom: 18px; } +/* Jump to latest (12px up, 32px tall) stands in the bottom band: the fade runs a gap past its top. */ +.acpmux-scroll[data-fade-bottom][data-jump-shown] { + --acpmux-fade-bottom: 56px; +} /* Find in Chat's bar sits in the top band: it keeps full ink, and rows pass under it. */ .acpmux-scroll[data-fade-top]:has(> .acpmux-find) { --acpmux-fade-top: 0px; diff --git a/webviews/src/agent-session/acpmux/useFolderTrustAsk.ts b/webviews/src/agent-session/acpmux/useFolderTrustAsk.ts index 6b1a1368caa5..8756edd983ac 100644 --- a/webviews/src/agent-session/acpmux/useFolderTrustAsk.ts +++ b/webviews/src/agent-session/acpmux/useFolderTrustAsk.ts @@ -16,8 +16,9 @@ export type FolderTrustAsk = | { cwd: string; state: "ask" | "failed" | "remote" } | { cwd: string; state: "decided"; level: Exclude }; -/// Why Send is off; no `reason` while the folder's trust is still being read. -export type SendBlock = { reason?: StringKey }; +/// Why Send is off; no `reason` while the folder's trust is still being read. `shownAbove`: the +/// question over the card already says it, so the card draws no caption (Send's tooltip keeps it). +export type SendBlock = { reason?: StringKey; shownAbove?: boolean }; /// The last answer read for each folder, kept for the next chat there (another tab, workspace, or /// a new page from the pool): a switch draws the folder's known trust from its first render, and @@ -231,7 +232,7 @@ export function useFolderTrustAsk( const blocked: SendBlock | undefined = reading ? {} : ask?.state === "ask" || ask?.state === "failed" - ? { reason: "trust.answerFirst" } + ? { reason: "trust.answerFirst", shownAbove: true } : ask?.state === "remote" ? { reason: "trust.remote" } : ask?.state === "decided" && ask.level === "untrusted" From ea5f9ce67a408851f37d8a3e2b828e4575dd5d45 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 20 Oct 2026 20:23:44 -0700 Subject: [PATCH 5/5] test(next): the open trust question's block says the bar shows its reason Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bf1-fdc7-79b0-b869-8ac78b0edd90&host=cosmic-gray --- webviews/test/trust-read-keeps-permission.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviews/test/trust-read-keeps-permission.test.tsx b/webviews/test/trust-read-keeps-permission.test.tsx index 7600563167e9..1a4a84fe2fdc 100644 --- a/webviews/test/trust-read-keeps-permission.test.tsx +++ b/webviews/test/trust-read-keeps-permission.test.tsx @@ -129,7 +129,7 @@ test("an answer saved for another folder leaves this folder's read standing", as await trust.saved(); await trust.answer("unknown", second); expect(shown!.ask).toEqual({ cwd: second, state: "ask" }); - expect(shown!.blocked).toEqual({ reason: "trust.answerFirst" }); + expect(shown!.blocked).toEqual({ reason: "trust.answerFirst", shownAbove: true }); }); // Leo 2026-10-19: a switch to another chat in the very same folder checked the folder's trust