From b0d49737176fb7e511a0baa6ff8f1c410e7bc8cc Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 4 Oct 2026 21:03:16 +0900 Subject: [PATCH] fix: align console metadata and details controls --- .../console_pending_shell_test.ts | 56 ++++++++++++++++ .../WorkerContextStatus.browser.spec.ts | 23 ++++++- .../console/WorkerContextStatus.svelte | 65 +++++++++++++++---- .../lib/workspace/console/run-status.test.ts | 6 +- .../console/worker-console.ui.test.ts | 20 ++++++ .../lib/workspace/console/worker-metadata.ts | 6 +- .../workers/[workerRef]/console/+page.svelte | 22 +++---- 7 files changed, 162 insertions(+), 36 deletions(-) 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('
", composer); + const status = page.indexOf("= 0 && composerEnd > composer && status > composerEnd, "Model information must follow Composer"); + assert(page.includes("metadata={selectedConsoleProjection.workerMetadata}"), "Keep selected Worker metadata"); + assert(page.indexOf(" { const files = [ "ComposerInput.svelte", diff --git a/web/workspace/src/lib/workspace/console/worker-metadata.ts b/web/workspace/src/lib/workspace/console/worker-metadata.ts index 03f9618e3..d9217cb61 100644 --- a/web/workspace/src/lib/workspace/console/worker-metadata.ts +++ b/web/workspace/src/lib/workspace/console/worker-metadata.ts @@ -36,11 +36,11 @@ export function formatContextSummary( used === null || used === undefined || limit === null || limit === undefined || !Number.isSafeInteger(used) || !Number.isSafeInteger(limit) || used < 0 || limit <= 0 - ) return "Context unavailable"; + ) return "unavailable"; const percent = Math.round((used / limit) * 100); - if (!Number.isFinite(percent)) return "Context unavailable"; + if (!Number.isFinite(percent)) return "unavailable"; const estimate = metadata?.contextSource === "estimated" ? "~" : ""; - return `Context ${estimate}${formatRunTokens(used)} / ${ + return `${estimate}${formatRunTokens(used)} / ${ formatRunTokens(limit) } (${percent}%)`; } diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte index 773e1996d..1a00e244e 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte @@ -1652,7 +1652,6 @@
-
-
@@ -2094,6 +2082,14 @@
+ { + workerDetailsOpen = !workerDetailsOpen; + if (workerDetailsOpen) taskPaneOpen = false; + }} + />