diff --git a/apps/array/src/renderer/features/panels/hooks/useDragDropHandlers.ts b/apps/array/src/renderer/features/panels/hooks/useDragDropHandlers.ts index c60bf7c3..a0b57ca0 100644 --- a/apps/array/src/renderer/features/panels/hooks/useDragDropHandlers.ts +++ b/apps/array/src/renderer/features/panels/hooks/useDragDropHandlers.ts @@ -67,32 +67,50 @@ export const useDragDropHandlers = (taskId: string) => { const sourceData = event.operation.source?.data; const targetData = event.operation.target?.data; - // Tab reordering within same panel is handled by onDragOver - // Here we only handle cross-panel moves and splits - - // Handle panel splitting/moving if ( sourceData?.type !== "tab" || - targetData?.type !== "panel" || !sourceData.tabId || - !sourceData.panelId || - !targetData.panelId || - !targetData.zone + !sourceData.panelId ) { return; } const { tabId, panelId: sourcePanelId } = sourceData; - const { panelId: targetPanelId, zone } = targetData; - - if (zone === "center") { - moveTab(taskId, tabId, sourcePanelId, targetPanelId); - setFocusedPanel(taskId, targetPanelId); - } else if (isSplitDirection(zone)) { - splitPanel(taskId, tabId, sourcePanelId, targetPanelId, zone); - // For splits, the new panel gets a generated ID, so we can't easily focus it here - // The target panel remains focused which is reasonable behavior - setFocusedPanel(taskId, targetPanelId); + + // Handle drop on panel drop zones (center or split directions) + if (targetData?.type === "panel" && targetData.panelId && targetData.zone) { + const { panelId: targetPanelId, zone } = targetData; + + if (zone === "center") { + moveTab(taskId, tabId, sourcePanelId, targetPanelId); + setFocusedPanel(taskId, targetPanelId); + } else if (isSplitDirection(zone)) { + splitPanel(taskId, tabId, sourcePanelId, targetPanelId, zone); + setFocusedPanel(taskId, targetPanelId); + } + return; + } + + // Handle drop on tab bar (cross-panel move) + if ( + targetData?.type === "tab-bar" && + targetData.panelId && + targetData.panelId !== sourcePanelId + ) { + moveTab(taskId, tabId, sourcePanelId, targetData.panelId); + setFocusedPanel(taskId, targetData.panelId); + return; + } + + // Handle drop on another tab in a different panel (cross-panel move) + if ( + targetData?.type === "tab" && + targetData.panelId && + targetData.panelId !== sourcePanelId + ) { + moveTab(taskId, tabId, sourcePanelId, targetData.panelId); + setFocusedPanel(taskId, targetData.panelId); + return; } }; diff --git a/apps/array/src/renderer/features/panels/store/panelLayoutStore.test.ts b/apps/array/src/renderer/features/panels/store/panelLayoutStore.test.ts index 153baa57..5d748c08 100644 --- a/apps/array/src/renderer/features/panels/store/panelLayoutStore.test.ts +++ b/apps/array/src/renderer/features/panels/store/panelLayoutStore.test.ts @@ -512,6 +512,53 @@ describe("panelLayoutStore", () => { const updatedMainPanel = getNestedPanel("task-1", 0); expect(updatedMainPanel.type).toBe("leaf"); }); + + it("removes split pane when moving its only tab to another pane", () => { + // Create a split: main-panel becomes a group with [original, new-panel] + usePanelLayoutStore + .getState() + .splitPanel( + "task-1", + "file-src/App.tsx", + "main-panel", + "main-panel", + "right", + ); + + // Verify we have a split with 2 panels + const mainPanelNode = getNestedPanel("task-1", 0); + expect(mainPanelNode.type).toBe("group"); + if (mainPanelNode.type !== "group") return; + expect(mainPanelNode.children).toHaveLength(2); + + // Get the new panel (contains file-src/App.tsx) + const newPanel = mainPanelNode.children[1]; + expect(newPanel.type).toBe("leaf"); + if (newPanel.type !== "leaf") return; + + // Original panel (contains logs and file-src/Other.tsx) + const originalPanel = mainPanelNode.children[0]; + expect(originalPanel.type).toBe("leaf"); + if (originalPanel.type !== "leaf") return; + + // Move the tab from the new panel back to the original panel + usePanelLayoutStore + .getState() + .moveTab("task-1", "file-src/App.tsx", newPanel.id, originalPanel.id); + + // The split should be collapsed since new panel is now empty + const updatedMainPanel = getNestedPanel("task-1", 0); + expect(updatedMainPanel.type).toBe("leaf"); + + // The tab should now be in the original panel + if (updatedMainPanel.type === "leaf") { + expect( + updatedMainPanel.content.tabs.some( + (t) => t.id === "file-src/App.tsx", + ), + ).toBe(true); + } + }); }); describe("preview tabs", () => {