Files
orca/src/shared/native-file-drop.ts
T
Brennan Benson e4cd14d914 Remove legacy OS file-drop routing (STA-6940 PR6/6) (#26385)
* feat(file-drop): add element owner preparation plumbing

* Fix terminal and chat file drop destination ownership

* fix runtime terminal drop ownership and queued chat retries

* fix(file-drop): preserve feedback acceptance and destination ordering

* fix(file-drop): attach chat and composer files at the drop surface

* fix(file-drop): preserve project destinations and live chat availability

* fix(file-drop): keep path resolution in filesystem namespace

* test(file-drop): use filesystem path bridge in PR3 fixtures

* fix(file-drop): bind terminal drops to pane elements

* fix(file-drop): compare terminal pane identity across public views

* test(file-drop): align composer lifecycle with element-owned drops

* Fix interrupted chat composition and refuse unused quick-create drops

* Type the quick-create drop regression transfer

* fix(file-drop): let the explorer, project sidebar, tab strip and editor own OS drops

STA-6940 PR5. The file explorer tree, the project sidebar, each editor
group's tab strip and its editor area now take OS file drops on their own
element through the shared owner hook, with identity from their own render
(the explorer's shown workspace and target row, the tab strip's and editor
group's worktree and group) instead of the active worktree. The explorer and
sidebar broadcast subscribers and their legacy drop markers are deleted;
editor-open moves out of useGlobalFileDrop into editor-dropped-file-open,
which the legacy unmarked-chrome route still uses until PR6.

* fix(file-drop): keep editor delivery tied to its destination group

* fix(file-drop): guard delayed opens and share floating ownership

* fix(file-drop): remove legacy OS drop routing (STA-6940 PR6/6)

* test(file-drop): finish owner test and comment cleanup

* Clarify dropped-file default editor destination

* Retain UI test namespace for terminal resume coverage
2026-10-07 23:11:37 -07:00

116 lines
3.4 KiB
TypeScript

import { measureClipboardTextByteLength } from './clipboard-text'
export const ORCA_INTERNAL_FILE_DRAG_TYPE = 'text/x-orca-file-path'
export const NATIVE_FILE_DROP_MAX_PATHS = 256
export const NATIVE_FILE_DROP_MAX_PATH_BYTES = 256 * 1024
export type NativeFileDropRejectedPayload = {
byteLength: number
pathCount: number
reason: NativeFileDropRejectionReason
target: 'rejected'
/** Why every file in a `temp-copy-failed` drop went uncopied, when they share one reason. */
commonReason?: NativeFileDropCopyFailureReason
}
// Why tokens: the renderer owns the localized copy; main never sends display text.
export const NATIVE_FILE_DROP_COPY_FAILURE_REASONS = [
'missing',
'permission-denied',
'changed',
'out-of-space',
'storage-unavailable',
'storage-not-private',
'copy-failed',
'timed-out',
'busy',
// Too big to copy, so agents in terminals and composers can't be given it.
'too-large',
'storage-full'
] as const
export type NativeFileDropCopyFailureReason = (typeof NATIVE_FILE_DROP_COPY_FAILURE_REASONS)[number]
/** What path validation alone can reject a drop for. */
export type NativeFileDropSizeRejectionReason = 'paths-too-large' | 'too-many-paths'
/** `unresolved-paths`: the OS handed us file items no path could be read from
* (promised/virtual files), which used to be swallowed with no feedback.
* `temp-copy-failed`: main could not copy a macOS drag-temp file; only main sends it. */
export type NativeFileDropRejectionReason =
| NativeFileDropSizeRejectionReason
| 'unresolved-paths'
| 'temp-copy-failed'
export type NativeFileDropPathValidation =
| { byteLength: number; pathCount: number; status: 'accepted' }
| {
byteLength: number
pathCount: number
reason: NativeFileDropSizeRejectionReason
status: 'rejected'
}
function getDataTransferTypes(
types: Iterable<string> | ArrayLike<string> | null | undefined
): string[] {
return types ? Array.from(types) : []
}
export function hasNativeFileDragTypes(
types: Iterable<string> | ArrayLike<string> | null | undefined
): boolean {
const values = getDataTransferTypes(types)
return values.includes('Files') && !values.includes(ORCA_INTERNAL_FILE_DRAG_TYPE)
}
export function validateNativeFileDropPaths(
paths: readonly string[],
options: {
maxPathBytes?: number
maxPaths?: number
} = {}
): NativeFileDropPathValidation {
const pathCount = paths.length
const maxPaths = options.maxPaths ?? NATIVE_FILE_DROP_MAX_PATHS
if (pathCount > maxPaths) {
return {
byteLength: 0,
pathCount,
reason: 'too-many-paths',
status: 'rejected'
}
}
const maxPathBytes = options.maxPathBytes ?? NATIVE_FILE_DROP_MAX_PATH_BYTES
let byteLength = 0
for (const path of paths) {
const measurement = measureClipboardTextByteLength(path, {
stopAfterBytes: maxPathBytes - byteLength
})
byteLength += measurement.byteLength
if (byteLength > maxPathBytes) {
return {
byteLength,
pathCount,
reason: 'paths-too-large',
status: 'rejected'
}
}
}
return { byteLength, pathCount, status: 'accepted' }
}
export function createRejectedNativeFileDropPayload(
validation: Extract<NativeFileDropPathValidation, { status: 'rejected' }>
): NativeFileDropRejectedPayload {
return {
byteLength: validation.byteLength,
pathCount: validation.pathCount,
reason: validation.reason,
target: 'rejected'
}
}