From 0cc25d7e1c915e2b4583305eeae3b934257f719b Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Fri, 4 Sep 2026 13:29:03 +0100 Subject: [PATCH] fix(maestro): equalize split widths after spawning a session splitLeaf always halved just the target pane, so repeated splits left one pane at 50% and the rest squeezed into shrinking fractions instead of equal columns. Co-Authored-By: Claude Sonnet 5 --- web/src/layout.test.ts | 18 ++++++++++++++++++ web/src/layout.ts | 11 +++++++++++ web/src/multiview.ts | 4 ++++ 3 files changed, 33 insertions(+) diff --git a/web/src/layout.test.ts b/web/src/layout.test.ts index 2ae13c8..1842669 100644 --- a/web/src/layout.test.ts +++ b/web/src/layout.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { emptyLayout, + equalizeRatios, leaves, locateSession, openSessions, @@ -44,6 +45,23 @@ describe("splitLeaf", () => { }); }); +describe("equalizeRatios", () => { + it("resets nested splits to equal width, not just the newest one", () => { + const tab = tabWithOneLeaf("s1"); + const first = leaves(tab.root)[0].id; + splitLeaf(tab, first, "s2", "row", true); // A|B, 0.5/0.5 + const second = leaves(tab.root)[1].id; + splitLeaf(tab, second, "s3", "row", true); // A(0.5) | (B|C, 0.5/0.5) -> B,C at 0.25 each + + equalizeRatios(tab.root); + + const root = tab.root as Extract; + expect(root.ratio).toBeCloseTo(1 / 3); + const inner = root.b as Extract; + expect(inner.ratio).toBeCloseTo(1 / 2); + }); +}); + describe("removeLeaf", () => { it("collapses the parent split into the surviving sibling", () => { const tab = tabWithOneLeaf("s1"); diff --git a/web/src/layout.ts b/web/src/layout.ts index 51a2ec5..d2dc8cc 100644 --- a/web/src/layout.ts +++ b/web/src/layout.ts @@ -118,6 +118,17 @@ export function removeLeaf(tab: Tab, leafId: string): void { tab.root = tab.root ? rec(tab.root) : null; } +/** Reset every split ratio under `node` so all leaves get equal width, + * regardless of split direction or nesting order. Returns the leaf count. */ +export function equalizeRatios(node: Node | null): number { + if (!node) return 0; + if (node.kind === "leaf") return 1; + const na = equalizeRatios(node.a); + const nb = equalizeRatios(node.b); + node.ratio = na / (na + nb); + return na + nb; +} + export function addTab(layout: Layout): Tab { const tab: Tab = { id: freshId("t"), diff --git a/web/src/multiview.ts b/web/src/multiview.ts index aad876e..6c837c6 100644 --- a/web/src/multiview.ts +++ b/web/src/multiview.ts @@ -10,6 +10,7 @@ import { activeTab, addTab, closeTab, + equalizeRatios, findLeaf, freshId, type Dir, @@ -457,6 +458,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { dropPreview(); const tab = activeTab(layout); tab.focusedLeafId = splitLeaf(tab, leafId, null, dir, true); + equalizeRatios(tab.root); render(); } @@ -542,6 +544,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { target.sessionId = sessionId; } else { tab.focusedLeafId = splitLeaf(tab, target.id, sessionId, "row", true); + equalizeRatios(tab.root); } render(); opts.onOpenSessionsChange?.(); @@ -815,6 +818,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { if (source.kind === "pane" && self) removeLeaf(tab, self); else if (loc) removeLeaf(loc.tab, loc.leaf.id); tab.focusedLeafId = splitLeaf(tab, targetLeafId, sessionId, dir, after); + equalizeRatios(tab.root); render(); }