From 6eee6a6b3ac236f044393bc4723130e19e6be95a Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:30:53 +0200 Subject: [PATCH] fix(app): let every split divider drag after panes are added to a resized row Divider drags store sizes per group id, apart from the layout tree. A group keeps its id when panes are added, so after resizing a row and splitting it again the stored list had fewer entries than panes, and dividers past its end could not move. Use the dragged sizes only while they have one entry per pane. Closes #5893 --- .../app/e2e/browser/split-pane-resize.spec.ts | 88 +++++++++++++++++++ .../app/src/components/split-container.tsx | 20 ++++- 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/app/e2e/browser/split-pane-resize.spec.ts diff --git a/packages/app/e2e/browser/split-pane-resize.spec.ts b/packages/app/e2e/browser/split-pane-resize.spec.ts new file mode 100644 index 0000000000..75414ee321 --- /dev/null +++ b/packages/app/e2e/browser/split-pane-resize.spec.ts @@ -0,0 +1,88 @@ +import { expect, type Locator, type Page } from "@playwright/test"; +import { test } from "../support/fixtures"; +import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions"; +import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; + +const DRAG_DISTANCE = -40; + +function splitDividers(page: Page): Locator { + return page.getByTestId("workspace-split-resize-handle").filter({ visible: true }); +} + +async function dividerX(divider: Locator): Promise { + const box = await divider.boundingBox(); + if (!box) throw new Error("Divider has no bounding box"); + return box.x; +} + +async function dragDivider(page: Page, divider: Locator, deltaX: number): Promise { + const box = await divider.boundingBox(); + if (!box) throw new Error("Divider has no bounding box"); + const x = box.x + box.width / 2; + const y = box.y + box.height / 2; + await page.mouse.move(x, y); + await page.mouse.down(); + await page.mouse.move(x + deltaX / 2, y, { steps: 4 }); + await page.mouse.move(x + deltaX, y, { steps: 4 }); + await page.mouse.up(); +} + +async function expectDividerFollowsDrag(page: Page, index: number): Promise { + const divider = splitDividers(page).nth(index); + const before = await dividerX(divider); + await dragDivider(page, divider, DRAG_DISTANCE); + await expect + .poll(async () => (await dividerX(divider)) - before, { + message: `divider ${index + 1} should follow the drag`, + }) + .toBeLessThan(DRAG_DISTANCE * 0.75); +} + +async function splitRight(page: Page, expectedDividers: number): Promise { + await runWorkspaceActionFromCommandCenter(page, "Split pane right"); + await expect(splitDividers(page)).toHaveCount(expectedDividers); +} + +test.describe("Split pane resize", () => { + test("every divider drags in a four-pane row", async ({ page }) => { + await page.setViewportSize({ width: 1600, height: 900 }); + const workspace = await seedWorkspace({ repoPrefix: "split-resize-fresh-" }); + try { + await gotoWorkspace(page, workspace.workspaceId); + await waitForWorkspaceTabsVisible(page); + await clickNewChat(page); + await splitRight(page, 1); + await splitRight(page, 2); + await splitRight(page, 3); + + for (const index of [0, 1, 2]) { + await expectDividerFollowsDrag(page, index); + } + } finally { + await workspace.cleanup(); + } + }); + + test("every divider drags after panes are added to a resized row", async ({ page }) => { + await page.setViewportSize({ width: 1600, height: 900 }); + const workspace = await seedWorkspace({ repoPrefix: "split-resize-grown-" }); + try { + await gotoWorkspace(page, workspace.workspaceId); + await waitForWorkspaceTabsVisible(page); + await clickNewChat(page); + await splitRight(page, 1); + await expectDividerFollowsDrag(page, 0); + + await splitRight(page, 2); + await splitRight(page, 3); + + for (const index of [0, 1, 2]) { + await expectDividerFollowsDrag(page, index); + } + } finally { + await workspace.cleanup(); + } + }); +}); diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 9705f3be15..7f627a8c72 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -872,6 +872,19 @@ function SplitGroupChild({ ); } +/** + * Sizes from a divider drag are kept per group id, apart from the layout tree. A group keeps its + * id when panes are added or removed, so the dragged sizes only apply while they still have one + * entry per child; otherwise the dividers past the end of the list could not move. + */ +function resolveGroupSizes( + layoutSizes: number[], + draggedSizes: number[] | undefined, + childCount: number, +): number[] { + return draggedSizes?.length === childCount ? draggedSizes : layoutSizes; +} + /** * Flex grow per child, renormalized so the visible ones always sum to 1. * @@ -968,8 +981,11 @@ function SplitNodeView({ groupId ? state.splitSizesByWorkspace[workspaceKey]?.[groupId] : undefined, ); const groupChildren = node.kind === "group" ? node.group.children : EMPTY_SPLIT_NODES; - const groupSizes = - storedGroupSizes ?? (node.kind === "group" ? node.group.sizes : EMPTY_SPLIT_SIZES); + const groupSizes = resolveGroupSizes( + node.kind === "group" ? node.group.sizes : EMPTY_SPLIT_SIZES, + storedGroupSizes, + groupChildren.length, + ); const visibleFlex = useMemo( () => resolveVisibleGroupFlex(groupChildren, groupSizes, maximizedPaneId), [groupChildren, groupSizes, maximizedPaneId],