From fec85dd38d8c8fc5308a4deca2cbad10a1cda9aa Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 5 Oct 2026 01:58:22 +0900 Subject: [PATCH] fix: simplify console mode controls with a compact footer toggle --- .../console_pending_shell_test.ts | 52 ++++++- .../console/WorkerContextStatus.svelte | 62 +++++---- .../console/worker-console.ui.test.ts | 20 ++- .../workers/[workerRef]/console/+page.svelte | 127 ++++++++---------- .../console/console-display.browser.spec.ts | 21 ++- 5 files changed, 168 insertions(+), 114 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 a419e985f..57e6f7a16 100644 --- a/tools/web-ux/browser-tests/console_pending_shell_test.ts +++ b/tools/web-ux/browser-tests/console_pending_shell_test.ts @@ -85,14 +85,14 @@ Deno.test("Console pending scopes are independent and match Tasks row typography }; }); assertEquals(statusGeometry.typography, statusGeometry.taskTypography); - assertEquals(statusGeometry.height, 16); + assertEquals(statusGeometry.height, 24); 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); + assertEquals(await page.locator(".console-header").count(), 0); const detailsGeometry = await details.evaluate((button) => { const row = button.closest(".worker-context-status")!; const rect = button.getBoundingClientRect(); @@ -105,7 +105,49 @@ Deno.test("Console pending scopes are independent and match Tasks row typography }); assertEquals(detailsGeometry.rightAligned, true); assertEquals(detailsGeometry.centered, true); - assert(detailsGeometry.width > 0); + assertEquals(detailsGeometry.width, 24); + const overview = metadata.getByRole("switch", { name: "Overview", exact: true }); + const controls = await overview.evaluate((group) => { + const row = group.closest(".worker-context-status")!; + const rowRect = row.getBoundingClientRect(); + const buttons = [...row.querySelectorAll("button")]; + return { + buttonsInsideRow: buttons.every((button) => { + const rect = button.getBoundingClientRect(); + return rect.left >= rowRect.left && rect.right <= rowRect.right && + rect.top >= rowRect.top && rect.bottom <= rowRect.bottom; + }), + heights: buttons.map((button) => button.getBoundingClientRect().height), + modeFont: getComputedStyle(buttons[0]).fontFamily, + divider: getComputedStyle(buttons[1]).borderLeftWidth, + beforeDetails: group.nextElementSibling?.getAttribute("aria-label") === "Details", + }; + }); + assertEquals(controls.buttonsInsideRow, true); + assertEquals(controls.heights, [24, 24]); + assertEquals(controls.modeFont.includes("mono"), false); + assertEquals(controls.divider, "0px"); + assertEquals(controls.beforeDetails, true); + const onSize = (await overview.boundingBox())!; + assert(onSize.width < 90, "Switch and fixed label should use less than 90px"); + assertEquals(await overview.getAttribute("aria-checked"), "true"); + await overview.focus(); + assertEquals(await overview.evaluate((button) => getComputedStyle(button).outlineStyle), "solid"); + await page.keyboard.press("Enter"); + assertEquals(await overview.getAttribute("aria-checked"), "false"); + assertEquals((await overview.textContent())!.trim(), "Overview"); + assertEquals((await overview.boundingBox())!.width, onSize.width); + if (mode === "both") { + await Deno.mkdir(join(root, "target/web-ux/console-footer-switch"), { recursive: true }); + await page.screenshot({ path: join(root, `target/web-ux/console-footer-switch/${colorScheme}-${width}-off.png`) }); + } + await page.keyboard.press("Space"); + assertEquals(await overview.getAttribute("aria-checked"), "true"); + assertEquals((await overview.boundingBox())!.width, onSize.width); + await overview.locator("span").last().click(); + assertEquals(await overview.getAttribute("aria-checked"), "false"); + await overview.locator(".console-view-toggle-track").click(); + assertEquals(await overview.getAttribute("aria-checked"), "true"); await details.focus(); await page.keyboard.press("Enter"); assertEquals(await details.getAttribute("aria-expanded"), "true"); @@ -117,8 +159,8 @@ Deno.test("Console pending scopes are independent and match Tasks row typography 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`) }); + await Deno.mkdir(join(root, "target/web-ux/console-footer-switch"), { recursive: true }); + await page.screenshot({ path: join(root, `target/web-ux/console-footer-switch/${colorScheme}-${width}.png`) }); } const queue = mode === "both" || mode === "queue"; const notifications = ["both", "notifications", "legacy"].includes(mode); diff --git a/web/workspace/src/lib/workspace/console/WorkerContextStatus.svelte b/web/workspace/src/lib/workspace/console/WorkerContextStatus.svelte index c2fdb3e65..6f6ad9db1 100644 --- a/web/workspace/src/lib/workspace/console/WorkerContextStatus.svelte +++ b/web/workspace/src/lib/workspace/console/WorkerContextStatus.svelte @@ -1,4 +1,5 @@
{modelSummary} - + {contextSummary} - {#if onToggleDetails} - + {#if controls || onToggleDetails} +
+ {@render controls?.()} + {#if onToggleDetails} + + {/if} +
{/if}
@@ -72,30 +79,39 @@ color: var(--line-strong); } + .worker-context-actions { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: var(--space-2); + margin-left: auto; + } + .details-button { display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; - width: 20px; - height: var(--line-height-compact); - margin-left: auto; + width: var(--space-5); + height: var(--space-5); padding: 0; border: 0; + border-radius: var(--space-1); background: transparent; color: var(--text-muted); cursor: pointer; } .details-button:hover, - .details-button[aria-expanded="true"], - .details-button:focus-visible { - color: var(--text); + .details-button[aria-expanded="true"] { + background: var(--bg-subtle); + color: var(--text-strong); } .details-button:focus-visible { + color: var(--text-strong); outline: 1px solid currentColor; - outline-offset: -1px; + outline-offset: -2px; } .details-button svg { 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 9160e9583..6194e31f5 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 @@ -756,6 +756,12 @@ Deno.test("Model information stays below Composer in one compact monospace row", "line-height: var(--line-height-compact)", "padding-inline: var(--space-3)", ]) assert(component.includes(rule), rule); assert(!component.includes("@media"), "Narrow layouts must retain the same single row"); + const modes = page.indexOf('class="console-view-toggle"'); + assert(modes > status && modes < page.indexOf("", status), "Display toggle must be in the model information row"); + assert(!page.includes("console-header"), "Remove the empty header and its styles"); + assert(component.indexOf("{@render controls?.()}") < component.indexOf('class="details-button"'), "Display controls must precede Details"); + assert(/\.worker-context-actions\s*\{[^}]*flex: 0 0 auto/.test(component), "Keep display controls from shrinking on narrow screens"); + assert(component.includes("margin-left: auto"), "Align footer controls to the right"); }); Deno.test("Console spacing and text metrics use existing design tokens", async () => { @@ -1571,14 +1577,10 @@ Deno.test("Composer owns TUI-style completion and replaces Compact/Rewind header const input = await Deno.readTextFile( new URL("./ComposerInput.svelte", import.meta.url), ); - const header = page.slice( - page.indexOf('
{"), "Only the mini summary should own Tasks toggling", ); 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 71ce6a5a0..c3b5713af 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 @@ -1663,33 +1663,6 @@
-
-
-
- - -
-
-
- {#if rewindTargets.length > 0}

Rewind targets

@@ -2101,7 +2074,21 @@ workerDetailsOpen = !workerDetailsOpen; if (workerDetailsOpen) taskPaneOpen = false; }} - /> + > + {#snippet controls()} + + {/snippet} +
diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/console-display.browser.spec.ts b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/console-display.browser.spec.ts index fc951de67..125f09ff5 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/console-display.browser.spec.ts +++ b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/console-display.browser.spec.ts @@ -261,13 +261,28 @@ test.each(["live_protocol", "retained_snapshot"] as const)("overlapping history await screen.findByText("Start of conversation"); expect(view.container.querySelectorAll(".activity-summary")).toHaveLength(1); expect(screen.getAllByText("searched 1 time・ran 1 command")).toHaveLength(1); - await fireEvent.click(screen.getByRole("button", { name: "Normal" })); + const footer = screen.getByLabelText("Worker model and context"); + const overview = screen.getByRole("switch", { name: "Overview" }); + expect(screen.queryByRole("button", { name: "Normal" })).toBeNull(); + const details = screen.getByRole("button", { name: "Details" }); + expect(view.container.querySelector(".console-header")).toBeNull(); + expect(footer.contains(overview)).toBe(true); + expect(overview.nextElementSibling).toBe(details); + expect(overview.closest("form")).toBeNull(); + expect(view.container.querySelector(".console-composer")?.nextElementSibling).toBe(footer); + expect(overview.getAttribute("aria-checked")).toBe("true"); + await fireEvent.click(overview); + expect(overview.getAttribute("aria-checked")).toBe("false"); + expect(overview.textContent?.trim()).toBe("Overview"); const transcript = screen.getByRole("article", { name: "main transcript" }); const ids = [...transcript.querySelectorAll("[data-console-line-id]")].map((line) => line.getAttribute("data-console-line-id")); expect(ids).toHaveLength(3); expect(new Set(ids).size).toBe(ids.length); - await fireEvent.click(screen.getByRole("button", { name: "Overview" })); + await fireEvent.click(overview); + expect(overview.getAttribute("aria-checked")).toBe("true"); + expect(overview.textContent?.trim()).toBe("Overview"); expect(view.container.querySelectorAll(".activity-summary")).toHaveLength(1); + expect(multiplexer.sendWorkerMethod).not.toHaveBeenCalled(); }); test.each([ @@ -473,7 +488,7 @@ test("mini task summary opens and closes details without a header Tasks button", latestListener().onFrame(subscribedFrame(session)); const summary = await screen.findByRole("button", { name: /1 task — pending/ }); expect(screen.queryByRole("complementary", { name: "Worker tasks" })).toBeNull(); - expect(view.container.querySelector(".console-header")?.textContent).not.toContain("Tasks"); + expect(view.container.querySelector(".console-header")).toBeNull(); await fireEvent.click(screen.getByRole("button", { name: "Details" })); const details = await screen.findByRole("complementary", { name: "Worker detail" }); expect(details.textContent).not.toContain("Capabilities");