diff --git a/src/features/file-explorer/hooks/use-file-explorer-drag-drop.ts b/src/features/file-explorer/hooks/use-file-explorer-drag-drop.ts index 65ee5a767..433bb9358 100644 --- a/src/features/file-explorer/hooks/use-file-explorer-drag-drop.ts +++ b/src/features/file-explorer/hooks/use-file-explorer-drag-drop.ts @@ -108,9 +108,22 @@ export function useFileExplorerDragDrop( } }, [dragState.mousePosition]); + const dragStateRef = useRef(dragState); + useEffect(() => { + dragStateRef.current = dragState; + }, [dragState]); + useEffect(() => { if (!dragState.isDragging) return; + const handleAbort = () => { + setDragState(initialDragState); + clearAutoExpand(); + clearEditorDropHover(); + }; + + window.addEventListener("athas-drag-abort", handleAbort); + const handleMouseMove = (e: MouseEvent) => { setDragState((prev) => ({ ...prev, @@ -125,11 +138,12 @@ export function useFileExplorerDragDrop( elementUnder?.closest("[data-pane-container]") || elementUnder?.closest("[data-tab-bar-pane-id]"); + const draggedItem = dragStateRef.current.draggedItem; + if (fileTreeItem) { clearEditorDropHover(); const path = fileTreeItem.getAttribute("data-file-path"); const isDir = fileTreeItem.getAttribute("data-is-dir") === "true"; - const draggedItem = dragState.draggedItem; if (path && draggedItem && path !== draggedItem.path) { const separator = getPathSeparator(draggedItem.path); @@ -163,7 +177,7 @@ export function useFileExplorerDragDrop( dragOverIsDir: true, })); clearAutoExpand(); - } else if (aiContextDropTarget && dragState.draggedItem) { + } else if (aiContextDropTarget && draggedItem) { clearEditorDropHover(); setDragState((prev) => ({ ...prev, @@ -171,7 +185,7 @@ export function useFileExplorerDragDrop( dragOverIsDir: false, })); clearAutoExpand(); - } else if (editorDropTarget && dragState.draggedItem && !dragState.draggedItem.isDir) { + } else if (editorDropTarget && draggedItem && !draggedItem.isDir) { setInternalTabDragHover({ x: e.clientX, y: e.clientY }); setDragState((prev) => ({ ...prev, @@ -191,19 +205,19 @@ export function useFileExplorerDragDrop( }; const handleMouseUp = async (e: MouseEvent) => { - // Check if dropping on a pane container (outside file tree) + const live = dragStateRef.current; const elementUnder = document.elementFromPoint(e.clientX, e.clientY); const isOverPane = elementUnder?.closest("[data-pane-container]") !== null; const isOverFileTree = elementUnder?.closest(".file-tree-container") !== null; const isOverAIContextDropTarget = elementUnder?.closest("[data-ai-context-drop-target]") !== null; - if (isOverAIContextDropTarget && dragState.draggedItem) { + if (isOverAIContextDropTarget && live.draggedItem) { dispatchSidebarResourceDropOnAI({ type: "file", - path: dragState.draggedItem.path, - name: dragState.draggedItem.name, - isDir: dragState.draggedItem.isDir, + path: live.draggedItem.path, + name: live.draggedItem.name, + isDir: live.draggedItem.isDir, }); setDragState(initialDragState); clearAutoExpand(); @@ -211,14 +225,13 @@ export function useFileExplorerDragDrop( return; } - // If dropping on a pane (not in file tree), dispatch event for pane to handle - if (isOverPane && !isOverFileTree && dragState.draggedItem && !dragState.draggedItem.isDir) { + if (isOverPane && !isOverFileTree && live.draggedItem && !live.draggedItem.isDir) { window.dispatchEvent( new CustomEvent("file-tree-drop-on-pane", { detail: { - path: dragState.draggedItem.path, - name: dragState.draggedItem.name, - isDir: dragState.draggedItem.isDir, + path: live.draggedItem.path, + name: live.draggedItem.name, + isDir: live.draggedItem.isDir, x: e.clientX, y: e.clientY, }, @@ -230,9 +243,9 @@ export function useFileExplorerDragDrop( return; } - if (dragState.dragOverPath && dragState.draggedItem) { - const { path: sourcePath, name: sourceName } = dragState.draggedItem; - let targetPath = dragState.dragOverPath; + if (live.dragOverPath && live.draggedItem) { + const { path: sourcePath, name: sourceName } = live.draggedItem; + let targetPath = live.dragOverPath; if (targetPath === "__ROOT__") { targetPath = rootFolderPath || ""; @@ -243,7 +256,7 @@ export function useFileExplorerDragDrop( } } - if (!dragState.dragOverIsDir && targetPath !== "__ROOT__") { + if (!live.dragOverIsDir && targetPath !== "__ROOT__") { targetPath = getDirName(targetPath) || rootFolderPath || ""; } @@ -272,13 +285,14 @@ export function useFileExplorerDragDrop( document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); document.removeEventListener("mouseleave", handleMouseUp); + window.removeEventListener("athas-drag-abort", handleAbort); clearAutoExpand(); clearEditorDropHover(); }; }, [ + dragState.isDragging, clearAutoExpand, clearEditorDropHover, - dragState, onFileMove, onMoveError, rootFolderPath, @@ -297,7 +311,6 @@ export function useFileExplorerDragDrop( mousePosition: { x: e.clientX, y: e.clientY }, }); - // Store drag data globally for pane containers to access window.__fileDragData = { path: file.path, name: file.name, @@ -305,7 +318,6 @@ export function useFileExplorerDragDrop( }; }, []); - // Clean up global drag data on drag end useEffect(() => { if (!dragState.isDragging) { delete window.__fileDragData; diff --git a/src/features/layout/components/main-layout.tsx b/src/features/layout/components/main-layout.tsx index beecc6d51..28e169176 100644 --- a/src/features/layout/components/main-layout.tsx +++ b/src/features/layout/components/main-layout.tsx @@ -23,6 +23,10 @@ import { SplitViewRoot } from "@/features/panes/components/split-view-root"; import { usePaneKeyboard } from "@/features/panes/hooks/use-pane-keyboard"; import QuickOpen from "@/features/quick-open/components/quick-open"; import { useSettingsStore } from "@/features/settings/store"; +import { + attachDragKeyHandlers, + getInternalTabDragData, +} from "@/features/tabs/utils/internal-tab-drag"; import VimCommandBar from "@/features/vim/components/vim-command-bar"; import { useVimKeyboard } from "@/features/vim/hooks/use-vim-keyboard"; import { useVimStore } from "@/features/vim/stores/vim-store"; @@ -34,7 +38,6 @@ import { useUIState } from "@/features/window/stores/ui-state-store"; import { ExtensionDialogs } from "@/extensions/ui/components/extension-dialog"; import { toast } from "@/ui/toast"; import { frontendTrace } from "@/utils/frontend-trace"; -import { getInternalTabDragData } from "@/features/tabs/utils/internal-tab-drag"; import { VimSearchBar } from "../../vim/components/vim-search-bar"; import CustomTitleBarWithSettings from "../../window/components/custom-title-bar"; import { TerminalHost } from "@/features/terminal/components/terminal-host"; @@ -121,6 +124,10 @@ export function MainLayout() { void initializeDebuggerEventBridge(); }, []); + useEffect(() => { + attachDragKeyHandlers(); + }, []); + useEffect(() => { if (!onboardingOpen || !onboardingContext) return; diff --git a/src/features/panes/components/pane-container.tsx b/src/features/panes/components/pane-container.tsx index 60bea70db..346dfffe3 100644 --- a/src/features/panes/components/pane-container.tsx +++ b/src/features/panes/components/pane-container.tsx @@ -1,10 +1,6 @@ import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useShallow } from "zustand/react/shallow"; import type { DatabaseType } from "@/features/database/models/provider.types"; -import { - PROVIDER_REGISTRY, - type DatabaseViewerProps, -} from "@/features/database/providers/provider-registry"; +import { PROVIDER_REGISTRY } from "@/features/database/providers/provider-registry"; import CodeEditor from "@/features/editor/components/code-editor"; import { useBufferStore } from "@/features/editor/stores/buffer-store"; import type { Buffer } from "@/features/editor/stores/buffer-store"; @@ -23,23 +19,22 @@ import { useSettingsStore } from "@/features/settings/store"; import TabBar from "@/features/tabs/components/tab-bar"; import { extractDroppedFilePaths } from "@/features/file-system/utils/file-system-dropped-paths"; import { + applyDropZoneGates, clearInternalTabDragData, + getSplitDropConfig, getInternalTabDragData, getInternalTabDragHover, + isEdgeDropZone, + isDragAborted, resolveDropTarget, } from "@/features/tabs/utils/internal-tab-drag"; import { cn } from "@/utils/cn"; -import { activateBufferInPaneAndSync, activatePaneAndSyncBuffer } from "../utils/pane-activation"; import { EmptyEditorState } from "./empty-editor-state"; import { BOTTOM_PANE_ID } from "../constants/pane"; import { usePaneStore } from "../stores/pane-store"; import type { PaneGroup } from "../types/pane"; +import { hasTextContent } from "../types/pane-content"; import type { EditorContent, PullRequestContent } from "../types/pane-content"; -import { - ensureBufferInPaneDropTarget, - getOrCreatePaneDropTarget, - moveBufferToPaneDropTarget, -} from "../utils/pane-drop-actions"; import { type DropZone, SplitDropOverlay } from "./split-drop-overlay"; const AgentTab = lazy(() => @@ -50,7 +45,7 @@ const AgentTab = lazy(() => const databaseViewerCache = new Map< DatabaseType, - React.LazyExoticComponent> + React.LazyExoticComponent> >(); function getDatabaseViewer(dbType: DatabaseType) { if (!databaseViewerCache.has(dbType)) { @@ -71,7 +66,7 @@ const DiagnosticsBuffer = lazy( () => import("@/features/diagnostics/components/diagnostics-buffer"), ); const OnboardingView = lazy(() => import("@/features/onboarding/components/onboarding-view")); -const GitHubPRViewer = lazy(() => import("@/features/github/components/github-pr-viewer")); +const PRViewer = lazy(() => import("@/features/github/components/github-pr-viewer")); const GitHubIssueViewer = lazy(() => import("@/features/github/components/github-issue-viewer")); const GitHubActionViewer = lazy(() => import("@/features/github/components/github-action-viewer")); const ImageViewer = lazy(() => @@ -108,14 +103,10 @@ const DEFAULT_CAROUSEL_CARD_WIDTH = 640; const MIN_CAROUSEL_CARD_WIDTH = 320; const CAROUSEL_OUTER_GAP_PX = 160; -type EditorBufferShell = Pick; -type PaneRenderBuffer = Exclude | EditorBufferShell; - -function BufferPreviewCard({ buffer }: { buffer: PaneRenderBuffer }) { - const previewText = - "content" in buffer && typeof buffer.content === "string" - ? buffer.content.split("\n").slice(0, 14).join("\n").trim() - : ""; +function BufferPreviewCard({ buffer }: { buffer: Buffer }) { + const previewText = hasTextContent(buffer) + ? buffer.content.split("\n").slice(0, 14).join("\n").trim() + : ""; const summary = buffer.type === "terminal" @@ -147,23 +138,23 @@ function BufferPreviewCard({ buffer }: { buffer: PaneRenderBuffer }) { return (
-
+
{previewLines.map((_, index) => ( {index + 1} ))}
-
+          
             {summary}
           
-
+
{buffer.type === "diff" ? formatDiffBufferLabel(buffer.name, buffer.path) : buffer.name}
-
{buffer.path}
+
{buffer.path}
); @@ -185,12 +176,12 @@ function PullRequestPreviewCard({ buffer }: { buffer: PullRequestContent }) {
- + #{buffer.prNumber ?? "--"} -
{buffer.name}
+
{buffer.name}
-
+
{authorLogin ? `@${authorLogin}` : "Pull request"} @@ -198,7 +189,7 @@ function PullRequestPreviewCard({ buffer }: { buffer: PullRequestContent }) { {commitCount ?? "--"} commits {commentCount ?? "--"} comments
-
+
Description @@ -214,13 +205,13 @@ function PullRequestPreviewCard({ buffer }: { buffer: PullRequestContent }) {
-
+
{details?.body?.trim() ? details.body : "Activate this card to inspect the full pull request description, changed files, comments, review state, and checkout actions."}
-
+
{buffer.path}
@@ -228,7 +219,7 @@ function PullRequestPreviewCard({ buffer }: { buffer: PullRequestContent }) { ); } -function isStandardEditorBuffer(buffer: PaneRenderBuffer): buffer is EditorBufferShell { +function isStandardEditorBuffer(buffer: Buffer): buffer is EditorContent { return buffer.type === "editor"; } @@ -237,8 +228,17 @@ function EmptyPaneState() { } export function PaneContainer({ pane }: PaneContainerProps) { + const buffers = useBufferStore.use.buffers(); const activePaneId = usePaneStore.use.activePaneId(); - const { reorderPaneBuffers } = usePaneStore.use.actions(); + const { + setActivePane, + activatePaneBuffer, + addBufferToPane, + moveBufferToPane, + removeBufferFromPane, + reorderPaneBuffers, + splitPane, + } = usePaneStore.use.actions(); const { closeBufferForce, openTerminalBuffer, showNewTabView } = useBufferStore.use.actions(); const rootFolderPath = useFileSystemStore.use.rootFolderPath?.(); const handleFileOpen = useFileSystemStore.use.handleFileOpen?.(); @@ -258,26 +258,12 @@ export function PaneContainer({ pane }: PaneContainerProps) { const suppressAutoCenterRef = useRef(false); const isActivePane = pane.id === activePaneId; - const paneBuffers = useBufferStore( - useShallow((state) => { - const buffersById = new Map(state.buffers.map((buffer) => [buffer.id, buffer])); - - return pane.bufferIds - .map((bufferId) => { - const buffer = buffersById.get(bufferId); - if (!buffer) return undefined; - if (buffer.type === "editor") { - return { - id: buffer.id, - path: buffer.path, - name: buffer.name, - type: buffer.type, - } satisfies EditorBufferShell; - } - return buffer; - }) - .filter((buffer): buffer is PaneRenderBuffer => buffer !== undefined); - }), + const paneBuffers = useMemo( + () => + pane.bufferIds + .map((bufferId) => buffers.find((buffer) => buffer.id === bufferId)) + .filter((buffer): buffer is Buffer => buffer !== undefined), + [buffers, pane.bufferIds], ); const activeBuffer = useMemo(() => { @@ -298,15 +284,19 @@ export function PaneContainer({ pane }: PaneContainerProps) { if (paneBuffers.length > 0) return; if (suppressAutoNewTab) return; - activatePaneAndSyncBuffer(pane.id); + setActivePane(pane.id); showNewTabView(); - }, [pane.id, paneBuffers.length, showNewTabView, suppressAutoNewTab]); + }, [pane.id, paneBuffers.length, setActivePane, showNewTabView, suppressAutoNewTab]); const handlePaneClick = useCallback(() => { if (!isActivePane) { - activatePaneAndSyncBuffer(pane.id); + setActivePane(pane.id); + // Sync buffer store's activeBufferId with this pane's active buffer + if (pane.activeBufferId) { + useBufferStore.getState().actions.setActiveBuffer(pane.activeBufferId); + } } - }, [isActivePane, pane.id]); + }, [isActivePane, pane.id, pane.activeBufferId, setActivePane]); const handlePaneMouseDownCapture = useCallback( (e: React.MouseEvent) => { @@ -322,17 +312,23 @@ export function PaneContainer({ pane }: PaneContainerProps) { } if (!isActivePane) { - activatePaneAndSyncBuffer(pane.id); + setActivePane(pane.id); + if (pane.activeBufferId) { + useBufferStore.getState().actions.setActiveBuffer(pane.activeBufferId); + } } }, - [isActivePane, pane.id], + [isActivePane, pane.id, pane.activeBufferId, setActivePane], ); const handleTabClick = useCallback( (bufferId: string) => { - activateBufferInPaneAndSync(pane.id, bufferId); + setActivePane(pane.id); + activatePaneBuffer(pane.id, bufferId); + // Sync buffer store's activeBufferId + useBufferStore.getState().actions.setActiveBuffer(bufferId); }, - [pane.id], + [activatePaneBuffer, pane.id, setActivePane], ); const openFileTreeDropInPane = useCallback( @@ -346,25 +342,45 @@ export function PaneContainer({ pane }: PaneContainerProps) { const target = resolveDropTarget(point); if (target.paneId !== pane.id) return; - const targetPaneId = getOrCreatePaneDropTarget({ paneId: pane.id, zone: target.zone }); - if (!targetPaneId) return; + if (!window.__fileDragData) return; + + if (isDragAborted()) { + delete window.__fileDragData; + return; + } - activatePaneAndSyncBuffer(targetPaneId); + delete window.__fileDragData; + + const effectiveZone = applyDropZoneGates(target.zone); try { + setActivePane(pane.id); await handleFileOpen(fileDragData.path, false); const openedBufferId = useBufferStore.getState().activeBufferId; - if (openedBufferId) { - ensureBufferInPaneDropTarget(openedBufferId, { paneId: targetPaneId, zone: "center" }); - activateBufferInPaneAndSync(targetPaneId, openedBufferId); + if (!openedBufferId) return; + + if (!isEdgeDropZone(effectiveZone)) { + activatePaneBuffer(pane.id, openedBufferId); + useBufferStore.getState().actions.setActiveBuffer(openedBufferId); + return; } + + const { direction, placement } = getSplitDropConfig(effectiveZone); + const newPaneId = splitPane(pane.id, direction, openedBufferId, placement); + if (!newPaneId) return; + + removeBufferFromPane(pane.id, openedBufferId); + + setActivePane(newPaneId); + activatePaneBuffer(newPaneId, openedBufferId); + useBufferStore.getState().actions.setActiveBuffer(openedBufferId); } catch (error) { console.error("Failed to open file from file tree drop:", error); } finally { delete window.__fileDragData; } }, - [handleFileOpen, pane.id], + [activatePaneBuffer, handleFileOpen, pane.id, removeBufferFromPane, setActivePane, splitPane], ); const openSidebarResourceInPane = useCallback( @@ -374,24 +390,32 @@ export function PaneContainer({ pane }: PaneContainerProps) { const target = resolveDropTarget(point); if (target.paneId !== pane.id) return; - const targetPaneId = opensBuffer - ? getOrCreatePaneDropTarget({ paneId: pane.id, zone: target.zone }) - : pane.id; - if (!targetPaneId) return; + let targetPaneId = pane.id; + if (opensBuffer && isEdgeDropZone(target.zone)) { + const { direction, placement } = getSplitDropConfig(target.zone); + const newPaneId = splitPane(pane.id, direction, undefined, placement); + if (!newPaneId) return; + targetPaneId = newPaneId; + } - activatePaneAndSyncBuffer(targetPaneId); + setActivePane(targetPaneId); try { const bufferId = await openSidebarResourceBuffer(resource); if (!bufferId) return; - ensureBufferInPaneDropTarget(bufferId, { paneId: targetPaneId, zone: "center" }); - activateBufferInPaneAndSync(targetPaneId, bufferId); + const targetPane = usePaneStore.getState().actions.getPaneById(targetPaneId); + if (!targetPane?.bufferIds.includes(bufferId)) { + addBufferToPane(targetPaneId, bufferId, true); + } else { + activatePaneBuffer(targetPaneId, bufferId); + } + useBufferStore.getState().actions.setActiveBuffer(bufferId); } catch (error) { console.error("Failed to open sidebar resource from drop:", error); } }, - [pane.id], + [activatePaneBuffer, addBufferToPane, pane.id, setActivePane, splitPane], ); const getCarouselWidthBounds = useCallback(() => { @@ -517,7 +541,6 @@ export function PaneContainer({ pane }: PaneContainerProps) { } }, [activeBuffer, closeBufferForce]); - // Listen for file tree drops on this pane useEffect(() => { const syncHover = () => { const hover = getInternalTabDragHover(); @@ -525,26 +548,26 @@ export function PaneContainer({ pane }: PaneContainerProps) { }; window.addEventListener("athas-internal-tab-drag-hover", syncHover); - return () => window.removeEventListener("athas-internal-tab-drag-hover", syncHover); + window.addEventListener("athas-drag-abort", syncHover); + + return () => { + window.removeEventListener("athas-internal-tab-drag-hover", syncHover); + window.removeEventListener("athas-drag-abort", syncHover); + }; }, [pane.id]); useEffect(() => { - const handleFileTreeDrop = async (e: CustomEvent) => { + const handleFileTreeDrop = (event: Event) => { const fileDragData = window.__fileDragData; if (!fileDragData) return; - await openFileTreeDropInPane(fileDragData, { x: e.detail.x, y: e.detail.y }); + const { detail } = event as CustomEvent<{ x: number; y: number }>; + void openFileTreeDropInPane(fileDragData, { x: detail.x, y: detail.y }); }; - window.addEventListener( - "file-tree-drop-on-pane", - handleFileTreeDrop as unknown as EventListener, - ); + window.addEventListener("file-tree-drop-on-pane", handleFileTreeDrop); return () => { - window.removeEventListener( - "file-tree-drop-on-pane", - handleFileTreeDrop as unknown as EventListener, - ); + window.removeEventListener("file-tree-drop-on-pane", handleFileTreeDrop); }; }, [openFileTreeDropInPane]); @@ -591,6 +614,14 @@ export function PaneContainer({ pane }: PaneContainerProps) { if (!zone) return; + if (isDragAborted()) { + clearInternalTabDragData(); + return; + } + + const gatedZone = applyDropZoneGates(zone); + if (!gatedZone) return; + const tabDataString = e.dataTransfer.getData("application/tab-data"); const fallbackTabData = getInternalTabDragData(); if (!tabDataString && !fallbackTabData) return; @@ -619,71 +650,57 @@ export function PaneContainer({ pane }: PaneContainerProps) { clearInternalTabDragData(); } - if (zone === "center") { + if (gatedZone === "center") { if (source === "terminal-panel" && terminalId) { - const newBufferId = openTerminalBuffer({ + setActivePane(pane.id); + openTerminalBuffer({ sessionId: terminalId, name: terminalName, command: initialCommand, workingDirectory: currentDirectory, remoteConnectionId, }); - activateBufferInPaneAndSync(pane.id, newBufferId); window.dispatchEvent( new CustomEvent("terminal-detach-to-buffer", { detail: { terminalId }, }), ); } else if (sourcePaneId && sourcePaneId !== pane.id && bufferId) { - moveBufferToPaneDropTarget(bufferId, sourcePaneId, { paneId: pane.id, zone: "center" }); - activateBufferInPaneAndSync(pane.id, bufferId); + moveBufferToPane(bufferId, sourcePaneId, pane.id); } else if (!sourcePaneId && bufferId) { - ensureBufferInPaneDropTarget(bufferId, { paneId: pane.id, zone: "center" }); - activateBufferInPaneAndSync(pane.id, bufferId); + addBufferToPane(pane.id, bufferId, true); } return; } - const newPaneId = getOrCreatePaneDropTarget({ paneId: pane.id, zone }); + if (!isEdgeDropZone(gatedZone)) return; + + const { direction, placement } = getSplitDropConfig(gatedZone); + const newPaneId = splitPane(pane.id, direction, undefined, placement); if (!newPaneId) return; // Move the dragged buffer into the newly created pane. if (source === "terminal-panel" && terminalId) { - const newBufferId = openTerminalBuffer({ + setActivePane(newPaneId); + openTerminalBuffer({ sessionId: terminalId, name: terminalName, command: initialCommand, workingDirectory: currentDirectory, remoteConnectionId, }); - activateBufferInPaneAndSync(newPaneId, newBufferId); window.dispatchEvent( new CustomEvent("terminal-detach-to-buffer", { detail: { terminalId }, }), ); } else if (sourcePaneId && sourcePaneId !== pane.id && bufferId) { - moveBufferToPaneDropTarget(bufferId, sourcePaneId, { paneId: newPaneId, zone: "center" }); - activateBufferInPaneAndSync(newPaneId, bufferId); + moveBufferToPane(bufferId, sourcePaneId, newPaneId); } else if (bufferId) { - moveBufferToPaneDropTarget(bufferId, pane.id, { paneId: newPaneId, zone: "center" }); - activateBufferInPaneAndSync(newPaneId, bufferId); + moveBufferToPane(bufferId, pane.id, newPaneId); } }, - [pane.id, openTerminalBuffer], - ); - - // Handle mouse up for file tree drag (which uses mouse events, not HTML5 drag API) - const handleMouseUp = useCallback( - async (event: React.MouseEvent) => { - const fileDragData = window.__fileDragData; - if (!fileDragData || fileDragData.isDir) { - return; // Only handle file drops, not directory drops - } - - await openFileTreeDropInPane(fileDragData, { x: event.clientX, y: event.clientY }); - }, - [openFileTreeDropInPane], + [pane.id, splitPane, moveBufferToPane, addBufferToPane, openTerminalBuffer, setActivePane], ); const handleDrop = useCallback( @@ -692,7 +709,7 @@ export function PaneContainer({ pane }: PaneContainerProps) { e.stopPropagation(); setIsDragOver(false); setIsTabDragOver(false); - activatePaneAndSyncBuffer(pane.id); + setActivePane(pane.id); // Tab drops are handled by SplitDropOverlay — skip here if (e.dataTransfer.types.includes("application/tab-data") || getInternalTabDragData()) { @@ -713,7 +730,7 @@ export function PaneContainer({ pane }: PaneContainerProps) { return; } }, - [pane.id, handleFileOpen, openSidebarResourceInPane], + [addBufferToPane, handleFileOpen, openSidebarResourceInPane, pane.id, setActivePane], ); const handleCarouselWheel = useCallback( @@ -814,7 +831,7 @@ export function PaneContainer({ pane }: PaneContainerProps) { horizontalBufferCarousel && (paneBuffers.length > 1 || shouldShowNewTabCard); const renderActiveBuffer = useCallback( - (buffer: PaneRenderBuffer) => { + (buffer: Buffer) => { switch (buffer.type) { case "newTab": return null; @@ -842,7 +859,7 @@ export function PaneContainer({ pane }: PaneContainerProps) { return ; case "pullRequest": - return ; + return ; case "githubIssue": return ( @@ -889,20 +906,9 @@ export function PaneContainer({ pane }: PaneContainerProps) { case "database": { const config = PROVIDER_REGISTRY[buffer.databaseType]; const DatabaseViewer = getDatabaseViewer(buffer.databaseType); - let viewerProps: DatabaseViewerProps; - if (config.isFileBased) { - viewerProps = { databasePath: buffer.path }; - } else { - const connectionId = buffer.connectionId; - if (!connectionId) { - return ( -
- Missing database connection -
- ); - } - viewerProps = { connectionId }; - } + const viewerProps = config.isFileBased + ? { databasePath: buffer.path } + : { connectionId: buffer.connectionId }; return ; } @@ -951,19 +957,10 @@ export function PaneContainer({ pane }: PaneContainerProps) { } ${isDragOver || internalHoverZone ? "ring-2 ring-accent" : ""}`} onMouseDownCapture={handlePaneMouseDownCapture} onClick={handlePaneClick} - onMouseUp={handleMouseUp} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} > - {(isDragOver || internalHoverZone) && !isTabDragOver && !internalHoverZone && ( -
- )} -
+ {!activeBuffer && !shouldRenderCarousel && } {activeBuffer?.type === "newTab" && !shouldRenderCarousel && } diff --git a/src/features/panes/components/split-drop-overlay.tsx b/src/features/panes/components/split-drop-overlay.tsx index c8bbb0012..2d4f24ef1 100644 --- a/src/features/panes/components/split-drop-overlay.tsx +++ b/src/features/panes/components/split-drop-overlay.tsx @@ -1,8 +1,11 @@ import { useCallback, useState } from "react"; +import { + getDropZoneForPoint, + type InternalDropZone, +} from "@/features/tabs/utils/internal-tab-drag"; import { cn } from "@/utils/cn"; -import { getPaneDropZoneFromRect, type PaneDropZone } from "../utils/pane-drop-zones"; -export type DropZone = PaneDropZone; +export type DropZone = InternalDropZone; interface SplitDropOverlayProps { activeZoneOverride?: DropZone; @@ -10,12 +13,28 @@ interface SplitDropOverlayProps { visible: boolean; } -const zoneStyles: Record = { - left: "right-1/2 inset-y-1 left-1 rounded-lg", - right: "left-1/2 inset-y-1 right-1 rounded-lg", - top: "bottom-1/2 inset-x-1 top-1 rounded-lg", - bottom: "top-1/2 inset-x-1 bottom-1 rounded-lg", - center: "inset-1 rounded-lg", +const INSET = 4; +const TRANSITION_EASING = "cubic-bezier(0.16, 1, 0.3, 1)"; +const OVERLAY_TRANSITION = ["left", "right", "top", "bottom", "inset", "opacity"] + .map((property) => `${property} 120ms ${property === "opacity" ? "ease-out" : TRANSITION_EASING}`) + .join(", "); + +const zonePositions: Record, React.CSSProperties> = { + left: { left: INSET, top: INSET, bottom: INSET, right: "50%" }, + right: { right: INSET, top: INSET, bottom: INSET, left: "50%" }, + top: { top: INSET, left: INSET, right: INSET, bottom: "50%" }, + bottom: { bottom: INSET, left: INSET, right: INSET, top: "50%" }, + center: { inset: INSET }, +}; + +const overlayStyle: React.CSSProperties = { + backgroundColor: "color-mix(in srgb, var(--color-accent) 4%, transparent)", + border: "1px solid color-mix(in srgb, var(--color-accent) 45%, transparent)", + backdropFilter: "blur(8px)", + WebkitBackdropFilter: "blur(8px)", + boxShadow: + "0 0 0 1px color-mix(in srgb, var(--color-accent) 10%, transparent), 0 8px 24px rgba(0, 0, 0, 0.12)", + transition: OVERLAY_TRANSITION, }; export function SplitDropOverlay({ activeZoneOverride, onDrop, visible }: SplitDropOverlayProps) { @@ -26,7 +45,7 @@ export function SplitDropOverlay({ activeZoneOverride, onDrop, visible }: SplitD e.stopPropagation(); e.dataTransfer.dropEffect = "move"; const rect = e.currentTarget.getBoundingClientRect(); - setActiveZone(getPaneDropZoneFromRect({ x: e.clientX, y: e.clientY }, rect)); + setActiveZone(getDropZoneForPoint({ x: e.clientX, y: e.clientY }, rect)); }, []); const handleDrop = useCallback( @@ -34,7 +53,7 @@ export function SplitDropOverlay({ activeZoneOverride, onDrop, visible }: SplitD e.preventDefault(); e.stopPropagation(); const rect = e.currentTarget.getBoundingClientRect(); - const zone = getPaneDropZoneFromRect({ x: e.clientX, y: e.clientY }, rect); + const zone = getDropZoneForPoint({ x: e.clientX, y: e.clientY }, rect); setActiveZone(null); onDrop(zone, e); }, @@ -66,10 +85,11 @@ export function SplitDropOverlay({ activeZoneOverride, onDrop, visible }: SplitD > {effectiveZone && (
)}
diff --git a/src/features/tabs/components/tab-bar.tsx b/src/features/tabs/components/tab-bar.tsx index 5933fc21f..7bf4adcfa 100644 --- a/src/features/tabs/components/tab-bar.tsx +++ b/src/features/tabs/components/tab-bar.tsx @@ -16,8 +16,6 @@ import { ArrowRight, ArrowsOut as Maximize2, ArrowsIn as Minimize2, - Lock, - LockOpen, SidebarSimple as PanelLeftClose, SplitHorizontal as SplitSquareHorizontal, } from "@phosphor-icons/react"; @@ -30,11 +28,8 @@ import { navigateToJumpEntry } from "@/features/editor/utils/jump-navigation"; import { useFileSystemStore } from "@/features/file-system/controllers/store"; import { formatDiffBufferLabel } from "@/features/git/utils/diff-buffer-label"; import { BOTTOM_PANE_ID } from "@/features/panes/constants/pane"; -import { usePaneStore } from "@/features/panes/stores/pane-store"; -import { activateBufferInPaneAndSync } from "@/features/panes/utils/pane-activation"; -import { splitEditorGroup } from "@/features/panes/utils/pane-command-actions"; -import { moveBufferToPaneDropTarget } from "@/features/panes/utils/pane-drop-actions"; import { findPaneGroup } from "@/features/panes/utils/pane-tree"; +import { usePaneStore } from "@/features/panes/stores/pane-store"; import { useSettingsStore } from "@/features/settings/store"; import type { PaneContent } from "@/features/panes/types/pane-content"; import { useEditorAppStore } from "@/features/editor/stores/editor-app-store"; @@ -47,7 +42,11 @@ import { Button } from "@/ui/button"; import { getRelativePath } from "@/utils/path-helpers"; import { calculateDisplayNames } from "../utils/path-shortener"; import { + applyDropZoneGates, clearInternalTabDragData, + getSplitDropConfig, + isEdgeDropZone, + isDragAborted, resolveDropTarget, setInternalTabDragHover, setInternalTabDragData, @@ -76,7 +75,8 @@ const TabBar = ({ const paneRoot = usePaneStore.use.root(); const bottomRoot = usePaneStore.use.bottomRoot(); const fullscreenPaneId = usePaneStore.use.fullscreenPaneId(); - const { closePane, togglePaneFullscreen, setPaneLocked } = usePaneStore.use.actions(); + const { moveBufferToPane, setActivePane, splitPane, closePane, togglePaneFullscreen } = + usePaneStore.use.actions(); // Filter buffers by paneId if provided const pane = paneId @@ -118,7 +118,6 @@ const TabBar = ({ ? Boolean(activeWebViewerNavigation?.canGoForward) : jumpListActions.canGoForward(); const isPaneFullscreen = paneId ? fullscreenPaneId === paneId : false; - const isPaneLocked = Boolean(pane?.locked); const isInSplit = paneRoot.type === "split"; const isBottomPane = paneId === BOTTOM_PANE_ID; @@ -221,19 +220,23 @@ const TabBar = ({ const handleSplitActivePane = useCallback(() => { if (!paneId) return; - splitEditorGroup(paneId, "horizontal", activeBufferId); - }, [activeBufferId, paneId]); + + // Terminal, agent, and other session-based buffers cannot be shared + // across panes — open the new split with an empty new-tab view instead. + const isSessionBuffer = + activeBuffer && + (activeBuffer.type === "terminal" || + activeBuffer.type === "agent" || + activeBuffer.type === "webViewer"); + + splitPane(paneId, "horizontal", isSessionBuffer ? undefined : (activeBufferId ?? undefined)); + }, [activeBuffer, activeBufferId, paneId, splitPane]); const handleTogglePaneFullscreen = useCallback(() => { if (!paneId) return; togglePaneFullscreen(paneId); }, [paneId, togglePaneFullscreen]); - const handleTogglePaneLocked = useCallback(() => { - if (!paneId) return; - setPaneLocked(paneId, !isPaneLocked); - }, [isPaneLocked, paneId, setPaneLocked]); - const canScrollTabsHorizontally = useCallback(() => { const container = tabBarRef.current; if (!container) return false; @@ -296,10 +299,6 @@ const TabBar = ({ (buffer: PaneContent) => { if (buffer.type === "terminal") { const session = terminalSessions.get(buffer.sessionId); - if (session?.customName) { - return session.name?.trim() || buffer.name; - } - const title = session?.title?.trim(); if (isUsefulTerminalTitle(title)) return title!; @@ -478,20 +477,6 @@ const TabBar = ({ }; }; - const isPointOutsideTabBar = (point: { x: number; y: number }) => { - const rect = tabBarRef.current?.getBoundingClientRect(); - if (!rect) return false; - - const horizontalSlop = 24; - const verticalSlop = 64; - return ( - point.x < rect.left - horizontalSlop || - point.x > rect.right + horizontalSlop || - point.y < rect.top - verticalSlop || - point.y > rect.bottom + verticalSlop - ); - }; - const handleDragStart = useCallback( (event: DragStartEvent) => { const buffer = sortedBuffers.find((item) => item.id === String(event.active.id)); @@ -514,9 +499,7 @@ const TabBar = ({ if (!point) return; dragPointRef.current = point; - if (isPointOutsideTabBar(point)) { - setInternalTabDragHover(point); - } + setInternalTabDragHover(point); }, []); const resetDrag = useCallback(() => { @@ -543,27 +526,31 @@ const TabBar = ({ const dragged = sortedBuffers.find((buffer) => buffer.id === activeId); const point = getDragPoint(event); const target = point ? resolveDropTarget(point) : { paneId: null, zone: null }; - const isOutsideTabBar = point ? isPointOutsideTabBar(point) : false; + + // Esc-aborted: drop nothing, snap tab back. + if (isDragAborted()) { + resetDrag(); + return; + } + + const gatedZone = applyDropZoneGates(target.zone); if ( dragged && paneId && - isOutsideTabBar && target.paneId && - (target.paneId !== paneId || (target.zone && target.zone !== "center")) + (target.paneId !== paneId || isEdgeDropZone(gatedZone)) ) { + let destinationPaneId = target.paneId; const preserveEmptySource = target.paneId === paneId; - const destinationPaneId = moveBufferToPaneDropTarget( - dragged.id, - paneId, - { paneId: target.paneId, zone: target.zone }, - preserveEmptySource, - ); - if (!destinationPaneId) { - resetDrag(); - return; + if (isEdgeDropZone(gatedZone)) { + const { direction, placement } = getSplitDropConfig(gatedZone); + destinationPaneId = + splitPane(target.paneId, direction, undefined, placement) ?? target.paneId; } - activateBufferInPaneAndSync(destinationPaneId, dragged.id); + + setActivePane(destinationPaneId); + moveBufferToPane(dragged.id, paneId, destinationPaneId, preserveEmptySource); if (destinationPaneId === BOTTOM_PANE_ID) { useUIState.getState().setBottomPaneActiveTab("buffers"); useUIState.getState().setIsBottomPaneVisible(true); @@ -581,7 +568,16 @@ const TabBar = ({ resetDrag(); }, - [handleTabClick, paneId, reorderBuffers, resetDrag, sortedBuffers], + [ + handleTabClick, + moveBufferToPane, + paneId, + reorderBuffers, + resetDrag, + setActivePane, + sortedBuffers, + splitPane, + ], ); useEffect(() => { @@ -686,7 +682,7 @@ const TabBar = ({
@@ -711,12 +707,12 @@ const TabBar = ({ onClick={handleJumpForward} disabled={!canGoForward} variant="ghost" - className="h-5 min-w-5 shrink-0 rounded-md px-1 text-text-lighter" + compact + className="shrink-0 rounded-lg text-text-lighter" tooltip="Go Forward" tooltipSide="bottom" commandId="navigation.goForward" aria-label="Go forward to next location" - compact > @@ -757,7 +753,7 @@ const TabBar = ({ onClick={() => closePane(paneId)} variant="ghost" compact - className="h-5 min-w-5 shrink-0 rounded-md px-1 text-text-lighter" + className="shrink-0 rounded-lg text-text-lighter" tooltip="Close Split" tooltipSide="bottom" aria-label="Close split pane" @@ -770,43 +766,25 @@ const TabBar = ({ type="button" onClick={handleSplitActivePane} variant="ghost" - className="h-5 min-w-5 shrink-0 rounded-md px-1 text-text-lighter" + compact + className="shrink-0 rounded-lg text-text-lighter" tooltip="Split Editor" tooltipSide="bottom" aria-label="Split editor" - compact > )} - {paneId && !disablePaneActions && !isBottomPane && ( - - )} {paneId && !disablePaneActions && !isBottomPane && ( @@ -819,7 +797,7 @@ const TabBar = ({ {draggedBuffer ? ( -
+
{draggedBuffer.name}
) : null} @@ -871,14 +849,16 @@ const TabBar = ({ onSplitRight={ paneId ? (targetPaneId, bufferId) => { - splitEditorGroup(targetPaneId, "horizontal", bufferId); + const { splitPane } = usePaneStore.getState().actions; + splitPane(targetPaneId, "horizontal", bufferId); } : undefined } onSplitDown={ paneId ? (targetPaneId, bufferId) => { - splitEditorGroup(targetPaneId, "vertical", bufferId); + const { splitPane } = usePaneStore.getState().actions; + splitPane(targetPaneId, "vertical", bufferId); } : undefined } diff --git a/src/features/tabs/utils/internal-tab-drag.ts b/src/features/tabs/utils/internal-tab-drag.ts index 459c2c38d..67a6098e9 100644 --- a/src/features/tabs/utils/internal-tab-drag.ts +++ b/src/features/tabs/utils/internal-tab-drag.ts @@ -1,7 +1,10 @@ import { BOTTOM_PANE_ID } from "@/features/panes/constants/pane"; -import { getPaneDropZoneFromRect, type PaneDropZone } from "@/features/panes/utils/pane-drop-zones"; +import type { SplitDirection, SplitPlacement } from "@/features/panes/types/pane"; -export type InternalDropZone = PaneDropZone; +export type InternalDropZone = "left" | "right" | "top" | "bottom" | "center" | null; +export type EdgeDropZone = Exclude; + +type ClientPoint = { x: number; y: number }; export interface InternalTabDragData { source?: "pane" | "terminal-panel"; @@ -23,9 +26,68 @@ declare global { interface Window { __athasInternalTabDragData?: InternalTabDragData; __athasInternalTabDragHover?: InternalTabDragHoverTarget; + __athasDragSplitSuppressed?: boolean; + __athasDragAborted?: boolean; + } +} + +const DRAG_DOUBLE_ESC_WINDOW_MS = 600; +let lastEscAt = 0; +let dragKeyHandlersAttached = false; + +function isAnyDragActive(): boolean { + return !!window.__athasInternalTabDragData || !!window.__fileDragData; +} + +function abortDrag() { + window.__athasDragAborted = true; + window.__athasDragSplitSuppressed = false; + delete window.__fileDragData; + delete window.__athasInternalTabDragData; + delete window.__athasInternalTabDragHover; + window.dispatchEvent(new CustomEvent("athas-internal-tab-drag-hover")); + window.dispatchEvent(new CustomEvent("athas-drag-abort")); +} + +function handleDragKeydown(event: KeyboardEvent) { + if (!isAnyDragActive()) return; + if (event.key !== "Escape") return; + + event.preventDefault(); + const now = performance.now(); + if (now - lastEscAt < DRAG_DOUBLE_ESC_WINDOW_MS) { + abortDrag(); + lastEscAt = 0; + return; + } + + lastEscAt = now; + window.__athasDragSplitSuppressed = true; + const hover = window.__athasInternalTabDragHover; + if (hover?.paneId) { + window.__athasInternalTabDragHover = { paneId: hover.paneId, zone: "center" }; + window.dispatchEvent(new CustomEvent("athas-internal-tab-drag-hover")); } } +export function attachDragKeyHandlers() { + if (dragKeyHandlersAttached) return; + dragKeyHandlersAttached = true; + window.addEventListener("keydown", handleDragKeydown, true); + window.__athasDragSplitSuppressed = false; + window.__athasDragAborted = false; +} + +export function resetDragModifierState() { + window.__athasDragSplitSuppressed = false; + window.__athasDragAborted = false; + lastEscAt = 0; +} + +export function isDragAborted(): boolean { + return !!window.__athasDragAborted; +} + export function setInternalTabDragData(data: InternalTabDragData) { window.__athasInternalTabDragData = data; } @@ -37,9 +99,53 @@ export function getInternalTabDragData(): InternalTabDragData | null { export function clearInternalTabDragData() { delete window.__athasInternalTabDragData; delete window.__athasInternalTabDragHover; + resetDragModifierState(); window.dispatchEvent(new CustomEvent("athas-internal-tab-drag-hover")); } +export function getDropZoneForPoint(point: ClientPoint, rect: DOMRect): InternalDropZone { + const x = point.x - rect.left; + const y = point.y - rect.top; + const nx = x / rect.width; + const ny = y / rect.height; + const threshold = 1 / 3; + + if (nx < threshold && nx < ny && nx < 1 - ny) return "left"; + if (nx > 1 - threshold && 1 - nx < ny && 1 - nx < 1 - ny) return "right"; + if (ny < threshold) return "top"; + if (ny > 1 - threshold) return "bottom"; + return "center"; +} + +function containsPoint(rect: DOMRect, point: ClientPoint): boolean { + return ( + point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom + ); +} + +function findContainingClosest( + elements: Element[], + selector: string, + point: ClientPoint, +): HTMLElement | null { + for (const element of elements) { + const candidate = element.closest(selector); + if (candidate && containsPoint(candidate.getBoundingClientRect(), point)) { + return candidate; + } + } + return null; +} + +function findContainingElement(selector: string, point: ClientPoint): HTMLElement | null { + const candidates = document.querySelectorAll(selector); + return ( + Array.from(candidates).find((candidate) => + containsPoint(candidate.getBoundingClientRect(), point), + ) ?? null + ); +} + export function setInternalTabDragHoverTarget(next: InternalTabDragHoverTarget) { const prev = window.__athasInternalTabDragHover; if (prev?.paneId === next.paneId && prev?.zone === next.zone) return; @@ -47,43 +153,67 @@ export function setInternalTabDragHoverTarget(next: InternalTabDragHoverTarget) window.dispatchEvent(new CustomEvent("athas-internal-tab-drag-hover")); } -export function setInternalTabDragHover(point: { x: number; y: number }) { - setInternalTabDragHoverTarget(resolveDropTarget(point)); +export function setInternalTabDragHover(point: ClientPoint) { + const target = resolveDropTarget(point); + + if (window.__athasDragSplitSuppressed && target.zone && target.zone !== "center") { + setInternalTabDragHoverTarget({ paneId: target.paneId, zone: "center" }); + return; + } + + setInternalTabDragHoverTarget(target); +} + +export function getInternalTabDragHover(): InternalTabDragHoverTarget { + return window.__athasInternalTabDragHover ?? { paneId: null, zone: null }; +} + +export function isEdgeDropZone(zone: InternalDropZone): zone is EdgeDropZone { + return zone !== null && zone !== "center"; } -export function getInternalTabDragHover() { - return window.__athasInternalTabDragHover ?? { paneId: null, zone: null as InternalDropZone }; +export function getSplitDropConfig(zone: EdgeDropZone): { + direction: SplitDirection; + placement: SplitPlacement; +} { + return { + direction: zone === "left" || zone === "right" ? "horizontal" : "vertical", + placement: zone === "left" || zone === "top" ? "before" : "after", + }; } -export function resolveDropPaneId(point: { x: number; y: number }): string | null { - return resolveDropTarget(point).paneId; +export function applyDropZoneGates(zone: InternalDropZone): InternalDropZone { + if (window.__athasDragAborted) return null; + if (zone === null || zone === "center") return zone; + if (window.__athasDragSplitSuppressed) return "center"; + return zone; } -export function resolveDropTarget(point: { x: number; y: number }) { +export function resolveDropTarget(point: ClientPoint): InternalTabDragHoverTarget { const elements = document.elementsFromPoint(point.x, point.y); if (elements.length === 0) { - return { paneId: null, zone: null as InternalDropZone }; + return { paneId: null, zone: null }; } - const tabBar = elements - .map((element) => element.closest("[data-tab-bar-pane-id]")) - .find((element) => Boolean(element?.dataset.tabBarPaneId)); + const tabBar = + findContainingClosest(elements, "[data-tab-bar-pane-id]", point) ?? + findContainingElement("[data-tab-bar-pane-id]", point); if (tabBar?.dataset.tabBarPaneId) { return { paneId: tabBar.dataset.tabBarPaneId, - zone: "center" as InternalDropZone, + zone: "center", }; } - const paneContainer = elements - .map((element) => element.closest("[data-pane-id]")) - .find((element) => Boolean(element?.dataset.paneId)); + const paneContainer = + findContainingClosest(elements, "[data-pane-id]", point) ?? + findContainingElement("[data-pane-id]", point); if (paneContainer?.dataset.paneId) { return { paneId: paneContainer.dataset.paneId, - zone: getPaneDropZoneFromRect(point, paneContainer.getBoundingClientRect()), + zone: getDropZoneForPoint(point, paneContainer.getBoundingClientRect()), }; } @@ -94,9 +224,9 @@ export function resolveDropTarget(point: { x: number; y: number }) { if (bottomPaneTarget) { return { paneId: BOTTOM_PANE_ID, - zone: "center" as InternalDropZone, + zone: "center", }; } - return { paneId: null, zone: null as InternalDropZone }; + return { paneId: null, zone: null }; } diff --git a/src/features/terminal/components/terminal-tab-bar.tsx b/src/features/terminal/components/terminal-tab-bar.tsx index 80047788d..3ea9e7139 100644 --- a/src/features/terminal/components/terminal-tab-bar.tsx +++ b/src/features/terminal/components/terminal-tab-bar.tsx @@ -56,7 +56,9 @@ import { Dropdown, MenuItemsList, type MenuItem } from "@/ui/dropdown"; import { Button } from "@/ui/button"; import { cn } from "@/utils/cn"; import { + applyDropZoneGates, clearInternalTabDragData, + isDragAborted, resolveDropTarget, setInternalTabDragHover, setInternalTabDragData, @@ -570,11 +572,17 @@ const TerminalTabBar = ({ const point = getDragPoint(event); const target = point ? resolveDropTarget(point) : { paneId: null, zone: null }; const isOutsideTabBar = point ? isPointOutsideTabBar(point) : false; + const gatedZone = applyDropZoneGates(target.zone); + + if (isDragAborted()) { + resetDrag(); + return; + } if (terminal && isOutsideTabBar && target.paneId) { const destinationPaneId = getOrCreatePaneDropTarget({ paneId: target.paneId, - zone: target.zone, + zone: gatedZone, }); if (!destinationPaneId) { resetDrag();