From 1aea6c65c7398000e06f1e0b1807868232f105e6 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 2 Oct 2026 22:59:04 -0700 Subject: [PATCH 1/2] fix(web): the composer never scrolls horizontally and sidebar rows go full-width (CL-9785) --- apps/web/src/chat/composer.css | 10 +++++++--- apps/web/src/shell/chrome.css | 3 +++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/web/src/chat/composer.css b/apps/web/src/chat/composer.css index ace34ac02..1f71abdb4 100644 --- a/apps/web/src/chat/composer.css +++ b/apps/web/src/chat/composer.css @@ -2,8 +2,8 @@ .chat-composer-box { display: flex; align-items: flex-end; - gap: 4px; - padding: 6px; + gap: 8px; + padding: 8px; background: var(--surface); border-radius: 22px; box-shadow: var(--floating); @@ -21,13 +21,17 @@ max-height: 180px; display: block; resize: none; + overflow-x: hidden; + overflow-y: auto; border: 0; background: transparent; - padding: 5px 4px; + padding: 6px 8px; font: inherit; font-size: 15px; line-height: 1.5; color: var(--ink); + white-space: pre-wrap; + overflow-wrap: anywhere; } .chat-composer-input:focus { outline: none; diff --git a/apps/web/src/shell/chrome.css b/apps/web/src/shell/chrome.css index 3a12ced5b..bef1b4c83 100644 --- a/apps/web/src/shell/chrome.css +++ b/apps/web/src/shell/chrome.css @@ -72,9 +72,12 @@ display: flex; align-items: center; gap: 10px; + width: 100%; + box-sizing: border-box; min-height: 32px; padding: 5px 8px; border-radius: var(--r-md); + text-align: left; } .shell-sidebar .shell-ch-row:hover { From c103010012e39151f2599cfc0a4d776711704c71 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sat, 3 Oct 2026 09:18:50 -0700 Subject: [PATCH 2/2] test(e2e): prove composer never scrolls and sidebar rows go full-width (CL-9785) --- e2e/browser/composer-layout.test.ts | 107 ++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 e2e/browser/composer-layout.test.ts diff --git a/e2e/browser/composer-layout.test.ts b/e2e/browser/composer-layout.test.ts new file mode 100644 index 000000000..9af68d98e --- /dev/null +++ b/e2e/browser/composer-layout.test.ts @@ -0,0 +1,107 @@ +// CL-9785: the composer never scrolls horizontally (long unbroken tokens +// wrap) and sidebar rows go full-width with their radius intact. +import { expect, test } from "bun:test"; +import { bootBrowserApp, browserGate } from "../lib/browser"; +import { clickText, STEP_TIMEOUT, waitForText } from "../lib/first-run"; + +const describeBrowser = browserGate(import.meta.path); + +const VIEWPORT = { width: 1400, height: 900 }; +// One unbroken token wider than any composer: without wrapping it would force +// a horizontal scrollbar. +const LONG_TOKEN = "a".repeat(400); + +type RowBox = { + active: string | null; + offsetWidth: number; + containerWidth: number; + radius: string; +}; + +const ROW_BOX_SCRIPT = `(() => { + const out = []; + for (const row of document.querySelectorAll(".shell-ch-row")) { + const parent = row.parentElement; + out.push({ + active: row.getAttribute("aria-current"), + offsetWidth: row.offsetWidth, + containerWidth: parent ? parent.clientWidth : -1, + radius: getComputedStyle(row).borderTopLeftRadius, + }); + } + return out; +})()`; + +describeBrowser("composer layout", () => { + const app = bootBrowserApp(); + + test("long tokens wrap and sidebar rows go full-width", async () => { + const { page, errors } = await app().newPage(); + try { + await page.setViewport(VIEWPORT); + await page.goto(app().origin, { waitUntil: "networkidle0" }); + await clickText(page, "button", "Create an account"); + await waitForText(page, "Create your account"); + await page.type("input[type=email]", `alice+${String(Date.now())}@example.com`); + await page.type("input[type=password]", "correct-horse-battery-staple"); + await page.click("button[type=submit]"); + + // A placeholder key is enough: connecting stores a credential without + // calling the provider, and creating a bench needs no inference. + await waitForText(page, "Connect a brain"); + await clickText(page, "label", "Anthropic"); + await page.waitForSelector("input[type=password]"); + await page.type("input[type=password]", "sk-ant-placeholder"); + await clickText(page, "button", "Connect"); + + // The new-workbench prompt is a bare composer: a long unbroken token + // must wrap (no horizontal overflow) instead of scrolling. + await page.waitForSelector(".chat-composer-input", { timeout: STEP_TIMEOUT }); + await page.type(".chat-composer-input", LONG_TOKEN); + await page.waitForFunction( + `(() => { const el = document.querySelector(".chat-composer-input"); return el !== null && el.scrollWidth <= el.clientWidth + 1; })()`, + { timeout: STEP_TIMEOUT }, + ); + const overflowX = (await page.evaluate( + `getComputedStyle(document.querySelector(".chat-composer-input")).overflowX`, + )) as string; + expect(overflowX).toBe("hidden"); + + // Create the bench; its sidebar row is populated and active. + await page.goto(`${app().origin}/new`, { waitUntil: "networkidle0" }); + await page.waitForSelector(".chat-composer-input", { timeout: STEP_TIMEOUT }); + await page.type(".chat-composer-input", "Summarize what shipped this week"); + await page.click("button[aria-label='Start this workbench']"); + await page.waitForFunction(`location.pathname.startsWith("/w/")`, { + timeout: STEP_TIMEOUT, + }); + await page.waitForSelector(".shell-ch-row", { timeout: STEP_TIMEOUT }); + + const rows = (await page.evaluate(ROW_BOX_SCRIPT)) as RowBox[]; + expect(rows.length).toBeGreaterThan(0); + for (const row of rows) { + expect(Math.abs(row.offsetWidth - row.containerWidth)).toBeLessThanOrEqual(1); + expect(row.radius).not.toBe("0px"); + } + expect(rows.some((row) => row.active === "true")).toBe(true); + + // Full-width did not smush the radius: it is stable under hover. + const hovered = (await page.evaluate( + `(() => { const row = document.querySelector(".shell-ch-row"); return row === null ? null : getComputedStyle(row).borderTopLeftRadius; })()`, + )) as string | null; + await page.hover(".shell-ch-row"); + const hoverRadius = (await page.evaluate( + `(() => { const row = document.querySelector(".shell-ch-row:hover"); return row === null ? null : getComputedStyle(row).borderTopLeftRadius; })()`, + )) as string | null; + expect(hovered).not.toBeNull(); + expect(hoverRadius).toBe(hovered); + } catch (cause) { + const body = await page.evaluate("document.body.innerText"); + process.stderr.write( + `composer-layout failed at ${page.url()}:\n${String(body)}\nconsole: ${errors.join(" | ")}\n`, + ); + throw cause; + } + await page.close(); + }, 240_000); +});