diff --git a/tools/web-ux/browser-tests/console_pending_shell_test.ts b/tools/web-ux/browser-tests/console_pending_shell_test.ts index 7be6c4046..a419e985f 100644 --- a/tools/web-ux/browser-tests/console_pending_shell_test.ts +++ b/tools/web-ux/browser-tests/console_pending_shell_test.ts @@ -64,6 +64,62 @@ Deno.test("Console pending scopes are independent and match Tasks row typography `${url}/w/console-history-review/workers/W-900-console-fixture/console?pending=${mode}`, ); await page.locator(".task-mini-row").waitFor(); + const metadata = page.getByLabel("Worker model and context", { exact: true }); + const statusGeometry = await metadata.evaluate((status) => { + const task = document.querySelector(".task-mini-row")!; + const composer = document.querySelector(".console-composer")!; + const metrics = (node: Element) => { + const style = getComputedStyle(node); + return [style.fontFamily, style.fontSize, style.lineHeight]; + }; + const rect = status.getBoundingClientRect(); + return { + typography: metrics(status), + taskTypography: metrics(task), + height: rect.height, + belowComposer: rect.top >= composer.getBoundingClientRect().bottom, + alignedWithTasks: rect.left + parseFloat(getComputedStyle(status).paddingLeft) === task.getBoundingClientRect().left, + visible: rect.bottom <= innerHeight, + wrap: getComputedStyle(status).flexWrap, + pageOverflow: document.documentElement.scrollWidth > innerWidth, + }; + }); + assertEquals(statusGeometry.typography, statusGeometry.taskTypography); + assertEquals(statusGeometry.height, 16); + assertEquals(statusGeometry.belowComposer, true); + assertEquals(statusGeometry.alignedWithTasks, true); + assertEquals(statusGeometry.visible, true); + assertEquals(statusGeometry.wrap, "nowrap"); + assertEquals(statusGeometry.pageOverflow, false); + const details = metadata.getByRole("button", { name: "Details", exact: true }); + assertEquals(await page.locator(".console-header").getByRole("button", { name: "Details" }).count(), 0); + const detailsGeometry = await details.evaluate((button) => { + const row = button.closest(".worker-context-status")!; + const rect = button.getBoundingClientRect(); + const rowRect = row.getBoundingClientRect(); + return { + rightAligned: Math.abs(rect.right - (rowRect.right - parseFloat(getComputedStyle(row).paddingRight))) < 1, + centered: Math.abs(rect.top + rect.height / 2 - (rowRect.top + rowRect.height / 2)) < 1, + width: rect.width, + }; + }); + assertEquals(detailsGeometry.rightAligned, true); + assertEquals(detailsGeometry.centered, true); + assert(detailsGeometry.width > 0); + await details.focus(); + await page.keyboard.press("Enter"); + assertEquals(await details.getAttribute("aria-expanded"), "true"); + await page.getByRole("button", { name: "Close", exact: true }).waitFor(); + await page.keyboard.press("Space"); + assertEquals(await details.getAttribute("aria-expanded"), "false"); + await details.click(); + assertEquals(await details.getAttribute("aria-expanded"), "true"); + await page.getByRole("button", { name: "Close", exact: true }).click(); + assertEquals(await details.getAttribute("aria-expanded"), "false"); + if (mode === "both") { + await Deno.mkdir(join(root, "target/web-ux/composer-metadata"), { recursive: true }); + await page.screenshot({ path: join(root, `target/web-ux/composer-metadata/${colorScheme}-${width}.png`) }); + } const queue = mode === "both" || mode === "queue"; const notifications = ["both", "notifications", "legacy"].includes(mode); assertEquals( diff --git a/web/workspace/src/lib/workspace/console/WorkerContextStatus.browser.spec.ts b/web/workspace/src/lib/workspace/console/WorkerContextStatus.browser.spec.ts index f45776f62..fd37c8bcf 100644 --- a/web/workspace/src/lib/workspace/console/WorkerContextStatus.browser.spec.ts +++ b/web/workspace/src/lib/workspace/console/WorkerContextStatus.browser.spec.ts @@ -6,6 +6,23 @@ import WorkerContextStatus from "./WorkerContextStatus.svelte"; afterEach(cleanup); +test("the three-dot Details control invokes the existing toggle and reflects its state", async () => { + let toggles = 0; + const view = render(WorkerContextStatus, { + metadata: null, + detailsOpen: false, + onToggleDetails: () => { toggles++; }, + }); + const button = view.getByRole("button", { name: "Details" }); + expect(button.querySelectorAll("circle")).toHaveLength(3); + expect(button.textContent?.trim()).toBe(""); + expect(button.getAttribute("aria-expanded")).toBe("false"); + button.click(); + expect(toggles).toBe(1); + await view.rerender({ detailsOpen: true }); + expect(button.getAttribute("aria-expanded")).toBe("true"); +}); + test("renders resolved model, reasoning, and measured context accessibly", () => { const view = render(WorkerContextStatus, { metadata: { @@ -19,13 +36,13 @@ test("renders resolved model, reasoning, and measured context accessibly", () => const status = view.getByLabelText("Worker model and context"); expect(status.textContent).toContain("gpt-6-astra · high"); - expect(status.textContent).toContain("Context 142.0k / 272.0k (52%)"); + expect(status.textContent).toContain("142.0k / 272.0k (52%)"); }); test("renders honest unavailable state instead of a fabricated zero percent", async () => { const view = render(WorkerContextStatus, { metadata: null }); expect(view.getByText("Model unavailable")).not.toBeNull(); - expect(view.getByText("Context unavailable")).not.toBeNull(); + expect(view.getByText("unavailable")).not.toBeNull(); expect(view.container.textContent).not.toContain("0%"); await view.rerender({ @@ -38,7 +55,7 @@ test("renders honest unavailable state instead of a fabricated zero percent", as }, }); expect(view.container.textContent).toContain("claude · 8.2k token budget"); - expect(view.container.textContent).toContain("Context ~12.0k / 64.0k (19%)"); + expect(view.container.textContent).toContain("~12.0k / 64.0k (19%)"); await view.rerender({ metadata: { diff --git a/web/workspace/src/lib/workspace/console/WorkerContextStatus.svelte b/web/workspace/src/lib/workspace/console/WorkerContextStatus.svelte index 248cf5025..c2fdb3e65 100644 --- a/web/workspace/src/lib/workspace/console/WorkerContextStatus.svelte +++ b/web/workspace/src/lib/workspace/console/WorkerContextStatus.svelte @@ -7,9 +7,11 @@ type Props = { metadata: ConsoleWorkerMetadata | null; + detailsOpen?: boolean; + onToggleDetails?: () => void; }; - let { metadata }: Props = $props(); + let { metadata, detailsOpen = false, onToggleDetails }: Props = $props(); const modelSummary = $derived(formatModelSummary(metadata)); const contextSummary = $derived(formatContextSummary(metadata)); @@ -18,12 +20,32 @@ {modelSummary} {contextSummary} + {#if onToggleDetails} + + {/if} diff --git a/web/workspace/src/lib/workspace/console/run-status.test.ts b/web/workspace/src/lib/workspace/console/run-status.test.ts index 29de10947..bc8a9f989 100644 --- a/web/workspace/src/lib/workspace/console/run-status.test.ts +++ b/web/workspace/src/lib/workspace/console/run-status.test.ts @@ -45,7 +45,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely" contextTokens: 142_000, contextSource: "estimated", }), - "Context ~142.0k / 272.0k (52%)", + "~142.0k / 272.0k (52%)", ); assertEquals(formatModelSummary(null), "Model unavailable"); assertEquals( @@ -64,7 +64,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely" formatReasoning({ kind: "budget_tokens", budget_tokens: -2 }), "reasoning unavailable", ); - assertEquals(formatContextSummary(null), "Context unavailable"); + assertEquals(formatContextSummary(null), "unavailable"); assertEquals( formatContextSummary({ model: null, @@ -73,7 +73,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely" contextTokens: 0, contextSource: null, }), - "Context unavailable", + "unavailable", ); }); diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index ba0543d4d..9160e9583 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -738,6 +738,26 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps" ); }); +Deno.test("Model information stays below Composer in one compact monospace row", async () => { + const page = await Deno.readTextFile(new URL( + "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", + import.meta.url, + )); + const composer = page.indexOf('
+