Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
80f0623
feat(file-drop): add element owner preparation plumbing
brennanb2025 Oct 6, 2026
2dcc2a9
Fix terminal and chat file drop destination ownership
brennanb2025 Oct 6, 2026
a0a5923
fix runtime terminal drop ownership and queued chat retries
brennanb2025 Oct 6, 2026
2e3f19a
fix(file-drop): preserve feedback acceptance and destination ordering
brennanb2025 Oct 6, 2026
1146abc
Merge branch 'brennanb2025/sta-6940-pr1-drop-ownership' into brennanb…
brennanb2025 Oct 6, 2026
015f710
fix(file-drop): attach chat and composer files at the drop surface
brennanb2025 Oct 6, 2026
036f0db
fix(file-drop): preserve project destinations and live chat availability
brennanb2025 Oct 6, 2026
7f5a703
fix(file-drop): keep path resolution in filesystem namespace
brennanb2025 Oct 6, 2026
3596fb7
Merge remote-tracking branch 'origin/brennanb2025/sta-6940-pr2-drop-p…
brennanb2025 Oct 6, 2026
e3b891b
test(file-drop): use filesystem path bridge in PR3 fixtures
brennanb2025 Oct 6, 2026
8391981
Merge origin/main into STA-6940 PR2
brennanb2025 Oct 6, 2026
d844eb9
fix(file-drop): bind terminal drops to pane elements
brennanb2025 Oct 6, 2026
fd67741
fix(file-drop): compare terminal pane identity across public views
brennanb2025 Oct 6, 2026
be26b1b
Merge remote-tracking branch 'origin/main' into brennanb2025/sta-6940…
brennanb2025 Oct 7, 2026
15d5b3f
Merge updated STA-6940 PR2 and main into PR3
brennanb2025 Oct 7, 2026
3dde5f1
test(file-drop): align composer lifecycle with element-owned drops
brennanb2025 Oct 7, 2026
ff58070
Merge origin/main into STA-6940 PR3
brennanb2025 Oct 7, 2026
fbb9334
Merge final STA-6940 chat and composer stack into terminal owners
brennanb2025 Oct 7, 2026
ad6d715
Merge origin/main into STA-6940 chat drop migration
brennanb2025 Oct 7, 2026
2c39f25
Fix interrupted chat composition and refuse unused quick-create drops
brennanb2025 Oct 7, 2026
c62129a
Type the quick-create drop regression transfer
brennanb2025 Oct 7, 2026
080123c
Merge remote-tracking branch 'origin/brennanb2025/sta-6940-pr3-chat-c…
brennanb2025 Oct 7, 2026
e9f07e7
Merge origin/main into STA-6940 PR4
brennanb2025 Oct 7, 2026
1e425ad
Merge origin/main into STA-6940 PR4
brennanb2025 Oct 7, 2026
c4825e8
Merge origin/main into STA-6940 PR3 chat drop migration
brennanb2025 Oct 7, 2026
0c95de3
Merge origin/main (main typecheck fix #26097) into STA-6940 PR3
brennanb2025 Oct 7, 2026
eda595d
Merge STA-6940 PR3 (0c95de36943) into PR4
brennanb2025 Oct 7, 2026
b4aa7f4
Merge origin/main (4077fb4a8de) into STA-6940 PR4
brennanb2025 Oct 7, 2026
89ffd1c
Merge origin/main into STA-6940 chat drop migration
brennanb2025 Oct 7, 2026
e0e04d7
Merge current main into STA-6940 chat drop migration
brennanb2025 Oct 7, 2026
204a1b1
Merge refreshed STA-6940 chat drop migration into terminal owners
brennanb2025 Oct 7, 2026
2f4b3e8
Merge main into STA-6940 terminal drop owners
brennanb2025 Oct 7, 2026
474e6ad
Merge main into STA-6940 terminal drop owners
brennanb2025 Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/preload/preload-runtime-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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')) {
Expand All @@ -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()
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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 (
<div
{...props}
ref={attach}
onDragOver={(event) => {
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
})
}}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down Expand Up @@ -170,6 +168,7 @@ function pressInputKey(
}

afterEach(() => {
vi.unstubAllGlobals()
for (const { container, root } of mounted.splice(0)) {
act(() => root.unmount())
container.remove()
Expand Down Expand Up @@ -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'
}
Expand Down Expand Up @@ -350,47 +351,18 @@ describe('terminal title drop ownership', () => {
if (!title) {
throw new Error('Title A did not render')
}
const deliveries: Promise<void>[] = []
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -145,51 +145,18 @@ export default function TerminalPaneHeaderOverlay({
}

return (
<div
key={`pane-title-${pane.leafId}`}
<TerminalPaneHeaderDropSurface
key={makePaneKey(tabId, pane.leafId)}
destination={{ pane, tabId, worktreeId, cwd, managerRef, paneTransportsRef }}
className="pane-title-bar"
data-native-file-drop-target="terminal"
data-terminal-tab-id={tabId}
data-terminal-pane-leaf-id={pane.leafId}
data-pane-prevent-terminal-focus=""
{...(isActivePane ? { 'data-active-pane': '' } : {})}
{...(isChromeless ? { 'data-chromeless': '' } : {})}
{...(isEditing ? { 'data-editing': '' } : {})}
onPointerDownCapture={
title || isEditing ? () => 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,
Expand Down Expand Up @@ -407,7 +374,7 @@ export default function TerminalPaneHeaderOverlay({
</div>
</>
)}
</div>
</TerminalPaneHeaderDropSurface>
)
})}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -123,7 +125,7 @@ export function TerminalPaneSurface({
<div
ref={setContainerRef}
className="absolute inset-0 min-h-0 min-w-0"
data-native-file-drop-target="terminal"
data-os-file-drop-boundary=""
data-terminal-tab-id={tabId}
data-terminal-chat-view={effectiveChatViewMode && activePaneIsChatLeaf ? 'true' : undefined}
data-terminal-layout-leaf-ids={expectedLayoutLeafIdsAttr}
Expand Down Expand Up @@ -165,6 +167,17 @@ export function TerminalPaneSurface({
})
}}
/>
{managedPanes.map((pane) => (
<TerminalPaneFileDropOwner
key={makePaneKey(tabId, pane.leafId)}
pane={pane}
tabId={tabId}
worktreeId={worktreeId}
cwd={cwd}
managerRef={managerRef}
paneTransportsRef={paneTransportsRef}
/>
))}
<TerminalPaneCodexRestartPortals controller={controller} />
<AgentLaunchPaneNoticePortal
refusal={visibleLaunchRefusal}
Expand Down
Loading
Loading