From 4f21bdb66ac2daa0f90472c7460e63fe13457e78 Mon Sep 17 00:00:00 2001 From: Shreyan Gupta <64853271+RA1NCS@users.noreply.github.com> Date: Fri, 8 May 2026 02:36:21 -0400 Subject: [PATCH 1/3] Polish pane split drag and drop Route file-tree and tab drags through the same pane-zone resolver so the preview and final drop agree on the target pane. Support Escape during drag to force a no-split drop, with a second quick Escape aborting the drag entirely. Keep the split preview scoped to the editor surface so it never covers the tab bar, and remove the redundant drop paths and helper UI left from the polish pass. --- .../hooks/use-file-explorer-drag-drop.ts | 52 +-- .../layout/components/main-layout.tsx | 9 +- .../panes/components/pane-container.tsx | 302 +++++++++--------- .../panes/components/split-drop-overlay.tsx | 48 ++- src/features/tabs/components/tab-bar.tsx | 144 ++++----- src/features/tabs/utils/internal-tab-drag.ts | 170 ++++++++-- 6 files changed, 444 insertions(+), 281 deletions(-) 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..6bb53221c 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/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,9 +228,19 @@ function EmptyPaneState() { } export function PaneContainer({ pane }: PaneContainerProps) { + const buffers = useBufferStore.use.buffers(); const activePaneId = usePaneStore.use.activePaneId(); - const { reorderPaneBuffers } = usePaneStore.use.actions(); - const { closeBufferForce, openTerminalBuffer, showNewTabView } = useBufferStore.use.actions(); + const { + setActivePane, + setActivePaneBuffer, + addBufferToPane, + moveBufferToPane, + removeBufferFromPane, + reorderPaneBuffers, + splitPane, + } = usePaneStore.use.actions(); + const { closeBufferForce, openBuffer, openTerminalBuffer, showNewTabView } = + useBufferStore.use.actions(); const rootFolderPath = useFileSystemStore.use.rootFolderPath?.(); const handleFileOpen = useFileSystemStore.use.handleFileOpen?.(); const horizontalBufferCarousel = useSettingsStore((state) => state.settings.horizontalTabScroll); @@ -258,26 +259,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 +285,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 +313,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); + setActivePaneBuffer(pane.id, bufferId); + // Sync buffer store's activeBufferId + useBufferStore.getState().actions.setActiveBuffer(bufferId); }, - [pane.id], + [pane.id, setActivePane, setActivePaneBuffer], ); const openFileTreeDropInPane = useCallback( @@ -346,25 +343,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; + } + + delete window.__fileDragData; - activatePaneAndSyncBuffer(targetPaneId); + 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)) { + setActivePaneBuffer(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); + setActivePaneBuffer(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], + [handleFileOpen, pane.id, removeBufferFromPane, setActivePane, setActivePaneBuffer, splitPane], ); const openSidebarResourceInPane = useCallback( @@ -374,24 +391,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 { + setActivePaneBuffer(targetPaneId, bufferId); + } + useBufferStore.getState().actions.setActiveBuffer(bufferId); } catch (error) { console.error("Failed to open sidebar resource from drop:", error); } }, - [pane.id], + [addBufferToPane, pane.id, setActivePane, setActivePaneBuffer, splitPane], ); const getCarouselWidthBounds = useCallback(() => { @@ -517,7 +542,6 @@ export function PaneContainer({ pane }: PaneContainerProps) { } }, [activeBuffer, closeBufferForce]); - // Listen for file tree drops on this pane useEffect(() => { const syncHover = () => { const hover = getInternalTabDragHover(); @@ -525,26 +549,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 +615,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 +651,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); - } - }, - [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 + moveBufferToPane(bufferId, pane.id, newPaneId); } - - await openFileTreeDropInPane(fileDragData, { x: event.clientX, y: event.clientY }); }, - [openFileTreeDropInPane], + [pane.id, splitPane, moveBufferToPane, addBufferToPane, openTerminalBuffer, setActivePane], ); const handleDrop = useCallback( @@ -692,7 +710,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 +731,18 @@ export function PaneContainer({ pane }: PaneContainerProps) { return; } }, - [pane.id, handleFileOpen, openSidebarResourceInPane], + [ + pane.id, + pane.bufferIds, + buffers, + setActivePane, + addBufferToPane, + moveBufferToPane, + setActivePaneBuffer, + openBuffer, + handleFileOpen, + openSidebarResourceInPane, + ], ); const handleCarouselWheel = useCallback( @@ -814,7 +843,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 +871,7 @@ export function PaneContainer({ pane }: PaneContainerProps) { return ; case "pullRequest": - return ; + return ; case "githubIssue": return ( @@ -889,20 +918,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 +969,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..ec3f61009 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" + size="icon-sm" + className="shrink-0 rounded-lg text-text-lighter" tooltip="Go Forward" tooltipSide="bottom" commandId="navigation.goForward" aria-label="Go forward to next location" - compact > @@ -756,8 +752,8 @@ const TabBar = ({ type="button" onClick={() => closePane(paneId)} variant="ghost" - compact - className="h-5 min-w-5 shrink-0 rounded-md px-1 text-text-lighter" + size="icon-sm" + 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" + size="icon-sm" + 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 }; } From d2000bbbca8f18e0f0e19123b07d711ced49da73 Mon Sep 17 00:00:00 2001 From: Shreyan Gupta <64853271+RA1NCS@users.noreply.github.com> Date: Sun, 10 May 2026 23:36:16 -0400 Subject: [PATCH 2/3] Fix escape gating for terminal tab drags - apply the same escape suppression and abort rules to terminal tab drops - let terminal drags attach to a pane without splitting after a single escape - stop double-escape from detaching a terminal after the drag was aborted --- src/features/terminal/components/terminal-tab-bar.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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(); From 1c8ade2578c2c7a143e5ae9718d8feb91ddc3095 Mon Sep 17 00:00:00 2001 From: Shreyan Gupta <64853271+RA1NCS@users.noreply.github.com> Date: Mon, 11 May 2026 00:14:50 -0400 Subject: [PATCH 3/3] Fix pane container and tab bar for current UI APIs --- .../panes/components/pane-container.tsx | 34 ++++++------------- src/features/tabs/components/tab-bar.tsx | 10 +++--- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/features/panes/components/pane-container.tsx b/src/features/panes/components/pane-container.tsx index 6bb53221c..346dfffe3 100644 --- a/src/features/panes/components/pane-container.tsx +++ b/src/features/panes/components/pane-container.tsx @@ -66,7 +66,7 @@ const DiagnosticsBuffer = lazy( () => import("@/features/diagnostics/components/diagnostics-buffer"), ); const OnboardingView = lazy(() => import("@/features/onboarding/components/onboarding-view")); -const PRViewer = lazy(() => import("@/features/github/components/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(() => @@ -232,15 +232,14 @@ export function PaneContainer({ pane }: PaneContainerProps) { const activePaneId = usePaneStore.use.activePaneId(); const { setActivePane, - setActivePaneBuffer, + activatePaneBuffer, addBufferToPane, moveBufferToPane, removeBufferFromPane, reorderPaneBuffers, splitPane, } = usePaneStore.use.actions(); - const { closeBufferForce, openBuffer, openTerminalBuffer, showNewTabView } = - useBufferStore.use.actions(); + const { closeBufferForce, openTerminalBuffer, showNewTabView } = useBufferStore.use.actions(); const rootFolderPath = useFileSystemStore.use.rootFolderPath?.(); const handleFileOpen = useFileSystemStore.use.handleFileOpen?.(); const horizontalBufferCarousel = useSettingsStore((state) => state.settings.horizontalTabScroll); @@ -325,11 +324,11 @@ export function PaneContainer({ pane }: PaneContainerProps) { const handleTabClick = useCallback( (bufferId: string) => { setActivePane(pane.id); - setActivePaneBuffer(pane.id, bufferId); + activatePaneBuffer(pane.id, bufferId); // Sync buffer store's activeBufferId useBufferStore.getState().actions.setActiveBuffer(bufferId); }, - [pane.id, setActivePane, setActivePaneBuffer], + [activatePaneBuffer, pane.id, setActivePane], ); const openFileTreeDropInPane = useCallback( @@ -361,7 +360,7 @@ export function PaneContainer({ pane }: PaneContainerProps) { if (!openedBufferId) return; if (!isEdgeDropZone(effectiveZone)) { - setActivePaneBuffer(pane.id, openedBufferId); + activatePaneBuffer(pane.id, openedBufferId); useBufferStore.getState().actions.setActiveBuffer(openedBufferId); return; } @@ -373,7 +372,7 @@ export function PaneContainer({ pane }: PaneContainerProps) { removeBufferFromPane(pane.id, openedBufferId); setActivePane(newPaneId); - setActivePaneBuffer(newPaneId, openedBufferId); + activatePaneBuffer(newPaneId, openedBufferId); useBufferStore.getState().actions.setActiveBuffer(openedBufferId); } catch (error) { console.error("Failed to open file from file tree drop:", error); @@ -381,7 +380,7 @@ export function PaneContainer({ pane }: PaneContainerProps) { delete window.__fileDragData; } }, - [handleFileOpen, pane.id, removeBufferFromPane, setActivePane, setActivePaneBuffer, splitPane], + [activatePaneBuffer, handleFileOpen, pane.id, removeBufferFromPane, setActivePane, splitPane], ); const openSidebarResourceInPane = useCallback( @@ -409,14 +408,14 @@ export function PaneContainer({ pane }: PaneContainerProps) { if (!targetPane?.bufferIds.includes(bufferId)) { addBufferToPane(targetPaneId, bufferId, true); } else { - setActivePaneBuffer(targetPaneId, bufferId); + activatePaneBuffer(targetPaneId, bufferId); } useBufferStore.getState().actions.setActiveBuffer(bufferId); } catch (error) { console.error("Failed to open sidebar resource from drop:", error); } }, - [addBufferToPane, pane.id, setActivePane, setActivePaneBuffer, splitPane], + [activatePaneBuffer, addBufferToPane, pane.id, setActivePane, splitPane], ); const getCarouselWidthBounds = useCallback(() => { @@ -731,18 +730,7 @@ export function PaneContainer({ pane }: PaneContainerProps) { return; } }, - [ - pane.id, - pane.bufferIds, - buffers, - setActivePane, - addBufferToPane, - moveBufferToPane, - setActivePaneBuffer, - openBuffer, - handleFileOpen, - openSidebarResourceInPane, - ], + [addBufferToPane, handleFileOpen, openSidebarResourceInPane, pane.id, setActivePane], ); const handleCarouselWheel = useCallback( diff --git a/src/features/tabs/components/tab-bar.tsx b/src/features/tabs/components/tab-bar.tsx index ec3f61009..7bf4adcfa 100644 --- a/src/features/tabs/components/tab-bar.tsx +++ b/src/features/tabs/components/tab-bar.tsx @@ -693,7 +693,7 @@ const TabBar = ({ onClick={handleJumpBack} disabled={!canGoBack} variant="ghost" - size="icon-sm" + compact className="shrink-0 rounded-lg text-text-lighter" tooltip="Go Back" tooltipSide="bottom" @@ -707,7 +707,7 @@ const TabBar = ({ onClick={handleJumpForward} disabled={!canGoForward} variant="ghost" - size="icon-sm" + compact className="shrink-0 rounded-lg text-text-lighter" tooltip="Go Forward" tooltipSide="bottom" @@ -752,7 +752,7 @@ const TabBar = ({ type="button" onClick={() => closePane(paneId)} variant="ghost" - size="icon-sm" + compact className="shrink-0 rounded-lg text-text-lighter" tooltip="Close Split" tooltipSide="bottom" @@ -766,7 +766,7 @@ const TabBar = ({ type="button" onClick={handleSplitActivePane} variant="ghost" - size="icon-sm" + compact className="shrink-0 rounded-lg text-text-lighter" tooltip="Split Editor" tooltipSide="bottom" @@ -780,7 +780,7 @@ const TabBar = ({ type="button" onClick={handleTogglePaneFullscreen} variant="ghost" - size="icon-sm" + compact className="shrink-0 rounded-lg text-text-lighter" tooltip={isPaneFullscreen ? "Exit Full Screen" : "Full Screen Editor"} tooltipSide="bottom"