From b2190237f8da2b078fc4dc40356be226db6e97aa Mon Sep 17 00:00:00 2001 From: Alex Lavaee Date: Thu, 1 Oct 2026 00:57:58 -0700 Subject: [PATCH 1/2] Keep the chat resize handle under the pointer The chat rail sits on the right of the workspace and is resized from its left edge, but resizeDelta treated a left-edge handle as if moving it right widened the pane. Dragging or pressing ArrowRight widened the chat rail and dragging left narrowed it. A pane resized from its left edge now narrows when the edge moves right, and arrow keys move the separator in the arrow's direction. The unit tests and the shell e2e test pinned the inverted mapping; they now assert the pointer-following direction for both pointer and keyboard input, and a new e2e test drags the chat rail's edge both ways. Fixes #214 Assistant-model: Claude Opus 5.5 Assistant-workflow: inline Assistant-duration: 4m converged, estimated 15m Assistant-verification: bun test unit passed: apps/web/src/resizable-pane.test.ts (5 pass) Assistant-verification: Playwright Chromium E2E passed: bun run e2e e2e/shell.e2e.ts (11 passed) Assistant-verification: Playwright Chromium E2E regression check passed: both chat-rail resize tests fail against the old resizeDelta Assistant-verification: typecheck and lint passed: bun run types, bun run ci Co-authored-by: Alex Lavaee --- apps/web/src/resizable-pane.test.ts | 14 +++++++++----- apps/web/src/resizable-pane.tsx | 7 +++++-- e2e/shell.e2e.ts | 28 ++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/apps/web/src/resizable-pane.test.ts b/apps/web/src/resizable-pane.test.ts index 28835a5f..f00e98a2 100644 --- a/apps/web/src/resizable-pane.test.ts +++ b/apps/web/src/resizable-pane.test.ts @@ -16,9 +16,11 @@ describe("bounded pane resizing", () => { expect(clampPane(401, 250, 400)).toBe(400); }); - it("moves each pane edge in its screen direction", () => { - expect(resizeDelta("left", 16)).toBe(16); - expect(resizeDelta("right", 16)).toBe(-16); + it("keeps each pane edge under the pointer", () => { + expect(resizeDelta("left", -16)).toBe(16); + expect(resizeDelta("left", 16)).toBe(-16); + expect(resizeDelta("right", 16)).toBe(16); + expect(resizeDelta("right", -16)).toBe(-16); }); it("restores a valid saved width and rejects invalid persisted values", () => { @@ -58,8 +60,10 @@ describe("bounded pane resizing", () => { it("maps keyboard commands to bounded pane movements", () => { let bounds = { min: 304, max: 400 }; - expect(keyboardPaneDelta("left", "ArrowRight", false, 320, bounds)).toBe(16); - expect(keyboardPaneDelta("right", "ArrowRight", true, 384, bounds)).toBe(-64); + expect(keyboardPaneDelta("left", "ArrowLeft", false, 320, bounds)).toBe(16); + expect(keyboardPaneDelta("left", "ArrowRight", true, 384, bounds)).toBe(-64); + expect(keyboardPaneDelta("right", "ArrowRight", false, 320, bounds)).toBe(16); + expect(keyboardPaneDelta("right", "ArrowLeft", true, 384, bounds)).toBe(-64); expect(keyboardPaneDelta("left", "Home", false, 360, bounds)).toBe(-56); expect(keyboardPaneDelta("right", "End", false, 360, bounds)).toBe(40); expect(keyboardPaneDelta("left", "Tab", false, 360, bounds)).toBeUndefined(); diff --git a/apps/web/src/resizable-pane.tsx b/apps/web/src/resizable-pane.tsx index 16af9985..76b12020 100644 --- a/apps/web/src/resizable-pane.tsx +++ b/apps/web/src/resizable-pane.tsx @@ -15,9 +15,12 @@ export function clampPane(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } -/** Translates an on-screen drag into the width change for the pane's edge. */ +/** + * Translates an on-screen drag into a width change for a pane resized from its `side` edge. + * Moving a left edge rightward narrows the pane; moving a right edge rightward widens it. + */ export function resizeDelta(side: PaneSide, delta: number): number { - return side === "left" ? delta : -delta; + return side === "left" ? -delta : delta; } export function restorePaneWidth(stored: string | null, { initial, min, max }: PaneBounds): number { diff --git a/e2e/shell.e2e.ts b/e2e/shell.e2e.ts index b544dc79..9011fff6 100644 --- a/e2e/shell.e2e.ts +++ b/e2e/shell.e2e.ts @@ -77,11 +77,15 @@ test("the chat rail has its own resize control", async ({ join, page }) => { let beforeValue = Number(await handle.getAttribute("aria-valuenow")); let before = (await box(rail)).width; - await handle.press("ArrowRight"); - await handle.press("ArrowRight"); + await handle.press("ArrowLeft"); + await handle.press("ArrowLeft"); expect((await box(rail)).width).toBeGreaterThan(before); expect(Number(await handle.getAttribute("aria-valuenow"))).toBeGreaterThan(beforeValue); + let widened = (await box(rail)).width; + await handle.press("ArrowRight"); + expect((await box(rail)).width).toBeLessThan(widened); + await handle.press("End"); let maximum = await handle.getAttribute("aria-valuemax"); if (maximum === null) throw new Error("Chat resize handle must expose aria-valuemax"); @@ -91,6 +95,26 @@ test("the chat rail has its own resize control", async ({ join, page }) => { ); }); +test("the chat rail edge follows the pointer", async ({ join, page }) => { + await join("ana"); + + let rail = page.getByRole("complementary", { name: "Chat" }); + let handle = page.getByRole("separator", { name: "Resize chat" }); + let start = await box(handle); + let y = start.y + start.height / 2; + let before = (await box(rail)).width; + + await handle.hover(); + await page.mouse.down(); + await page.mouse.move(start.x - 40, y, { steps: 4 }); + let widened = (await box(rail)).width; + expect(widened).toBeGreaterThan(before); + + await page.mouse.move(start.x + 20, y, { steps: 4 }); + await page.mouse.up(); + expect((await box(rail)).width).toBeLessThan(widened); +}); + test("the compact workspace keeps the document unobstructed", async ({ join, page }) => { await page.setViewportSize({ width: 640, height: 800 }); await join("ana"); From f4bb57a61fe0a02791ac76cb03db22f74518eb40 Mon Sep 17 00:00:00 2001 From: Alex Lavaee Date: Thu, 1 Oct 2026 01:14:20 -0700 Subject: [PATCH 2/2] Open resolved history before checking the saved custom option Once the last open decision is saved it moves into the collapsed resolved history, so the final assertion only passed when it ran before the card left the open list. The test now waits for "2 resolved", opens the history, and checks the saved card there. Assistant-model: Claude Opus 5.5 Assistant-workflow: inline Assistant-duration: 9m converged, estimated 15m Assistant-verification: Playwright Chromium E2E passed: sidecar "progressive custom answers" --repeat-each 10 (10 passed; before the fix 2 of 3 failed locally and all 3 CI attempts failed) Assistant-verification: Playwright Chromium E2E passed: bun run e2e e2e/sidecar.e2e.ts e2e/shell.e2e.ts (52 passed) Assistant-verification: typecheck and lint passed: bun run types, bun run ci Co-authored-by: Alex Lavaee --- e2e/sidecar.e2e.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/e2e/sidecar.e2e.ts b/e2e/sidecar.e2e.ts index 141e0518..06fd1116 100644 --- a/e2e/sidecar.e2e.ts +++ b/e2e/sidecar.e2e.ts @@ -669,8 +669,14 @@ test("decision cards save independently with progressive custom answers", async await added.check(); await scope.getByRole("checkbox", { name: /^Anchors/ }).check(); await saveScope.click(); - await expect(questionnaire(page).filter({ hasText: "Which of these belong in the first cut?" })) - .toContainText("Only collaborative anchors"); + let allResolved = page.getByRole("button", { name: "2 resolved" }); + await expect(allResolved).toBeVisible(); + await allResolved.click(); + let resolvedScope = questionnaire(page).filter({ + hasText: "Which of these belong in the first cut?", + }); + await expect(resolvedScope).toContainText("Only collaborative anchors"); + await expect(resolvedScope).toContainText("Answered by @ana"); }); test("Save and Next wait for a chosen answer", async ({ join, seed }) => {