diff --git a/src/preload/preload-runtime-support.ts b/src/preload/preload-runtime-support.ts
index 424e276418ed..696ff1ecfbff 100644
--- a/src/preload/preload-runtime-support.ts
+++ b/src/preload/preload-runtime-support.ts
@@ -4,7 +4,10 @@ import { createBrowserFindSubscriptions } from './browser-find-subscriptions'
import { registerRendererRestartIpcRelays } from './renderer-restart-wiring'
import { createUpdaterQuitAbortRelay } from '../shared/renderer-restart-preparation'
import { ORCA_UPDATER_QUIT_AND_INSTALL_ABORTED_EVENT } from '../shared/updater-renderer-events'
-import { OS_FILE_DROP_OWNER_ATTRIBUTE } from '../shared/native-file-drop-preparation'
+import {
+ OS_FILE_DROP_BOUNDARY_ATTRIBUTE,
+ OS_FILE_DROP_OWNER_ATTRIBUTE
+} from '../shared/native-file-drop-preparation'
import {
ORCA_INTERNAL_FILE_DRAG_TYPE,
createNativeFileDropPayload,
@@ -89,12 +92,15 @@ function resolveNativeFileDrop(event: DragEvent): NativeDropResolution | null {
return resolveNativeFileDropPath(pathEntries)
}
-function nearestDropBoundaryIsOwner(event: DragEvent): boolean {
+function nearestDropBoundaryIsMigrated(event: DragEvent): boolean {
for (const entry of event.composedPath()) {
if (!(entry instanceof HTMLElement)) {
continue
}
- if (entry.hasAttribute(OS_FILE_DROP_OWNER_ATTRIBUTE)) {
+ if (
+ entry.hasAttribute(OS_FILE_DROP_OWNER_ATTRIBUTE) ||
+ entry.hasAttribute(OS_FILE_DROP_BOUNDARY_ATTRIBUTE)
+ ) {
return true
}
if (entry.hasAttribute('data-native-file-drop-target')) {
@@ -117,7 +123,10 @@ export function installNativeFileDropHandlers(): void {
if (event.dataTransfer && !hasNativeFileDragTypes(event.dataTransfer.types)) {
return
}
- if (hasNativeFileDragTypes(event.dataTransfer?.types) && nearestDropBoundaryIsOwner(event)) {
+ if (
+ hasNativeFileDragTypes(event.dataTransfer?.types) &&
+ nearestDropBoundaryIsMigrated(event)
+ ) {
return
}
event.preventDefault()
@@ -133,7 +142,10 @@ export function installNativeFileDropHandlers(): void {
if (event.dataTransfer?.types.includes(ORCA_INTERNAL_FILE_DRAG_TYPE)) {
return
}
- if (hasNativeFileDragTypes(event.dataTransfer?.types) && nearestDropBoundaryIsOwner(event)) {
+ if (
+ hasNativeFileDragTypes(event.dataTransfer?.types) &&
+ nearestDropBoundaryIsMigrated(event)
+ ) {
return
}
event.preventDefault()
diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneFileDropOwner.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneFileDropOwner.tsx
new file mode 100644
index 000000000000..1be806689931
--- /dev/null
+++ b/src/renderer/src/components/terminal-pane/TerminalPaneFileDropOwner.tsx
@@ -0,0 +1,15 @@
+import { useLayoutEffect } from 'react'
+import {
+ useTerminalPaneFileDropOwner,
+ type TerminalPaneFileDropOwnerArgs
+} from './use-terminal-pane-file-drop-owner'
+
+/** The pane manager creates the body container outside React. */
+export function TerminalPaneFileDropOwner(args: TerminalPaneFileDropOwnerArgs): null {
+ const attach = useTerminalPaneFileDropOwner(args)
+ useLayoutEffect(() => {
+ attach(args.pane.container)
+ return () => attach(null)
+ }, [attach, args.pane.container])
+ return null
+}
diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneHeaderDropSurface.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderDropSurface.tsx
new file mode 100644
index 000000000000..4e3e63da8578
--- /dev/null
+++ b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderDropSurface.tsx
@@ -0,0 +1,46 @@
+import type { ComponentProps } from 'react'
+import { WORKSPACE_FILE_PATH_MIME, WORKSPACE_FILE_PATHS_MIME } from '@/lib/workspace-file-drag'
+import { handleInternalTerminalFileDrop } from './terminal-drop-handler'
+import {
+ useTerminalPaneFileDropOwner,
+ type TerminalPaneFileDropOwnerArgs
+} from './use-terminal-pane-file-drop-owner'
+
+export function TerminalPaneHeaderDropSurface({
+ destination,
+ ...props
+}: ComponentProps<'div'> & { destination: TerminalPaneFileDropOwnerArgs }): React.JSX.Element {
+ const attach = useTerminalPaneFileDropOwner(destination)
+ const isInternal = (types: readonly string[]) =>
+ types.includes(WORKSPACE_FILE_PATH_MIME) || types.includes(WORKSPACE_FILE_PATHS_MIME)
+ return (
+
{
+ if (isInternal(event.dataTransfer.types)) {
+ event.preventDefault()
+ event.dataTransfer.dropEffect = 'copy'
+ }
+ }}
+ onDrop={(event) => {
+ if (!isInternal(event.dataTransfer.types)) {
+ return
+ }
+ event.preventDefault()
+ event.stopPropagation()
+ const manager = destination.managerRef.current
+ if (!manager) {
+ return
+ }
+ void handleInternalTerminalFileDrop({
+ ...destination,
+ manager,
+ paneTransports: destination.paneTransportsRef.current,
+ dataTransfer: event.dataTransfer,
+ paneLeafId: destination.pane.leafId
+ })
+ }}
+ />
+ )
+}
diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx
index e9db4adc4bfd..a5444f44715f 100644
--- a/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx
@@ -9,8 +9,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import type { ManagedPane, PaneManager } from '@/lib/pane-manager/pane-manager'
import type { PtyTransport } from './pty-transport'
import TerminalPaneHeaderOverlay from './TerminalPaneHeaderOverlay'
-import { handleTerminalFileDrop } from './terminal-drop-handler'
-import { resolveNativeFileDropPath } from '../../../../shared/native-file-drop'
import { encodeWorkspaceFilePaths, WORKSPACE_FILE_PATHS_MIME } from '@/lib/workspace-file-drag'
vi.mock('@/components/ui/tooltip', () => ({
@@ -170,6 +168,7 @@ function pressInputKey(
}
afterEach(() => {
+ vi.unstubAllGlobals()
for (const { container, root } of mounted.splice(0)) {
act(() => root.unmount())
container.remove()
@@ -304,9 +303,11 @@ describe('TerminalPaneHeaderOverlay', () => {
function dispatchFileDrag(target: Element, type: 'dragover' | 'drop', internal: boolean): void {
const event = new Event(type, { bubbles: true, cancelable: true })
+ Object.defineProperty(event, 'isTrusted', { value: true })
Object.defineProperty(event, 'dataTransfer', {
value: {
types: internal ? [WORKSPACE_FILE_PATHS_MIME] : ['Files'],
+ files: [new File([], 'file.txt')],
getData: () => encodeWorkspaceFilePaths(['/repo/file.txt']),
dropEffect: 'none'
}
@@ -350,47 +351,18 @@ describe('terminal title drop ownership', () => {
if (!title) {
throw new Error('Title A did not render')
}
- const deliveries: Promise
[] = []
- const legacyCapture = (event: Event): void => {
- if (internal) {
- return
+ vi.stubGlobal('api', {
+ fs: {
+ getPathForFile: () => '/repo/file.txt',
+ prepareDroppedPaths: async ({ paths }: { paths: string[] }) => ({ paths, failures: [] })
}
- event.preventDefault()
- event.stopPropagation()
- const entries = event
- .composedPath()
- .filter((entry): entry is HTMLElement => entry instanceof HTMLElement)
- .map((entry) => ({
- nativeFileDropTarget: entry.dataset.nativeFileDropTarget,
- terminalTabId: entry.dataset.terminalTabId,
- terminalPaneLeafId: entry.dataset.terminalPaneLeafId
- }))
- const resolution = resolveNativeFileDropPath(entries)
- if (resolution?.target === 'terminal') {
- deliveries.push(
- handleTerminalFileDrop({
- manager,
- paneTransports: transports,
- worktreeId: 'wt-1',
- tabId: 'tab-1',
- cwd: '/repo',
- data: { paths: ['/repo/file.txt'], ...resolution }
- })
- )
+ })
+ await act(async () => {
+ if (dragover) {
+ dispatchFileDrag(title, 'dragover', internal)
}
- }
- document.addEventListener('drop', legacyCapture, true)
- try {
- await act(async () => {
- if (dragover) {
- dispatchFileDrag(title, 'dragover', internal)
- }
- dispatchFileDrag(title, 'drop', internal)
- await Promise.all(deliveries)
- })
- } finally {
- document.removeEventListener('drop', legacyCapture, true)
- }
+ dispatchFileDrag(title, 'drop', internal)
+ })
expect(sends[0]).toHaveBeenCalledExactlyOnceWith('/repo/file.txt ', 'driving')
expect(sends[1]).not.toHaveBeenCalled()
expect(activate).not.toHaveBeenCalled()
diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx
index f3b405f6b8e5..fef4e48ae591 100644
--- a/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx
@@ -11,10 +11,10 @@ import type { ManagedPane, PaneManager } from '@/lib/pane-manager/pane-manager'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
-import { WORKSPACE_FILE_PATH_MIME, WORKSPACE_FILE_PATHS_MIME } from '@/lib/workspace-file-drag'
import { isImeCompositionKeyDown } from '@/lib/ime-composition-keyboard-event'
import type { PtyTransport } from './pty-transport'
-import { handleInternalTerminalFileDrop } from './terminal-drop-handler'
+import { TerminalPaneHeaderDropSurface } from './TerminalPaneHeaderDropSurface'
+import { makePaneKey } from '../../../../shared/stable-pane-id'
export type PaneTitleOverlayRect = {
left: number
@@ -145,12 +145,11 @@ export default function TerminalPaneHeaderOverlay({
}
return (
- onActivatePaneTitleInteraction(pane.id) : undefined
}
- onDragOver={(event) => {
- if (
- event.dataTransfer.types.includes(WORKSPACE_FILE_PATH_MIME) ||
- event.dataTransfer.types.includes(WORKSPACE_FILE_PATHS_MIME)
- ) {
- event.preventDefault()
- event.dataTransfer.dropEffect = 'copy'
- }
- }}
- onDrop={(event) => {
- if (
- !event.dataTransfer.types.includes(WORKSPACE_FILE_PATH_MIME) &&
- !event.dataTransfer.types.includes(WORKSPACE_FILE_PATHS_MIME)
- ) {
- return
- }
- event.preventDefault()
- event.stopPropagation()
- const manager = managerRef.current
- if (!manager) {
- return
- }
- void handleInternalTerminalFileDrop({
- manager,
- paneTransports: paneTransportsRef.current,
- worktreeId,
- tabId,
- cwd,
- dataTransfer: event.dataTransfer,
- paneLeafId: pane.leafId
- })
- }}
onContextMenuCapture={(event) => onPaneTitleContextMenu(event, pane.id)}
style={{
left: overlayRect.left,
@@ -407,7 +374,7 @@ export default function TerminalPaneHeaderOverlay({
>
)}
-
+
)
})}
diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx
index 76e5bf88d0ae..ec7ece552523 100644
--- a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx
@@ -1,4 +1,6 @@
import { createPortal } from 'react-dom'
+import { TerminalPaneFileDropOwner } from './TerminalPaneFileDropOwner'
+import { makePaneKey } from '../../../../shared/stable-pane-id'
import TerminalSearch from '@/components/TerminalSearch'
import { DaemonActionDialog } from '@/components/shared/useDaemonActions'
import { AgentSessionContinuationDialog } from '@/components/agent-session-continuation/AgentSessionContinuationDialog'
@@ -123,7 +125,7 @@ export function TerminalPaneSurface({