Repository navigation
feat(transfers): progress and cancel for SSH uploads and all remote downloads - #23849
Chihen-Tai wants to merge 44 commits into
Conversation
Runtime-environment imports read the entire file into memory and base64-encoded it before sending, so peak main-process memory was ~2.3x the file size. A 25 MB per-file / 100 MB per-drop ceiling existed only to keep that from exhausting the heap, and it was checked right after lstat — so dropping a 40 MB recording failed instantly with no bytes moved. The socket already lives in main, but the chunking loop lived in the renderer, so the whole base64 payload crossed IPC only to be sliced and passed back through main. Move the byte pump into main, next to the file handle and the socket: one open handle per file, 384 KiB slices (512 KiB on the wire, matching the previous chunk size), sent through the existing files.writeBase64Chunk method. Staging now records byteLength instead of file contents. The renderer keeps deconflict, commit, rollback and cleanup, and never sees file bytes. Peak memory per upload drops to one slice. The ceilings become user safety limits rather than memory guards, raised to 2 GB / 8 GB until the drop UI can show progress and cancel a running upload. No protocol change: files.writeBase64Chunk already existed and was already used by this path for payloads over 512 KB. Small files now use it with append: false, which is exactly what files.writeBase64 does internally, so mixed client/host versions are unaffected. Symlink rejection, O_NOFOLLOW, realpath containment and the stat identity check all move to upload time and now share one handle for the whole file, so every slice comes from the same inode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropping a large file onto a remote terminal gave no feedback until the transfer finished or failed, so a slow upload looked like a hung one. That became easy to hit once streaming removed the 25 MB per-file import cap. The chunk loop now reports bytes acknowledged by the runtime, coalesced to at most one update per megabyte or 100ms so a multi-gigabyte file does not flood IPC. The renderer sums staged byte counts up front and tracks committed files plus the file in flight, so the drop toast shows one figure for the whole drop rather than per file. Client-local only: main to renderer, no RPC params or stream frames change, so there is no wire-compatibility surface. Cancellation is deliberately excluded; it needs an abort path threaded into the streamer plus temp-file cleanup, and belongs with UI that can trigger it. Fixes stablyai#16077 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the single-line progress toast with a panel: a header carrying the aggregate percent and a collapse toggle, then one row per dropped source showing name, transferred-of-total, a bar, percent, and a cancel control. Cancel is real rather than decorative. Each dropped source streams under one id that is both its progress key and its abort handle, so cancelling a row stops that source and leaves the rest of the drop running. The streamer checks between slices, which leaves a well-formed partial temp file for the existing cleanup to remove; nothing lands at the final path. A cancel that arrives between two files of a source is remembered so the next file aborts instead of the click doing nothing, and cancelled sources are excluded from the failure summary because a cancel is not a failure. Both figures in a row scale to the total's unit: formatting them independently rendered "900 / 32.5" when a file crossed a unit boundary. Per STYLEGUIDE, cancel is a back-out rather than a destructive action, so it stays a quiet ghost control with no colour, and the bar uses the existing Progress primitive's `primary` fill rather than a new hue. Splits the drop flow into terminal-runtime-drop-upload and terminal-drop-paste; the host file had grown past the max-lines limit. Fixes stablyai#16077 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cancelling every file removed the session immediately, so the panel vanished mid-gesture with its header still reading "Uploading 2 files" — the one moment the user most needs to see what happened. Finishing now settles the session instead of deleting it. The header switches to the outcome (cancelled, failed, uploaded X of N), the percent and collapse toggle drop away since nothing is moving, and the panel holds for 1.2s before fading out and dismissing itself. Reduced-motion skips the fade but keeps the hold, so the outcome is still readable. Dismissal moves from the drop flow to the panel, which owns the timing; only the error path still tears it down at once. Fixes stablyai#16077 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collapsing the file list left the header hanging where the expanded panel's top edge was, with dead space below it, instead of dropping to where a single-line toast sits. Sonner caches each toast's height and recomputes it only when the toast's element identity changes — `toast.jsx` is in that effect's dependency list. Our panel re-renders internally from its own store, so the cached height stayed at the expanded size and the shrunken content was top-aligned inside it. Collapse state moves into the session store and toggling re-issues toast.custom under the same id, which changes the element identity and makes sonner re-measure. Keeping the flag in the store is what makes the remount safe: local component state would reset on every toggle. Fixes stablyai#16077 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collapsing the file list added a second panel rendering the same session
instead of updating the first, so two identical cards ticked along together.
sonner's toast.custom mints an id, then spreads the caller's options over it:
const id = data?.id || toastsCounter++
this.create({ jsx: jsx(id), id, ...data })
return id
Passing `id: undefined` therefore reaches create() as an explicit undefined,
which mints a second id internally while custom() returns the first. The
stored id never matched the registered toast, so re-issuing under it found no
existing toast and added another.
The id key is now omitted entirely on the first publish and only supplied on
re-issue. A test pins that the first call carries no id key at all, since
passing undefined reads as harmless and is not.
Fixes stablyai#16077
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t jump An unstyled sonner toast is content-sized, so the expanded panel was as wide as its longest file name and the collapsed header shrank to fit "Uploading 2 files". Collapsing therefore moved the left edge as well as the bottom, which read as a jitter rather than a fold. The panel now takes the Toaster's own --width, so both states occupy the same box and only the height changes. File names are clipped inside that box instead of widening it. Fixes stablyai#16077 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collapsing swapped ChevronUp for ChevronDown, which replaces the DOM node, so there was nothing to tween and the arrow flipped in a single frame. One ChevronDown now rotates 180deg over 200ms. The rotation survives the toast re-issue because sonner keys the list item by toast id and only swaps its jsx, so React reconciles the same element rather than remounting it. motion-reduce drops the transition. Fixes stablyai#16077 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Uploading 2 files" reads as a local copy. This panel only ever appears for a drop onto a remote workspace, so it now says "to runtime", matching the wording the old single-line toast used. The cancelled and failed lines stay short: once nothing landed there, the destination is no longer the point. Fixes stablyai#16077 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-ups on the import ceilings. Staging and upload are separate IPC calls, so a source could be replaced or grown in between and the streamer would take the new size as authoritative — transferring a file the ceilings had already rejected, and invalidating the per-drop aggregate. The staged byteLength now rides the upload request and a mismatch is refused before any chunk is sent. formatByteCeiling rounded, so 2 GiB + 1 B rendered "2.0 GB" against a "2 GB" limit and the error read like a broken check. It now rounds up, so anything over the ceiling renders strictly greater than it. Symlink tests are skipped on Windows, where symlink() needs elevation or Developer Mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ty drop
Review follow-ups on the drop panel.
Electron does not order webContents.send against an invoke reply, so a final
progress event can arrive after completeFile and be counted a second time,
jumping the bar ahead. The tracker now only accepts progress inside an
explicit per-file window that completeFile closes.
onStart still fires when every source was skipped, so a drop that staged
nothing flashed an empty panel; it now returns before creating one.
releaseRuntimeUpload was a bare `void` on a promise, which surfaces as an
unhandled rejection if the IPC fails during cleanup — it now carries the same
`.catch(() => {})` the sibling cleanup calls use.
Comments trimmed to the one-line rule in AGENTS.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-progress Picks up the review follow-ups on the base branch: staged-size enforcement, the rounding fix for over-limit errors, and the Windows symlink test guards.
The ceiling lived only in staging, and the upload IPC takes the staged size as optional — so the limit held only for callers that had actually staged. The streamer now checks it itself, independent of whether a staged size was supplied. Both ceilings and their formatter move to runtime-import-limits so staging and the streamer share one definition rather than drifting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-progress Picks up the per-file ceiling check at the streaming boundary and the shared runtime-import-limits module.
…ings truncate() sets a logical length but does not guarantee a sparse file, so on NTFS the over-limit fixtures could allocate two to three gigabytes on Windows CI to prove a comparison. The ceiling tests now mock runtime-import-limits down to kilobytes and use small real files, and the formatter's one-byte-over case moves to a pure unit test on formatByteCeiling where no file is needed at all. Largest fixture in these suites drops from 3 GiB to 3 MiB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…op-upload-progress
…rows Headings now pass count so i18next picks the plural form, with named placeholders. Drop-handler success mocks settle their row before finishing, and the runtime file client harness stubs cancel/release so progress-enabled imports resolve.
[Medium impact] The PR appears safe to merge; no new actionable finding or outstanding previous finding remains. Findings
SummaryThe PR extends the runtime upload progress panel to explorer, editor, and terminal drops, with per-item cancellation. It adds renderer-side download progress and cancellation and routes SSH file downloads through the owning runtime.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the
Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough
Pre-merge checks |
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 8309eb7b-1277-4fc2-9052-9643195067ec
📥 Commits
Reviewing files that changed from the base of the PR and between 31012ae and 60803e540a40f3c2359e5eaf4252812d4053acd0.
📒 Files selected for processing (70)
src/main/ipc/filesystem-download-folder.tssrc/main/ipc/filesystem-download-progress.test.tssrc/main/ipc/filesystem-download-transfers.test.tssrc/main/ipc/filesystem-import-ssh-progress.test.tssrc/main/ipc/filesystem-import-ssh-progress.tssrc/main/ipc/filesystem-import-ssh.tssrc/main/ipc/filesystem-mutations.tssrc/main/ipc/filesystem/filesystem-download-handlers.tssrc/main/ipc/filesystem/remote-download-sessions.test.tssrc/main/ipc/filesystem/remote-download-sessions.tssrc/main/ipc/runtime-upload-cancellation.test.tssrc/main/ipc/runtime-upload-cancellation.tssrc/main/ipc/runtime-upload-file-stream.test.tssrc/main/ipc/runtime-upload-file-stream.tssrc/main/ipc/runtime-upload-progress.test.tssrc/main/ipc/runtime-upload-progress.tssrc/main/providers/filesystem-provider-contract.tssrc/main/providers/ssh-filesystem-download-progress.test.tssrc/main/providers/ssh-filesystem-download.tssrc/main/providers/ssh-filesystem-file-upload.tssrc/main/providers/ssh-filesystem-provider-download-folder.test.tssrc/main/providers/ssh-filesystem-provider-sftp.tssrc/main/providers/ssh-filesystem-provider.test.tssrc/main/providers/ssh-filesystem-provider.tssrc/main/ssh/sftp-upload.tssrc/main/ssh/ssh-connection.tssrc/main/ssh/ssh-relay-session.tssrc/main/ssh/ssh-system-fallback.test.tssrc/main/ssh/system-ssh-file-binary-transfer.tssrc/preload/api/filesystem-api.tssrc/preload/api/fs-bridge.tssrc/renderer/src/components/right-sidebar/FileExplorerRow.actions.test.tsxsrc/renderer/src/components/right-sidebar/file-explorer-row-file-transfer.tssrc/renderer/src/components/right-sidebar/useFileExplorerImport.tssrc/renderer/src/components/terminal-pane/terminal-drop-handler.test.tssrc/renderer/src/components/terminal-pane/terminal-drop-paste.tssrc/renderer/src/components/terminal-pane/terminal-native-file-drop.tssrc/renderer/src/components/terminal-pane/terminal-remote-file-download-open.tssrc/renderer/src/components/terminal-pane/terminal-runtime-drop-upload.tssrc/renderer/src/components/transfer-progress/TransferProgressPanel.tsxsrc/renderer/src/components/transfer-progress/download-progress-panel.test.tssrc/renderer/src/components/transfer-progress/download-progress-panel.tssrc/renderer/src/components/transfer-progress/open-transfer-progress-panel.tssrc/renderer/src/components/transfer-progress/transfer-progress-format.test.tssrc/renderer/src/components/transfer-progress/transfer-progress-format.tssrc/renderer/src/components/transfer-progress/transfer-progress-heading.test.tssrc/renderer/src/components/transfer-progress/transfer-progress-heading.tssrc/renderer/src/components/transfer-progress/transfer-session-state.test.tssrc/renderer/src/components/transfer-progress/transfer-session-state.tssrc/renderer/src/components/transfer-progress/upload-progress-panel.test.tssrc/renderer/src/components/transfer-progress/upload-progress-panel.tssrc/renderer/src/hooks/useGlobalFileDrop.tssrc/renderer/src/i18n/en-runtime-required.jsonsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/runtime/runtime-file-client-download-progress.test.tssrc/renderer/src/runtime/runtime-file-client-external-import.test.tssrc/renderer/src/runtime/runtime-file-client-test-harness.tssrc/renderer/src/runtime/runtime-file-client-types.tssrc/renderer/src/runtime/runtime-file-client.tssrc/renderer/src/runtime/runtime-file-import-client.tssrc/renderer/src/runtime/runtime-file-read-client.tssrc/renderer/src/runtime/runtime-file-upload-client.tssrc/renderer/src/runtime/runtime-upload-progress-tracker.test.tssrc/renderer/src/runtime/runtime-upload-progress-tracker.tssrc/renderer/src/runtime/ssh-upload-progress-client.test.tssrc/renderer/src/runtime/ssh-upload-progress-client.tssrc/renderer/src/web/preload-api/web-filesystem-api.tssrc/shared/remote-download-progress.test.tssrc/shared/remote-download-progress.tssrc/shared/runtime-upload-staging-contract.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 Minor · Check cancellation after the final chunk is acknowledged. · runtime-upload-file-stream.ts:114-127
src/main/ipc/runtime-upload-file-stream.ts:114-127
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winCheck cancellation after the final chunk is acknowledged.
cancelSignalis checked before each slice, but the finalsendChunkuses only the lifetime signal and is followed by no cancellation check. A user cancellation during that send can therefore let the upload resolve. The renderer then callsfiles.commitUploadand reports the file as imported.Suggested fix
if (afterReadStat.mtimeMs !== openedStat.mtimeMs || !isSameFile(afterReadStat, openedStat)) { throw new Error(`File changed during upload: '${displayPath}'`) } + throwIfCancelled(args.cancelSignal) return { byteLength: totalBytes }
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: a26a9a1b-d1af-4283-aa68-02632077ee40
📥 Commits
Reviewing files that changed from the base of the PR and between 60803e540a40f3c2359e5eaf4252812d4053acd0 and a5bac704974829c1212c9bd8bed44512c9151eb5.
📒 Files selected for processing (15)
src/main/ipc/filesystem-download-progress.test.tssrc/main/ipc/filesystem-import-ssh-progress.test.tssrc/main/ipc/filesystem-import-ssh-progress.tssrc/main/ipc/filesystem-mutations.tssrc/main/ipc/filesystem/filesystem-download-handlers.tssrc/main/ipc/runtime-upload-cancel-ipc.test.tssrc/main/ipc/runtime-upload-cancel-ipc.tssrc/main/ipc/runtime-upload-cancellation.test.tssrc/main/ipc/runtime-upload-cancellation.tssrc/main/providers/filesystem-provider-contract.tssrc/main/ssh/sftp-upload.tssrc/renderer/src/i18n/en-runtime-required.jsonsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/runtime/runtime-file-client-download-progress.test.tssrc/renderer/src/runtime/runtime-file-read-client.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/renderer/src/i18n/locales/en.json
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.
There was a problem hiding this comment.
ℹ️ No critical issues — one minor suggestion inline.
Reviewed changes — the top commit 60803e5, which extends #16107's runtime upload panel to every remote transfer.
- SSH upload progress and cancel —
fs:importExternalPaths/fs:resolveDroppedPathsForAgenttake per-sourceuploadIds; main measures each source locally, counts bytes off the SFTP/system-SSH upload streams, reports onfs:uploadProgress, and a cancel deletes a cancelled exclusive partial remote file. - Remote download progress and cancel —
fs:downloadFile/fs:downloadFoldertake adownloadId; main tracks bytes fromfastGet, the system-SSHcatstream and a folder size pre-walk, reports onfs:downloadProgress, andfs:cancelDownloadaborts. - One shared panel — the upload panel, session store, heading and byte formatting move to
components/transfer-progress/and take a direction;openTransferProgressPanel,createUploadProgressPanelandrunRemoteDownloadWithProgresswrap each entry point.
ℹ️ Visual proof is still a TODO
The PR body's "Visual proof" section still reads "TODO: attach the upload, cancelled-upload, download and cancelled-download captures." The checklist requires before/after captures for UI changes; attaching them (or marking the row N/A) would let a reviewer see the cancel/outcome states without driving the dev app.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ Follow-up commits reviewed — no new issues found.
Reviewed changes — the two commits pushed since the initial review (60803e5), both closing cancel-edge gaps:
- Window-scoped upload ids —
scopeRuntimeUploadId(senderId, uploadId)keys cancellation, and a newruntime-upload-cancel-ipc.tsvalidates the request and scopes byevent.sender.id, so one window cannot cancel another window's upload. The remembered-cancel set is capped at 1024 with oldest-first eviction. - Only delete a partial this upload created —
FileUploadSession.uploadFilegainsonRemoteCreated, fired from ssh2WriteStream'sopenevent (verified the pinned v1.17 emitsopenon a successful exclusive open) and from aset -C; : > …marker command on the system-SSH path. Delete-on-cancel now requires it, so an EEXIST loser no longer removes a file another client just wrote. - Cancel after the last byte — an SSH import rolls back an
importedresult when the source was cancelled (covers empty folders and a late click);fs:downloadFileand the chunked runtime download both re-check the signal before promotion; the runtime download loop now throws on a zero-byte non-EOF chunk instead of spinning.
No new defects found in these changes. The earlier note about runtime-upload-progress-tracker.ts's cross-file progress guard is unchanged and still open.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ No critical issues — one stale contract comment to update.
Reviewed changes — the single commit pushed since the prior pullfrog review at 5d12792d62 (60d329f9cd), which gives a folder download a moving bar.
- Folder size walk now runs alongside the download —
downloadFolderViaSftpstartsmeasureRemoteTreewithout awaiting it and reports the total when the walk finishes, dropping a total that arrives after the download settled; the 2 s budget and itsnullreturn are gone, and a failed walk or a throwing observer is swallowed so it never fails the download. - Indeterminate row bar while the total is unknown —
TransferProgressPanelrenders an animated,aria-valuenow-lessrole="progressbar"for an active row with no total, keeping the determinate<Progress>once the total is known and for ended rows. - Tests — the SFTP progress suite replaces the budget case with four cases (total lands mid-transfer, observer throws, slow walk does not block and its late total is dropped, walk failure still downloads), and a new
TransferProgressPanel.test.tsxcovers the animated/determinate/ended bar states.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the two commits pushed since the prior pullfrog review at 60d329f9cd (3689d8d21f, c20a5f85dc), which keep a failed SSH upload's real error and correct a stale contract comment.
- Kept a failed SSH upload's real error —
importOneSourceSshnow reports the failure toimportSshSourceWithProgressthrough anonFailurecallback that runs before any cleanup, and the cancel branch only relabels the result as cancelled when the failure did not precede the click; otherwise the real reason is returned (with any rollback leftovers appended). - Corrected the
onTotalBytescontract — the shared observer JSDoc now says a folder's total may arrive mid-transfer and is never reported after the transfer ends, matching the alongside-the-download walk.
The new tests are non-vacuous: the progress suite cancels from a beforeRemove hook so the click lands inside cleanup, and the rollback suite pins both directions (failure-then-cancel keeps the real reason; cancel-caused failure still reports as cancelled). The prior review's inline nit is addressed by c20a5f85dc, and its thread is resolved.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the single merge pushed since the prior pullfrog review at c20a5f85dc (2595868489), which merges main after #24559 reverted #24401.
- Restored the branch's transfers inline — main's revert deleted
ssh-connection-file-transfers.tsandssh-upload-session-lifetime.ts, and git auto-mergeddownloadFile/openFileUploadSessionto main's plain bodies, silently dropping this branch's download progress/cancel, upload cancel signal and callbacks, andremoveCreatedEntry. The merge restores both methods inssh-connection.tsto this branch's pre-#24401 form, keeping main's unrelated changes. - Verified faithful — the restored methods (
uploadDirectory,downloadFile,openFileUploadSession,writeFile,writeBuffer) match the previously approved03a44db0b7state;pnpm tc:nodeis clean, no references to the deleted modules or work ledger remain, and the SSH transfer suites pass (8 tests).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the main merge 88f930fe74 (main at e2da3a1eb1), the only commit since the prior pullfrog review at 2595868489.
- Merged main's
LocalFileAccessfile-access feature — main's #24489 replacedfilesystem-authwithlocal-file-access-resolutionand addedaccess?: LocalFileAccesstofs:readFile/rename/stat/pathExists/importExternalPaths. All five conflicts (filesystem-mutations.tsargs,filesystem-api.ts/fs-bridge.tsfield + import,runtime-upload-file-stream.ts's removedauthorizeExternalPath,runtime-file-import-client.tsoptions) were resolved by keeping both sides. - Kept the branch's import path intact —
runtime-file-import-clientretains itsprogressoption andrunSshUploadWithProgress, adding main'saccessand...localAccess(...)to the local import call only, so remote destinations stay root-relative. - Extracted the runtime upload tracking —
trackRuntimeUploadForSendermoved from thefs:uploadExternalFileToRuntimehandler intoruntime-upload-cancel-ipc.ts, behavior-identical to the prior inline wiring (null when nouploadId, same throttle/emit, same release).
Verified: pnpm tc:node and pnpm tc:web are clean; filesystem-mutations-runtime-upload, runtime-upload-cancel-ipc, runtime-upload-cancellation, and runtime-file-client-external-import pass (34 tests); pnpm run check:code-quality:changed reports 0 findings. git show --remerge-diff 88f930fe74 confirms every conflict kept both sides and dropped no branch behavior.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Thanks for this, @Chihen-Tai. Quick update: the legacy SSH relay has been removed (#27214, #27215), and SSH workspaces now run on Orca's managed server on the host, so their uploads and downloads go through the same remote-runtime transfer path as #16107 rather than the old desktop-side SFTP import. The idea here (one progress panel with cancel for remote transfers) still applies, but the SSH-specific half would need to be reworked onto that runtime path when this is rebased. |
Resolved against main's owner-routed runtime target (settings -> target), the shared streamExternalFileSlices streamer, and the terminal drop split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d downloads SSH workspaces now run on the host's managed runtime, so their transfers use the same runtime path as remote Orca servers. This builds on stablyai#16107's runtime upload progress instead of the removed desktop-side SFTP import: - stablyai#16107's terminal-drop panel moves to components/transfer-progress and takes a direction; file explorer and editor drops use it too. - Runtime downloads (explorer Download, terminal "download & open") count the chunks the renderer already pulls, show the panel, and cancel between chunks through an AbortSignal; the temp file is discarded and nothing is committed. - SSH workspace files download through the host runtime instead of the SSH filesystem provider, which nothing registers anymore. Folder downloads keep their existing SSH-only path. - Runtime import cancel fixes: directories check the cancel before and after each create, and a source is marked cancelled only when the cancel is what stopped it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88106e8 to
43c7743
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…progress # Conflicts: # src/main/ipc/filesystem/filesystem-mutations.ts # src/renderer/src/components/editor/editor-dropped-file-open.ts # src/renderer/src/components/right-sidebar/file-explorer-row-file-transfer.ts # src/renderer/src/components/terminal-pane/terminal-drop-handler.test.ts # src/renderer/src/components/terminal-pane/terminal-drop-runtime-catalog-owner.test.ts # src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts # src/renderer/src/components/terminal-pane/terminal-drop-upload-report.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/i18n/en-runtime-required.json # src/renderer/src/i18n/locales/en.json # src/renderer/src/runtime/runtime-file-client-external-import.test.ts # src/renderer/src/runtime/runtime-file-import-client.ts # src/renderer/src/runtime/runtime-file-read-client.ts
…ecord selector after main merge Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…progress # Conflicts: # src/renderer/src/runtime/runtime-file-client-test-harness.ts # src/renderer/src/runtime/runtime-file-read-client.ts
| await args.writeSlice(buffer.subarray(0, bytesRead).toString('base64'), offset) | ||
| offset += bytesRead | ||
| // Why: reported after the chunk is acknowledged, so the bar tracks bytes the | ||
| // runtime actually has rather than bytes handed to the socket. | ||
| args.onProgress?.({ sentBytes: offset, totalBytes }) |
There was a problem hiding this comment.
Cancel still publishes the file
If the user clicks Cancel during the final writeSlice, the upload still publishes the file. Cancellation is checked before each slice, but not after the last write returns. The renderer then calls files.commitUpload without another cancel check. This also affects the single write for an empty file.
Check cancellation after the final write, while the file is still temporary, so the upload can discard it instead of publishing it.
| if (controller.signal.aborted) { | ||
| view.panel?.updateRow(rowId, { status: 'cancelled' }) | ||
| view.panel?.settle() | ||
| return { canceled: true } |
There was a problem hiding this comment.
A Cancel click turns every download error into a cancelled result, even when cancellation did not cause the error. For example, if the pending chunk read fails because the connection drops after the click, this catch hides that failure and shows “Cancelled.” Users lose the actual reason the download failed.
Only treat the error thrown by the cancel checkpoint as cancellation. Pass unrelated errors back to the caller.
| <ul className="border-t border-border"> | ||
| {session.rows.map((row) => ( |
There was a problem hiding this comment.
Cancel buttons leave the screen
The expanded upload list has no height limit or scrollbar. Dropping enough separate files makes the bottom-right toast taller than the window, pushing the header and earlier rows above the screen. Those rows’ Cancel buttons—and the collapse button—then cannot be reached with the mouse.
Limit the list height to the available window space and let it scroll.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…progress Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also cover closed-window cleanup of remembered cancels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stopped it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lue tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ancel after the final stat Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps the shared transfer panel and upload progress/cancel here; download progress and cancel follow in a separate PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…progress Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ral extraction expects Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…progress # Conflicts: # src/renderer/src/i18n/en-runtime-required.json

ELI5
When you copy files to or from a remote workspace (an SSH host or a remote Orca server), Orca shows one progress panel: a row per item with bytes, a bar and a Cancel button. Before, only files dropped onto a remote terminal got a panel; explorer and editor drops and all downloads showed nothing until they finished.
What Changed
Before (main + #16107):
After:
Mechanism:
components/transfer-progress/and take a direction (upload/download).createUploadProgressPanelwraps feat(runtime): per-file progress and cancel for files dropped onto a remote workspace #16107'sRuntimeImportProgressHandlers;runRemoteDownloadWithProgresswraps a download. Rows end from the transfer's result, not the click: "Cancelling…" until the result arrives, "remote state unconfirmed" after 15 s.progresstoimportExternalPathsToRuntime, which already reports bytes over feat(runtime): per-file progress and cancel for files dropped onto a remote workspace #16107'sfs:uploadProgressand cancels throughfs:cancelRuntimeUpload. Local workspaces are untouched (their copy has no progress handlers).downloadRuntimeFilealready pulls the file chunk by chunk in the renderer (files.readChunk). It now takes an optional transfer: it reads the size withfiles.stat(failure only means "bytes, no percentage"), counts each appended chunk (throttled to one update per 100 ms), and checks anAbortSignalbefore every chunk and before the final commit. Older servers that answer in one preview reply still show the panel and can be cancelled before the save. No main-process or wire change.pickRemoteDownloadTarget); terminal "download & open" always callsdownloadRuntimeFile, which still falls back to the SSH connection when the context has no runtime owner. Folder downloads keep main's existing SSH-only path.cancelledonly when the cancel is what stopped it, so a failure after the click is still reported.Why
Linked Issue
Fixes #16077 (together with #16107)
Visual Proof
Captured from the dev app (this branch) against an Ubuntu 24.04 SSH host served by its managed runtime (orcad), driven through CDP. Crops show only the panel.
Testing
Automated (ran locally on this branch):
pnpm tc: pass.check:code-quality:changed, oxlint on changed files,check:max-lines-ratchet,check:dead-classes,check:owner-routing-ratchet,verify:localization-catalogs|extraction|coverage: pass.transfer-progress/,runtime/runtime-file-client-*,right-sidebar/FileExplorerRow.actions,terminal-pane/terminal-drop-*,terminal-runtime-drop-upload,terminal-pane-element-file-drop,main/ipc/runtime-upload-*,filesystem-mutations-runtime-upload): pass.src/main/ipc,src/renderer/src/{runtime,lib,components/terminal-pane,components/editor,...}) has 29 failures in files this PR does not touch; the same files fail on cleanmainon my machine (node:sqliteunavailable, aBrowserWindowmock, timing tests).Manual, macOS → Ubuntu 24.04 SSH host on the managed runtime (dev app, driven through CDP):
.orca-upload-*temp is removed on the host; the folder finishes and both files' sha256 match.orca/drops/path is pasted; sha256 matchesNot run by hand: editor drop and terminal "download & open" (same upload/download functions as above, covered by unit tests). The dev app needed
out/orcad-templatefrom a release build to connect at all; without it every host reports "isn't supported" (main's dev setup, unrelated to this PR).AI Disclosure
Implemented and tested with Claude Code (Claude Opus 5.5).
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)🤖 Generated with Claude Code