From 4c54aa2556c7c018da2fbadee1938bb13c3d480e Mon Sep 17 00:00:00 2001 From: elb-comt Date: Fri, 11 Sep 2026 18:56:16 +0800 Subject: [PATCH 1/2] Fix split-drag drop target for a single pane A single chat pane was not tagged with data-split-pane-id, so beginSplitDrag fell back to the whole
element for its drop rect. In the inline single-pane layout
also contains the right secondary panel, so the drop zones were measured over chat + panel: side drops were unreliable and a center drop landed in the split band whenever the right panel was open. Tag the inline pane content (the chat column, which excludes the panel) with data-split-pane-id so drops are measured against the pane that owns them. Split/replace behavior is now independent of the right panel. --- .../src/components/secondary-panel/SecondaryPanelLayout.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/app/src/components/secondary-panel/SecondaryPanelLayout.tsx b/apps/app/src/components/secondary-panel/SecondaryPanelLayout.tsx index bafad99c04c..1f23136689b 100644 --- a/apps/app/src/components/secondary-panel/SecondaryPanelLayout.tsx +++ b/apps/app/src/components/secondary-panel/SecondaryPanelLayout.tsx @@ -92,6 +92,8 @@ export function SecondaryPanelLayout({ }: SecondaryPanelLayoutProps) { const paneContext = useOptionalPaneContext(); const secondaryPanelHost = paneContext?.secondaryPanelHost ?? null; + const inlinePaneTargetId = + secondaryPanelHost === null ? paneContext?.paneId : undefined; const renderAsDrawer = useIsCompactViewport(); const sidebarDrawerShowing = useSyncExternalStore( subscribeCompactSidebarDrawerShowing, @@ -297,6 +299,7 @@ export function SecondaryPanelLayout({ const mainContent = (
Date: Fri, 11 Sep 2026 21:00:51 +0800 Subject: [PATCH 2/2] Split instead of replace when dropping onto a single pane With one visible chat, the center of the pane is a replace zone, so dragging a thread in from the sidebar replaced the open chat instead of adding a pane. Replace is also reachable by clicking the thread, so make a center drop create a split to the right when the workspace has one pane. Edge drops keep choosing their own side. --- .../src/components/sidebar/usePaneContentSplitDrag.ts | 1 + .../src/components/sidebar/useThreadRowSplitDrag.ts | 1 + apps/app/src/lib/split-drag/zones.test.ts | 11 +++++++++++ apps/app/src/lib/split-drag/zones.ts | 10 ++++++++-- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/app/src/components/sidebar/usePaneContentSplitDrag.ts b/apps/app/src/components/sidebar/usePaneContentSplitDrag.ts index 8130f6fa77d..7bebb77b2ec 100644 --- a/apps/app/src/components/sidebar/usePaneContentSplitDrag.ts +++ b/apps/app/src/components/sidebar/usePaneContentSplitDrag.ts @@ -127,6 +127,7 @@ export function usePaneContentSplitActions() { zone, threadAlreadyOpen: findPaneByContent(layout.root, content) !== null, atMaxPanes: countPanes(layout.root) >= MAX_PANES, + singlePane: countPanes(layout.root) === 1, }); }, onDrop: (target) => { diff --git a/apps/app/src/components/sidebar/useThreadRowSplitDrag.ts b/apps/app/src/components/sidebar/useThreadRowSplitDrag.ts index e1531f7277e..b4f5c4aad35 100644 --- a/apps/app/src/components/sidebar/useThreadRowSplitDrag.ts +++ b/apps/app/src/components/sidebar/useThreadRowSplitDrag.ts @@ -82,6 +82,7 @@ export function useThreadRowSplitDrag({ threadAlreadyOpen: findPaneByThread(layout.root, projectId, threadId) !== null, atMaxPanes: countPanes(layout.root) >= MAX_PANES, + singlePane: countPanes(layout.root) === 1, }); }, onDrop: (target) => { diff --git a/apps/app/src/lib/split-drag/zones.test.ts b/apps/app/src/lib/split-drag/zones.test.ts index 30e91a12eb4..cac2e37e706 100644 --- a/apps/app/src/lib/split-drag/zones.test.ts +++ b/apps/app/src/lib/split-drag/zones.test.ts @@ -89,6 +89,17 @@ describe("decideThreadDrop", () => { ).toEqual({ zone: "center", label: "Replace this chat" }); }); + it("splits a center zone when the workspace has a single pane", () => { + expect( + decideThreadDrop({ + zone: "center", + threadAlreadyOpen: false, + atMaxPanes: false, + singlePane: true, + }), + ).toEqual({ zone: "right", label: "Split right" }); + }); + it("coerces edges to center-replace at the pane cap", () => { expect( decideThreadDrop({ diff --git a/apps/app/src/lib/split-drag/zones.ts b/apps/app/src/lib/split-drag/zones.ts index 19016ea827a..77c3c7c2504 100644 --- a/apps/app/src/lib/split-drag/zones.ts +++ b/apps/app/src/lib/split-drag/zones.ts @@ -85,12 +85,14 @@ interface ThreadDropInput { zone: SplitZone; threadAlreadyOpen: boolean; atMaxPanes: boolean; + singlePane?: boolean; } export function decideThreadDrop({ zone, threadAlreadyOpen, atMaxPanes, + singlePane = false, }: ThreadDropInput): ZoneDecision { if (threadAlreadyOpen) { return { zone: "center", label: "Already open — focus pane" }; @@ -98,9 +100,13 @@ export function decideThreadDrop({ if (atMaxPanes) { return { zone: "center", label: "Replace this chat" }; } + const resolvedZone = singlePane && zone === "center" ? "right" : zone; return { - zone, - label: zone === "center" ? "Replace this chat" : `Split ${zone}`, + zone: resolvedZone, + label: + resolvedZone === "center" + ? "Replace this chat" + : `Split ${resolvedZone}`, }; }