From dae028337f9482fc8f59264fbe23a00aab78c0f5 Mon Sep 17 00:00:00 2001 From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:53:18 +0100 Subject: [PATCH 1/3] Fix tall chat transcript scrolling --- apps/web/src/chat/transcript.tsx | 5 +++- e2e/smoke.e2e.ts | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/apps/web/src/chat/transcript.tsx b/apps/web/src/chat/transcript.tsx index 37618a7c..39702adf 100644 --- a/apps/web/src/chat/transcript.tsx +++ b/apps/web/src/chat/transcript.tsx @@ -223,7 +223,10 @@ export function Transcript( pinned.current = distance < 40; }} > -
+
{groups.map(item => item.kind === "system" ? diff --git a/e2e/smoke.e2e.ts b/e2e/smoke.e2e.ts index ea16535d..f96785bd 100644 --- a/e2e/smoke.e2e.ts +++ b/e2e/smoke.e2e.ts @@ -294,6 +294,57 @@ test("chat uses one room-message composer when the planner is off", async ({ joi await expect(chat.getByRole("button", { name: "Stop Planner" })).toHaveCount(0); }); +test("chat keeps the start of a tall transcript reachable across layouts", async ({ join, page }) => { + await injectChatHistory(page, frame => ({ + ...frame, + entries: Array.from({ length: 24 }, (_, index) => ({ + author: index % 2 === 0 + ? { kind: "member" as const, handle: "ana" } + : { kind: "agent" as const }, + id: `history-${index}`, + text: index === 0 + ? "The first message in a deliberately tall transcript." + : `Transcript message ${index} with enough text to occupy a complete line.`, + ts: 1_700_000_000 + index, + })), + })); + + let chat = chatPane(await join("ana")); + let stack = chat.locator("[data-chat-stack]"); + let scroller = stack.locator(".."); + let firstMessagePosition = async () => { + await scroller.evaluate(element => element.scrollTop = 0); + return chat.getByText("The first message in a deliberately tall transcript.") + .evaluate((message, scrollRoot) => { + let messageBox = message.getBoundingClientRect(); + let scroller = scrollRoot as Element; + let scrollerBox = scroller.getBoundingClientRect(); + return { + clientHeight: scroller.clientHeight, + messageBottom: messageBox.bottom, + messageTop: messageBox.top, + scrollerBottom: scrollerBox.bottom, + scrollerTop: scrollerBox.top, + scrollHeight: scroller.scrollHeight, + }; + }, await scroller.elementHandle()); + }; + await expect(chat.getByText("Transcript message 23", { exact: false })).toBeVisible(); + let position = await firstMessagePosition(); + expect(position.scrollHeight).toBeGreaterThan(position.clientHeight); + expect(position.messageTop).toBeGreaterThanOrEqual(position.scrollerTop); + expect(position.messageBottom).toBeLessThanOrEqual(position.scrollerBottom); + + await page.setViewportSize({ width: 390, height: 844 }); + await page.getByRole("navigation", { name: "Workspace view" }) + .getByRole("button", { name: /^Chat/ }).click(); + await expect(chat).toBeVisible(); + position = await firstMessagePosition(); + expect(position.scrollHeight).toBeGreaterThan(position.clientHeight); + expect(position.messageTop).toBeGreaterThanOrEqual(position.scrollerTop); + expect(position.messageBottom).toBeLessThanOrEqual(position.scrollerBottom); +}); + test("chat disables Send when its socket disconnects", async ({ join, page }) => { let sockets: WebSocketRoute[] = []; await page.routeWebSocket("**/ws?**", route => { From caf00682486106d922ab98ea59a8a67b267a5e35 Mon Sep 17 00:00:00 2001 From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:18:41 +0100 Subject: [PATCH 2/3] Document chat composer spacing design --- ...2026-08-28-chat-composer-spacing-design.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-28-chat-composer-spacing-design.md diff --git a/docs/superpowers/specs/2026-08-28-chat-composer-spacing-design.md b/docs/superpowers/specs/2026-08-28-chat-composer-spacing-design.md new file mode 100644 index 00000000..bc5537be --- /dev/null +++ b/docs/superpowers/specs/2026-08-28-chat-composer-spacing-design.md @@ -0,0 +1,20 @@ +# Chat composer spacing + +## Goal + +Keep at least 32 pixels between the final Chat message and the top of the composer field on +desktop and compact layouts. + +## Design + +The transcript already leaves a 16-pixel flex gap before its bottom scroll marker. Give that +marker another 16 pixels of top margin. When Chat follows the marker to the bottom, the final +message will sit 32 pixels above the transcript edge and composer. + +This keeps the existing 16-pixel spacing between messages and does not change the composer's +height, padding, or reference-picker positioning. + +## Verification + +Extend the tall-transcript Playwright test to measure the rendered distance between the final +message and composer field. Require at least 32 pixels in desktop and compact layouts. From 1796a25a66fcfe9e7921753b08aad6504c06c980 Mon Sep 17 00:00:00 2001 From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:25:32 +0100 Subject: [PATCH 3/3] Add chat composer breathing room --- apps/web/src/chat/transcript.tsx | 2 +- .../2026-08-28-chat-composer-spacing-design.md | 6 +++--- e2e/smoke.e2e.ts | 14 +++++++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/web/src/chat/transcript.tsx b/apps/web/src/chat/transcript.tsx index 39702adf..d065c5a8 100644 --- a/apps/web/src/chat/transcript.tsx +++ b/apps/web/src/chat/transcript.tsx @@ -239,7 +239,7 @@ export function Transcript( /> ) )} -
+
); diff --git a/docs/superpowers/specs/2026-08-28-chat-composer-spacing-design.md b/docs/superpowers/specs/2026-08-28-chat-composer-spacing-design.md index bc5537be..890f8810 100644 --- a/docs/superpowers/specs/2026-08-28-chat-composer-spacing-design.md +++ b/docs/superpowers/specs/2026-08-28-chat-composer-spacing-design.md @@ -7,9 +7,9 @@ desktop and compact layouts. ## Design -The transcript already leaves a 16-pixel flex gap before its bottom scroll marker. Give that -marker another 16 pixels of top margin. When Chat follows the marker to the bottom, the final -message will sit 32 pixels above the transcript edge and composer. +The transcript leaves a 16-pixel flex gap before its bottom scroll marker. Make that marker a +16-pixel-high spacer. When Chat follows the marker to the bottom, the final message will sit 32 +pixels above the composer. This keeps the existing 16-pixel spacing between messages and does not change the composer's height, padding, or reference-picker positioning. diff --git a/e2e/smoke.e2e.ts b/e2e/smoke.e2e.ts index f96785bd..34cf857c 100644 --- a/e2e/smoke.e2e.ts +++ b/e2e/smoke.e2e.ts @@ -294,7 +294,7 @@ test("chat uses one room-message composer when the planner is off", async ({ joi await expect(chat.getByRole("button", { name: "Stop Planner" })).toHaveCount(0); }); -test("chat keeps the start of a tall transcript reachable across layouts", async ({ join, page }) => { +test("chat keeps both ends of a tall transcript clear across layouts", async ({ join, page }) => { await injectChatHistory(page, frame => ({ ...frame, entries: Array.from({ length: 24 }, (_, index) => ({ @@ -312,6 +312,16 @@ test("chat keeps the start of a tall transcript reachable across layouts", async let chat = chatPane(await join("ana")); let stack = chat.locator("[data-chat-stack]"); let scroller = stack.locator(".."); + let composer = chat.getByPlaceholder("Use @chopin to ask Chopin").locator(".."); + let lastMessageGap = async () => { + await scroller.evaluate(element => element.scrollTop = element.scrollHeight); + return chat.getByText("Transcript message 23", { exact: false }) + .evaluate( + (message, field) => + (field as Element).getBoundingClientRect().top - message.getBoundingClientRect().bottom, + await composer.elementHandle(), + ); + }; let firstMessagePosition = async () => { await scroller.evaluate(element => element.scrollTop = 0); return chat.getByText("The first message in a deliberately tall transcript.") @@ -330,6 +340,7 @@ test("chat keeps the start of a tall transcript reachable across layouts", async }, await scroller.elementHandle()); }; await expect(chat.getByText("Transcript message 23", { exact: false })).toBeVisible(); + expect(await lastMessageGap()).toBeGreaterThanOrEqual(32); let position = await firstMessagePosition(); expect(position.scrollHeight).toBeGreaterThan(position.clientHeight); expect(position.messageTop).toBeGreaterThanOrEqual(position.scrollerTop); @@ -339,6 +350,7 @@ test("chat keeps the start of a tall transcript reachable across layouts", async await page.getByRole("navigation", { name: "Workspace view" }) .getByRole("button", { name: /^Chat/ }).click(); await expect(chat).toBeVisible(); + expect(await lastMessageGap()).toBeGreaterThanOrEqual(32); position = await firstMessagePosition(); expect(position.scrollHeight).toBeGreaterThan(position.clientHeight); expect(position.messageTop).toBeGreaterThanOrEqual(position.scrollerTop);