Skip to content

fix(file-drop): deliver terminal files to the pane under the cursor (STA-6940 PR4/6) - #26008

Merged
brennanb2025 merged 33 commits into
mainfrom
brennanb2025/sta-6940-pr4-terminal-owners
Oct 8, 2026
Merged

brennanb2025 merged 33 commits into
mainfrom
brennanb2025/sta-6940-pr4-terminal-owners

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​579 $\color{#cf222e}{\Huge{\mathbf{−}}}$​151 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​428
Prod 15 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​357 $\color{#cf222e}{\Huge{\mathbf{−}}}$​195 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​162

ELI5

An operating-system file drop could reach the active terminal split instead of the split under the cursor because delivery was broadcast to the window and reconstructed from routing attributes. The first draft of this migration also silently refused every real terminal title and body drop: the pane manager returns a new public object on each read, but both acceptance and delivery compared those objects directly. The tests reused the same objects and missed that failure.

This is PR 4 of 6 for STA-6940. A file dropped on pane A's title or body now goes to A while pane B stays active. Nested chat receives its own drops; hidden terminals and gaps between panes receive nothing. Replacing a pane's container while a file is being prepared cancels that delivery, even if the pane's IDs stay the same.

Dependencies: none remain open. PR1 #25749, PR2 #25748 and PR3 #25781 are on main; PR3 merged as squash commit 2803be77f6785f6a14098c4c3187ca6cd1ff99c8. PR5 migrates the remaining explorer, sidebar, tab and editor owners; PR6 removes legacy delivery.

Final main sync (2026-10-07): merge 474e6ad03ae419f5cf4ddafad5f674826d020b2f brings in main a551aa5fe67ec4a6e3dd81fe384a146248b610b7 with a normal merge, without a rebase or force-push. PR4 contained an older copy of PR3's commits, which conflicted with PR3's final reviewed state on main in two attachment implementation files and three chat drop tests. None belongs to PR4's own 22-file change, so all five were resolved to main's version.

All 22 PR4-owned files are byte-identical to the previous PR4 head 2f4b3e83c8fe6173b80738a510ba6c78c2ed5d58. The final diff against the merged main snapshot contains exactly those 22 files; every chat file, including the automatic merges, matches that snapshot. The pane checks, shared title/body ordering and temporary divider boundary are unchanged, and dropScopeKey is absent. This sync adds no user-facing behavior, notices or destinations. PR4 remains a draft and has not been marked ready or merged.

What Changed

  • Register each pane's actual body container and sibling title row through the existing operating-system file owner hook. They share one delivery sequence for the same stable pane key, so preparation and uploads cannot reorder title and body drops.
  • Use one current-pane predicate for acceptance and delivery after preparation. It matches the captured pane against a fresh manager enumeration by pane ID, stable ID for its place in the split layout (leaf ID), and actual container element. Manager and transport-map identity checks and captured transport/pseudo-terminal (PTY) incarnation checks remain in place.
  • Make the terminal DOM fixture use the real collectPublicPanes and toPublicPane functions, returning new public objects on each call. Cover both split-pane titles and bodies, and old connected roots whose manager record has a replacement container before a drop or during preparation.
  • Capture the pane, PTY incarnation, transport, workspace path and execution host during the real drop event, before preparation. Recheck the captured destination before upload and writing.
  • Prepare files with the existing agent consumer, preserving readable macOS screenshot copies. Reuse local shell quoting, Windows Subsystem for Linux (WSL) mapping, SSH upload, runtime upload and host-qualified folder/worktree lookup. Floating terminals retain their local working directory even when another runtime is focused.
  • Remove the terminal broadcast subscription, operating-system pane resolver and legacy terminal drop markers. File-explorer drags retain their typed, provenance-checked lane. Operating-system drops do not change terminal focus.
  • Reuse preload's nearest-boundary release rule for a temporary cancellation-only boundary around terminal bodies. The document guard silently refuses divider/gap drops that would otherwise still open in the unmigrated editor during this stack. Pane and nested chat owners remain the destinations of their own drops.

No new notices or visual elements are added. Explorer, project sidebar, tab strip and editor delivery stay on their current paths.

Why

The browser identifies the element under the cursor, and its render closure identifies the pane. Capturing the destination before preparation prevents a replacement terminal or focused runtime from changing where a pending drop goes. Matching the pane's stable identity and container lets those checks work with the manager's existing public objects; it also distinguishes a replacement pane that reused the same IDs.

Differences from the common pattern:

  • Intended: native capture listeners and nearest registered roots protect nested chat and embedded editors.
  • Intended: main-process screenshot preparation and desktop paths preserve agent-readable files and existing guarded SSH/runtime uploads.
  • Intended: cancellation-only document guards prevent browser file navigation, including in the web build.
  • Temporary: preload's release rule and the cancellation-only terminal boundary remain during migration. STA-6940 PR 6/6 must delete the boundary attribute, shared constant and release check, together with legacy interception, breadcrumb routing and broadcasts.
  • Temporary: web path drops and runtime-chat path attachments retain their existing limitations until the separate file-byte attachment delivery follow-up.

PR5 migrates explorer, project sidebar, tab strips and editor groups. PR6 removes the remaining legacy delivery machinery, including this temporary boundary.

Linked Issue

STA-6940 — PR 4 of 6; this draft does not close the whole issue.

Visual Proof

A separate QA agent ran this branch (head fd67741bc53) as a hidden Orca dev window with an isolated profile and simulated OS file drops through the Chrome DevTools Protocol. Terminal text was read from each pane's screen buffer and the active pane from app state. All 8 scenarios passed: title-bar and body drops on each of two side-by-side panes land only in that pane, a divider drop does nothing, a file name with a space is quoted, a second terminal tab receives only its own drop, the floating terminal receives drops, and a drag from Orca's file explorer still pastes.

1. File dropped on the left pane's title bar while the right pane is active. The path lands in the left pane only; app state shows the right pane is still active.

Title-bar drop lands in its own pane

2. File dropped on the divider between the panes. Nothing is pasted anywhere and no editor tab opens.

Divider drop does nothing

3. Drop on a second terminal tab, then back to the first. The first tab's panes gained nothing from the second tab's drop.

First tab unchanged after a drop on the second tab

4. File dropped on the floating terminal. The path is pasted there; the main panes are unchanged.

Floating terminal drop

5. File dragged from Orca's file explorer onto a pane (the unchanged path for drags that start inside Orca). It is still pasted.

Explorer drag onto a pane

Not covered: a chat nested inside a terminal pane in the real app (DOM tests only), real SSH, WSL and remote-runtime hosts, Linux and Windows.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated

Follow-up verification on macOS:

  • Before the fix: with only the fixture/tests updated and production still at d844eb9b0befd72297efe249bed2b6e319176a38, terminal-pane-element-file-drop.test.tsx ran 18 cases: 12 failed / 6 passed. Both title and body delivery cases failed because the terminal received zero writes; the preparation-replacement cases also confirmed no preparation began. There were no import or missing-API failures.
  • After the fix: the same named file passed 18/18 cases, then passed again in the combined regression run. This checks both acceptance and the later delivery check against fresh public objects.
  • New replacement coverage: connected old title/body roots refuse a new gesture when the container is replaced despite matching pane and leaf IDs; a replacement during delayed preparation also receives no write.
  • Retained coverage: split A's title/body while B stays active; no legacy IPC/broadcast; divider refusal without editor opening; title/body ordering; enabled/disabled nested chat; display-none/inert tabs; replacement PTY; physical-local runtime transport with a different focused runtime; SSH connection capture; local WSL; floating terminals.

The before/after reproduction used this explicitly named file in a Bash array:

files=(src/renderer/src/components/terminal-pane/terminal-pane-element-file-drop.test.tsx)
ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts "${files[@]}"

Current maintenance verification (474e6ad03ae): the original 68 explicitly named files plus PR3's chat drop suites still present on main, conflict-related chat coverage and prior maintenance checks ran together in a 76-file Bash array: 76 passed (76); 677 passed (677). Every file was checked to exist and the array was checked to be non-empty before running Vitest. No test timeouts changed.

Current-head CI: All 37 checks on this head finished: 19 passed / 18 skipped by workflow path rules, with no failures and no reruns. PR Checks run 37703637854 passed on its first attempt, including static analysis/typechecking, all ten unit-test shards, relay integration, macOS and Windows packaging, final verification and the unit-selection report. Mobile verification and the line-count check also passed in their separate workflows.

The exact combined regression command was:

76-file regression command (Bash; includes the original 68 files)
files=(
  src/main/ipc/dropped-path-resolution.test.ts
  src/main/window/attach-main-window-services.test.ts
  src/main/window/dragged-temp-file-copy.test.ts
  src/main/window/dropped-path-preparation.test.ts
  src/main/window/native-file-drop-relay.test.ts
  src/preload/app-restart-checkpoint-routing.test.ts
  src/preload/preload-native-file-drop-release.test.ts
  src/preload/pty-snapshot-capability-ipc.test.ts
  src/preload/ssh-authority-forwarding.test.ts
  src/preload/updater-package-recovery.test.ts
  src/renderer/src/components/NewWorkspaceComposerCard.file-drop.test.tsx
  src/renderer/src/components/NewWorkspaceComposerCard.set-location-warm.test.tsx
  src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx
  src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx
  src/renderer/src/components/NewWorkspaceComposerCard.test.tsx
  src/renderer/src/components/automations/automation-editor-prompt-options.test.ts
  src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts
  src/renderer/src/components/native-chat/native-chat-composer-autogrow.test.tsx
  src/renderer/src/components/native-chat/native-chat-composer-composition.test.tsx
  src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx
  src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx
  src/renderer/src/components/native-chat/native-chat-element-file-drop.test.tsx
  src/renderer/src/components/native-chat/native-chat-external-attachments-destination.test.tsx
  src/renderer/src/components/native-chat/native-chat-floating-file-drop.test.tsx
  src/renderer/src/components/native-chat/native-chat-pane-file-drop.test.tsx
  src/renderer/src/components/native-chat/native-chat-view-file-drop.test.tsx
  src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx
  src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx
  src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.test.ts
  src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx
  src/renderer/src/components/sidebar/sidebar-project-drop.test.ts
  src/renderer/src/components/sidebar/use-feedback-image-drop.test.tsx
  src/renderer/src/components/sidebar/use-workspace-status-drop.test.ts
  src/renderer/src/components/sidebar/workspace-status.test.ts
  src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx
  src/renderer/src/components/terminal-pane/terminal-drop-handler.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-image-path.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-internal-handler.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-local-workspace.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-pane-resolution.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-path-writer.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-runtime-catalog-owner.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-runtime-owner.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-shell.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-transport-owner.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-worktree-path.test.ts
  src/renderer/src/components/terminal-pane/terminal-drop-write-failure.test.ts
  src/renderer/src/components/terminal-pane/terminal-pane-element-file-drop.test.tsx
  src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-active-pty-reporting.test.ts
  src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-file-drop.test.ts
  src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-paste-events.test.ts
  src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-sync-fit-registration.test.ts
  src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-window-focus-recovery.test.ts
  src/renderer/src/hooks/composer-drop-failure-toast.test.ts
  src/renderer/src/hooks/composer-drop-result.test.ts
  src/renderer/src/hooks/composer-native-file-drop.test.ts
  src/renderer/src/hooks/composer-state/attachment-drop-failure.test.tsx
  src/renderer/src/hooks/composer-state/attachment-drop-lifetime.test.tsx
  src/renderer/src/hooks/composer-state/composer-attachment-target.test.ts
  src/renderer/src/hooks/use-os-file-drop-owner.test.tsx
  src/renderer/src/hooks/useGlobalFileDrop.escaping-link.test.tsx
  src/renderer/src/hooks/useGlobalFileDrop.test.ts
  src/renderer/src/lib/os-file-drop-cancellation-guard.test.ts
  src/renderer/src/lib/os-file-drop-preload-integration.test.tsx
  src/renderer/src/web/web-preload-api-composition.test.ts
  src/renderer/src/web/web-preload-api-filesystem.test.ts
  src/shared/native-file-drop.test.ts
  src/renderer/src/components/NewWorkspaceComposerCard.quick-file-drop.test.tsx
  src/renderer/src/components/native-chat/native-chat-composer-field-resume.test.tsx
  src/renderer/src/components/native-chat/native-chat-composer-ime-file-drop.test.tsx
  src/renderer/src/components/native-chat/use-native-chat-file-attachment-actions.test.tsx
  src/renderer/src/hooks/useComposerState.integration.test.ts
  config/scripts/vitest-sqlite-runtime-boundary.test.ts
  src/renderer/src/components/terminal-pane/terminal-pane-client-host.test.ts
  src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts
)
for file in "${files[@]}"; do
  [[ -f "$file" ]] || { echo "Missing test: $file" >&2; exit 1; }
done
[[ ${#files[@]} -gt 0 ]]
[[ ${#files[@]} -eq 76 ]]
ORCA_BACKGROUND_LAUNCH=1 node_modules/.bin/vitest run --config config/vitest.config.ts --maxWorkers=1 "${files[@]}"

Fresh local checks passed:

  • pnpm install --frozen-lockfile; deleted config/tsconfig.*.tsbuildinfo before checks.
  • All 23 checks from orca-ci-checks, including lint, code-quality audits, localization and typechecking; checks for changed code and React components reran successfully after committing the merge.
  • Merge audit: exactly the 22 owned files differ from main, all unchanged from the previous PR4 head; all chat files match main; no dropScopeKey occurrences or unresolved conflict markers.

Restored pnpm's incidental @pnpm/exe addition, leaving no lockfile change. No fixes for unrelated main code, documentation files, user-visible strings or app launches were added. The Visual Proof above remains historical evidence from fd67741bc53; no new Electron, live SSH, WSL or remote-runtime checks were performed locally during this sync.

Review

Reviewed all PR4 production changes for comparisons against public pane objects; the two repaired checks were the only wrapper-identity comparisons. Self-reviewed capture, nested arbitration, sibling ordering, transport replacement, hidden owners, gap refusal, unchanged internal drags and the existing upload/path-mapping boundaries. The title overlay and global-effects files remain smaller and within their line limits.

The 22 files PR4 itself changes
  • src/preload/preload-runtime-support.ts
  • src/renderer/src/components/terminal-pane/TerminalPaneFileDropOwner.tsx
  • src/renderer/src/components/terminal-pane/TerminalPaneHeaderDropSurface.tsx
  • src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx
  • src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx
  • src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx
  • src/renderer/src/components/terminal-pane/terminal-drop-handler.test.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-handler.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-local-workspace.test.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-local-wsl.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-pane-resolution.test.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-pane-resolution.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-runtime-catalog-owner.test.ts
  • src/renderer/src/components/terminal-pane/terminal-drop-target.ts
  • src/renderer/src/components/terminal-pane/terminal-native-file-drop-destination.ts
  • src/renderer/src/components/terminal-pane/terminal-native-file-drop.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-element-file-drop.test.tsx
  • src/renderer/src/components/terminal-pane/use-terminal-pane-file-drop-owner.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-file-drop.test.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts
  • src/renderer/src/lib/pane-manager/pane-display-visibility.ts
  • src/shared/native-file-drop-preparation.ts

Agent skill upstream boundary

  • Not applicable; no upstream skill source or resources are copied.

Notes

PR4 adds no documentation or lockfile changes relative to main. The coordinator approved the temporary boundary so divider drops are refused correctly before the editor migration and the legacy removal. Dependency refreshes come in as normal merges, with no rebase or force-push. This remains a draft and has not been marked ready or merged.

Checklist

  • Focused on terminal panes and title bars
  • Problem, user-facing change, mechanism and approach explained
  • Before/after rendered Electron evidence attached
  • Self-reviewed for correctness, security and performance
  • Cross-platform, SSH/runtime, WSL and folder-workspace impact considered
  • 76-file regression suite, full local static checks and current-head CI results recorded above

…lumbing' into brennanb2025/sta-6940-pr3-chat-composer-feedback
Preserve element-owned drops while integrating the upstream multi-file
paperclip picker and all-or-nothing attachment limit.

Resolve NativeChatComposer through its existing file-drop hook, keep the
host-qualified attachment resolver with the current workspace state type,
retain captured destinations and unmount checks through attachment reads
and uploads, and keep attachment actions picker-only without a broadcast
subscriber. Update picker fixtures and the upstream subscriber test.

Validation: web and Node typechecks, full Oxlint on 39 PR3 files, and
372 tests across 45 explicitly named files pass.
…omposer-feedback' into brennanb2025/sta-6940-pr4-terminal-owners
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary

This PR went through an independent code review, a fix round, a focused re-review of the fix, and a rendered check in a real Orca window. The reviewers and the rendered-check agent did not write the code. Their reports are summarized here because they were kept off GitHub while the work was in progress.

During the build: a staging gap, resolved with a temporary boundary

Once this PR removes the terminal's old drop marker, a drop on the divider between two panes has no owner. Until PR 6 removes the old routing, a drop with no marker is still sent to the editor, so the file would have opened as an editor tab (today it does nothing). A temporary boundary on the outer terminal wrapper keeps that drop on the "not allowed" path: the app releases it, no surface claims it, and the existing guard refuses it silently. It is labelled temporary and listed for removal in PR 6.

First review (head d844eb9b0be): one serious bug

Every real terminal drop was silently refused. The check that a pane still exists compared pane objects by identity (manager.getPanes().includes(pane)), but the pane manager builds new wrapper objects on every call, so the check always failed in the real app, both when accepting the drop and again before delivering it. Because this PR also deletes the old broadcast path, nothing else would have delivered the drop. The tests passed because their stand-in pane manager returned the same objects every time.

The rest was confirmed: each pane's body and title bar own their drops and share one delivery order per pane; identity comes from the pane itself, never the active pane; delivery reuses the existing local, WSL, SSH, remote-runtime and floating-terminal paths; the broadcast subscriber and the old pane-guessing code are deleted; hidden tabs never receive drops; file line limits hold.

Fix (head fd67741bc53), re-review: approved

One shared "is this pane still current" check, matching pane id, leaf id and the actual container element, is used in both places. The tests now use stand-in panes that, like production, are new objects on every call; with the old code they fail (12 failures), and a replaced container is refused.

Rendered check (head fd67741bc53): all 8 scenarios passed

Real drops on real panes arrive, which confirms the fix outside the tests. Screenshots are in the description.

Bringing the branch up to date

The branch was refreshed onto the fixed PR 3 and current main with no conflicts; PR 4's own 22-file change is unchanged. Locally, all three typechecks, the 68-file drop suite (577 tests) and the full repository lint pass. CI's latest run stopped at a typecheck error in src/main/acp/acp-structured-host-restore-failed.test.ts, which is broken on main itself (#26038 removed a function that test still spies on; #26094 fixes it). This PR will be updated with main once that fix lands, for a full CI run.

Not verified

A chat nested inside a terminal pane in the real app (covered by DOM tests), real SSH, WSL and remote-runtime hosts, Linux and Windows.

Resolve NativeChatComposerField imports: keep PR3's flushSync (interrupted
composition settlement) and drop the old ImageOff notice import that main's
notice card replaced. Adapt PR3's IME file-drop test to main's notices and
@-file suggestion props.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Ready for review

PR 3 (#25781) is now merged, so this PR no longer depends on anything open. It was brought up to date with main once, at the end (merge of a551aa5fe67, head 474e6ad03ae):

  • The five conflicts were all in PR 3's chat files, where this branch still carried an older copy of PR 3. main's final version was taken for each. This PR's own 22 files are byte-identical to the reviewed head.
  • Local: CI's static checks (lint, code quality, localization, typecheck) and 76 test files (677 tests) passed.
  • CI on 474e6ad03ae, first attempt: 19 passed, 18 skipped because the changed paths don't need them, none failed. That covers all ten test shards, both packages and the relay integration.

The review summary is in the comment above and the rendered-check screenshots are in the description. Nothing in the review or rendered check changed with this sync.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 8, 2026 00:07
@brennanb2025
brennanb2025 merged commit 939288e into main Oct 8, 2026
61 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/sta-6940-pr4-terminal-owners branch October 8, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant