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
3 changes: 3 additions & 0 deletions webviews/src/agent-session/acpmux/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion webviews/src/agent-session/acpmux/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1304,7 +1304,7 @@ export function Composer({
{t(remote.note)}
</p>
)}
{!remote.note && blocked?.reason && (
{!remote.note && blocked?.reason && !blocked.shownAbove && (
<p className="acpmux-composer-remote-note acpmux-composer-trust-note" role="note">
{t(blocked.reason)}
</p>
Expand Down
13 changes: 11 additions & 2 deletions webviews/src/agent-session/acpmux/composerLocation.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
}
Expand Down Expand Up @@ -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 > * {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion webviews/src/agent-session/acpmux/styles.css

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

7 changes: 4 additions & 3 deletions webviews/src/agent-session/acpmux/useFolderTrustAsk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export type FolderTrustAsk =
| { cwd: string; state: "ask" | "failed" | "remote" }
| { cwd: string; state: "decided"; level: Exclude<TrustLevel, "unknown"> };

/// 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
Expand Down Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion webviews/test/agent-pane-column-edges.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
Expand Down
74 changes: 74 additions & 0 deletions webviews/test/agent-pane-trust-ask-corner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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";
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<ReturnType<typeof createServer>> | 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([]);
});

// 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();
});
});
6 changes: 6 additions & 0 deletions webviews/test/agent-pane-wheel-pass-through.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion webviews/test/trust-read-keeps-permission.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down