diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-split-cascade-resize.test.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-split-cascade-resize.test.tsx
index b5a77ce5fa..22d256c8b7 100644
--- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-split-cascade-resize.test.tsx
+++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-split-cascade-resize.test.tsx
@@ -76,6 +76,13 @@ function setWidth(element: HTMLElement, width: number) {
Object.defineProperty(element, 'getBoundingClientRect', { configurable: true, value: () => rect(width) })
}
+function setHeight(element: HTMLElement, height: number) {
+ Object.defineProperty(element, 'getBoundingClientRect', {
+ configurable: true,
+ value: () => ({ ...rect(1000), bottom: height, height })
+ })
+}
+
function row(): SplitNode {
const tree = $layoutTree.get()
@@ -180,4 +187,47 @@ describe('TreeSplit cascading expansion', () => {
expect(row().weights[0]).toBeCloseTo(2.2)
expect(row().children[3]).toMatchObject({ id: 'terminal-zone', minimized: true })
})
+ it('folding a tool zone at its floor leaves no drag preview pinned on the flex sibling', () => {
+ markCollapsePane('terminal')
+ disposers.push(
+ registry.register({
+ area: 'panes',
+ data: { height: '200px', placement: 'bottom' },
+ id: 'terminal',
+ render: () => null,
+ title: 'Terminal'
+ })
+ )
+
+ const tree = split(
+ 'column',
+ [group(['chat'], { id: 'chat-zone' }), group(['terminal'], { id: 'terminal-zone' })],
+ [1, 1],
+ 'root-column'
+ )
+
+ $layoutTree.set(tree)
+
+ render()
+
+ const container = document.querySelector('[data-tree-split="root-column"]')!
+ const [chat, terminal] = [...container.children] as HTMLElement[]
+ setHeight(container, 800)
+ setHeight(chat, 600)
+ setHeight(terminal, 200)
+ setHeight(document.querySelector('[data-tree-group="terminal-zone"]')!, 200)
+
+ const chatFlex = chat.style.flex
+ const terminalSash = document.querySelectorAll('[role="separator"]')[0]!
+ fireEvent.pointerDown(terminalSash, { button: 0, clientY: 600, pointerId: 1, pointerType: 'mouse' })
+ fireEvent.pointerMove(window, { clientY: 790, pointerId: 1, pointerType: 'mouse' })
+ fireEvent.pointerUp(window, { clientY: 790, pointerId: 1, pointerType: 'mouse' })
+
+ // The zone folds to its rail (no sliver persisted) and the chat wrapper —
+ // which the commit does not re-render — is back on React's own flex, not
+ // the `0 1 ` pin the gesture previewed.
+ expect(row().children[1]).toMatchObject({ id: 'terminal-zone', minimized: true })
+ expect($paneStates.get().terminal?.heightOverride).toBeUndefined()
+ expect(chat.style.flex).toBe(chatFlex)
+ })
})
diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-split.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-split.tsx
index f147b855ea..f51069d6ed 100644
--- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-split.tsx
+++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-split.tsx
@@ -387,10 +387,8 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo
// - every visible flex track is pinned to `0 1 `. The plan keeps
// the run's total px constant, so no leftover gap can open.
// - hidden (display:none) and minimized tracks are left untouched.
- // - cleanup: a real drag commits the store once, and React's re-render
- // rewrites the `flex` shorthand, which clears the overrides (writing
- // the shorthand resets the longhands). A no-movement click restores
- // the captured style attributes instead, since nothing re-renders.
+ // - cleanup: release restores the captured style attributes, then a
+ // real drag commits the store once and React re-renders from it.
const previewPlan = (plan: ReturnType) => {
sashTracks.forEach((track, index) => {
if (!track.visible || track.minimized) {
@@ -458,6 +456,14 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo
done = true
resize.finish()
+ // Put every wrapper's inline style back exactly as React last wrote
+ // it BEFORE the store commit. React only rewrites a wrapper whose
+ // style prop changed; a preview pinned on a track the commit leaves
+ // alone (the flex run beside a zone that folded to its rail) would
+ // otherwise survive as a stale `flex: 0 1 ` and stop it growing.
+ // A no-movement click has no commit, so this is also its whole cleanup.
+ restoreStyles()
+
if (lastPlan && lastPlan.moved !== 0) {
// Dragged a tool panel down to its collapsed header? Fold the zone
// to its rail instead of persisting a sliver — and DON'T write the
@@ -473,10 +479,6 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo
} else {
commitPlan(lastPlan)
}
- } else {
- // Click without movement: nothing will re-render, so put the
- // wrappers' inline styles back exactly as React last wrote them.
- restoreStyles()
}
// Geometry vars re-enable AFTER the final store commit above, so the