Skip to content

feat: wire web file upload through the facade write handles - #1071

Merged
FSM1 merged 4 commits into
mainfrom
feat/873-file-upload-through-facade-write-handles
Aug 5, 2026
Merged

feat: wire web file upload through the facade write handles#1071
FSM1 merged 4 commits into
mainfrom
feat/873-file-upload-through-facade-write-handles

Conversation

@FSM1

@FSM1 FSM1 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Closes #873.

The upload half of the web content path, on top of the write-handle surface packages/client already exposes. No engine or client command is added — this is UI wiring over beginWrite / pushChunk / commitWrite / abortWrite / cancelUpload and the opProgress event stream.

What lands

  • apps/web/src/hooks/useDropUpload.ts — the drive loop. A file is sliced at a 1 MiB boundary and each slice is read and handed to pushChunk in one step, so the buffer is detached by the transfer and no plaintext copy ever reaches a React value. Files run one at a time, because beginWrite reserves the whole version against the staging budget and files started together would otherwise contend for room only one can have.
  • UploadZone.tsx — the drop target, doubling as a file picker. Ignores drags that carry no files, and uses an enter/leave depth counter so crossing a child does not drop the highlight.
  • UploadListItem.tsx — one row per upload, in the same three columns as the listing below it. Determinate bar once the drain reports confirmed blocks, a shimmer only on the row actually being fed.
  • UploadPanel.tsx — owns the rows, so a block-confirmed event repaints them alone rather than the whole folder listing.
  • styles/upload.css — terminal-aesthetic styling carried over from the v1 upload surface, with prefers-reduced-motion honoured.

useFolderNavigation gains one field: the folder id the snapshot reported, which the drop target needs as its write target. It was already computing it to build the breadcrumb trail.

Phases

Rows key on the op id commitWrite resolves with, and every phase past that point is the engine's word:

phase source
staging the client is feeding chunks
queued commitWrite returned an op id
uploading uploadStarted / uploadProgress
uploaded uploadCompleted — blocks on the network, record publishes next; the row retires itself
stalled uploadFailed — one attempt stopped, and the drain retries it
cancelled uploadCancelled, or a handle abandoned before commit
failed a rejected write-handle call, or a deadLetter for the op

uploadFailed is deliberately not terminal: the facade documents an Event::DeadLetter in the same pass as the thing that says the op will never publish, so the row stays open and offers cancel until a dead letter arrives.

Cancel

Cancel before commit sets a flag the run loop reads at its next chunk boundary and aborts the handle; cancel after commit issues cancelUpload(opId). A cancel that lands mid-commit is picked up as soon as the op id exists. A refusal — tooLateToCancel, notAnUpload — is shown on the row without moving it off the upload it is still doing.

Over-budget

A refused write keeps EngineRequestError.code, so the UI renders overBudget in the warning colour with a retry — a ceiling, not a verdict — apart from the settled red of a row that will never publish. A stopped attempt reads the same way, for the same reason. The engine's own message is what distinguishes the six causes, verbatim. See below.

Review gates

/simplify and /security-review both ran on this diff; the second commit is their output.

Security review found three real defects, all fixed and covered by tests:

  • An engine-driven cancel stranded its row. Cancel was gated on the row being active and dismiss on it having failed, so a row the engine reported as uploadCancelled had no control at all and held its File for the life of the mount. Every settled row now retires itself and offers dismiss.
  • The retire timer was uncancellable. uploadCompleted means blocks are on the network, not that the record published, so a deadLetter can legitimately follow — and the timer the completed phase scheduled would then sweep the failed row away. Timers are now per row and cleared whenever a row moves off a settled phase.
  • The unbound-update slot had no owner filter. A foreign op reporting into the single slot could clobber the update a commit was waiting on, leaving a finished upload stuck at queued. It is now a map gated on an open commit window, so nothing accumulates and nothing clobbers.

Simplify's structural findings — the panel extraction, reading the folder id off the snapshot instead of the breadcrumb trail, taking the op id off the job rather than render state, and restricting the shimmer — are in the same commit.

Not done, on purpose

  • Machine-readable over-budget causes. web: wire file upload through the facade write handles #873 asks for the refusal in its three distinct forms. OverBudgetCause in crates/engine/src/facade.rs does separate them, but every variant collapses to the single code overBudget at the wasm boundary in crates/wasm/src/host.rs, so only the prose differs. Splitting them by substring-matching the diagnostic would be exactly the source-text assertion the testing law forbids, and the fix belongs in crates/wasm plus packages/client, which this PR does not own. Filed as engine: carry the over-budget cause across the wasm boundary #1073, with the rendering split as web: render the drain's over-budget hold on the upload row #1075 which it blocks. web: wire file upload through the facade write handles #873's scope bullet has been struck and pointed at that pair, so closing it here does not drop the requirement.
  • The drain's over-budget hold. SnapshotDescriptor.blocked names the op the drain is holding, and nothing in apps/web reads it, so a held op sits at queued with no explanation. Wiring it needs the snapshot store and the upload rows to meet somewhere, which is a design choice bigger than this issue. Filed as web: render the drain's over-budget hold on the upload row #1075.
  • Duplicate-name resolution. The engine suffixes on collision, so no replace dialog is needed, and dialogs are not this PR's surface.
  • Zeroizing the chunk buffer after the engine consumes it — that is packages/client/src/worker/engineHost.ts, tracked separately. Nothing here retains an extra copy.
  • A cap on the drop batch size. Dropping a very large directory renders a row per file. Uploads are serialised and peak heap stays one slice, so there is no byte-wise exhaustion; picking a limit is a product call, not a review fix.

Issue-body corrections

The body is stale in three places, all of them now fixed on main:

It also asks to harvest the v1 components. They were read for visual and interaction continuity, then rewritten: v1 leaned on react-dropzone, a Zustand upload store, and axios cancel tokens, none of which this app has, and its cancel was cosmetic once a batch was in flight.

Gate

web unit — 31 new tests across the hook and both components; the full apps/web suite is 141 green.

Verified in a real browser against the dev server: the drag highlight survives crossing a child, a text-only drag is ignored, a drop delivers the File, the determinate bar measures 42% of its track for a 0.42 fraction, only the sealing row animates, the over-budget and retrying lines render in the warning colour while a dead letter renders red, and a cancelled row offers retry and dismiss.

Note

Add file upload UI with drag-and-drop, progress tracking, and cancellation to the file browser

  • Adds UploadZone, UploadPanel, and UploadListItem components to the file browser, providing drag-and-drop and file picker upload entry points with per-upload progress rows.
  • Implements useDropUpload hook to orchestrate the full upload pipeline: queuing, chunking files into 1MB pieces, staging via beginWrite, committing, tracking progress via engine events, and auto-retiring completed rows.
  • Supports cancel, retry, and dismiss actions per upload, with transient (stalled, over-budget) vs terminal error classification.
  • Extends useFolderNavigation to expose the currently settled folder id, used to enable the drop target only after the routed folder has landed.
  • Risk: early engine progress events arriving before the commit reply are correlated in-memory; any missed events during that window could leave a row stuck in an intermediate state.

Macroscope summarized 9a0fd7d.

Summary by CodeRabbit

  • New Features
    • Added drag-and-drop and file-picker uploads within the current folder.
    • Added upload progress, status indicators, and accessibility-friendly feedback.
    • Added controls to cancel, retry, or dismiss uploads and upload errors.
    • Added support for multiple files, queued uploads, and automatic cleanup of completed items.
  • Bug Fixes
    • Improved handling of nested drag events, cancelled uploads, stalled transfers, and transient errors.
  • Tests
    • Added comprehensive coverage for upload interactions, progress states, retries, cancellation, and failures.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@FSM1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fe6bb829-18e4-44f9-8518-36d2f44d5407

📥 Commits

Reviewing files that changed from the base of the PR and between 450f789 and 9a0fd7d.

📒 Files selected for processing (7)
  • apps/web/src/components/file-browser/FileBrowser.tsx
  • apps/web/src/components/file-browser/UploadListItem.tsx
  • apps/web/src/components/file-browser/UploadPanel.test.tsx
  • apps/web/src/components/file-browser/UploadPanel.tsx
  • apps/web/src/hooks/useDropUpload.test.tsx
  • apps/web/src/hooks/useDropUpload.ts
  • apps/web/src/vault/useFolderNavigation.test.tsx

Walkthrough

Changes

The PR adds folder-scoped uploads. Files can be dropped or selected, staged in chunks, committed through the engine facade, and tracked through progress, cancellation, retry, failure, and dismissal states.

Upload flow

Layer / File(s) Summary
Upload lifecycle and engine coordination
apps/web/src/hooks/useDropUpload.ts, apps/web/src/hooks/useDropUpload.test.tsx
The hook queues files, stages 1 MiB chunks, commits writes, maps engine events, and supports cancellation, retry, dismissal, cleanup, and retirement. Tests cover lifecycle races and failures.
Upload input and row controls
apps/web/src/components/file-browser/UploadZone.tsx, apps/web/src/components/file-browser/UploadListItem.tsx, apps/web/src/components/file-browser/UploadPanel.tsx, apps/web/src/components/file-browser/*.test.tsx
The UI accepts drops and picker selections, displays progress and errors, and exposes upload actions. Tests cover drag handling, selection reset, status rendering, and row actions.
Folder integration and upload presentation
apps/web/src/vault/useFolderNavigation.ts, apps/web/src/components/file-browser/FileBrowser.tsx, apps/web/src/main.tsx, apps/web/src/styles/upload.css
Folder navigation exposes the current folder. FileBrowser renders the upload panel for that folder. The entry point loads upload styles for the upload interface.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UploadZone
  participant UploadPanel
  participant useDropUpload
  participant EngineFacade
  participant UploadListItem
  User->>UploadZone: drop or select files
  UploadZone->>UploadPanel: forward files
  UploadPanel->>useDropUpload: queue files
  useDropUpload->>EngineFacade: stage chunks and commit writes
  EngineFacade-->>useDropUpload: progress and lifecycle events
  useDropUpload-->>UploadListItem: update upload entry
  User->>UploadListItem: cancel, retry, or dismiss
  UploadListItem->>useDropUpload: invoke lifecycle action
Loading

Possibly related issues

Possibly related PRs

Suggested labels: release:web:feat

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: wiring web file uploads through facade write handles.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/873-file-upload-through-facade-write-handles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

FSM1 added 2 commits August 5, 2026 19:04
Drops and file picks now drive beginWrite / pushChunk / commitWrite: each
file is sliced a chunk at a time and the buffer is transferred into the
engine, so no plaintext copy lands in React state.

Per-file rows key on the op id commitWrite returns and render the engine's
own opProgress phases; a dead letter settles the row as terminal, where an
uploadFailed is one attempt the drain retries. Cancel aborts a staging
handle or issues cancelUpload once the op exists, and a refused write keeps
the engine's stable code so an over-budget refusal reads apart from a
terminal failure.

Closes #873
…vertook

Review gates on the upload wiring.

Security: an engine-driven cancel left a row with no control at all — cancel
was gated on the row being active and dismiss on it having failed — so the
row and the File handle behind it were stranded for the life of the mount.
Every settled row now retires itself and offers dismiss. The retire timer is
also cancellable again: a dead letter can follow the blocks landing, and the
timer the completed phase scheduled would otherwise sweep the failed row.
The unbound-update slot is a map gated on an open commit, so a foreign op's
report can neither accumulate nor clobber the update a commit is waiting for.

Simplify: the upload surface moves into UploadPanel so a block-confirmed
event no longer repaints the whole listing; cancel reads the op id off the
job rather than render state; the drop target takes the folder id the
snapshot reports instead of re-deriving it from the breadcrumb trail; and
the shimmer runs only on the row actually being fed. A stopped attempt and
an over-budget refusal now share the warning colour, since neither is the
settled red of a row that will never publish.
@FSM1
FSM1 force-pushed the feat/873-file-upload-through-facade-write-handles branch from 3ac86fa to 450f789 Compare August 5, 2026 17:04
@FSM1
FSM1 marked this pull request as ready for review August 5, 2026 17:48

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ticket requirements check against #873. One scope item from the ticket is not fulfilled in this diff; the rest of the ticket's scope (write-handle drive loop with transferred chunks, per-file progress keyed on the op id, cancel via cancelUpload, the harvested upload surface and stylesheet) is implemented.

Posted via Macroscope — Ticket Requirements

Comment thread apps/web/src/components/file-browser/UploadListItem.tsx
Comment thread apps/web/src/components/file-browser/FileBrowser.tsx Outdated
Comment thread apps/web/src/hooks/useDropUpload.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small notes on apps/web/src/hooks/useDropUpload.ts; everything else looks consistent with the surrounding code.

Posted via Macroscope — Language idioms

Comment thread apps/web/src/hooks/useDropUpload.ts Outdated
Comment thread apps/web/src/hooks/useDropUpload.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/web/src/hooks/useDropUpload.ts (1)

239-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the unused retire dependency.

run does not call retire; it reaches retirement through apply. The extra dependency rebuilds run without cause.

-    [apply, dropOp, engine, patch, retire]
+    [apply, dropOp, engine, patch]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/hooks/useDropUpload.ts` at line 239, Remove the unused retire
dependency from the dependency array associated with run in useDropUpload,
leaving apply and the other dependencies unchanged since retirement is reached
through apply.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/file-browser/FileBrowser.tsx`:
- Line 26: Move the useDropUpload lifecycle out of the folder-conditional
UploadPanel into a component that remains mounted across navigation, passing the
current folder as its write target. Update the rendering so the drop zone
appears only when folder is non-null while upload rows remain rendered
unconditionally, and reset or scope rows when the active folder changes so
uploads from the previous folder are not shown under the new folder.

In `@apps/web/src/hooks/useDropUpload.ts`:
- Around line 23-30: Run the formatter on useDropUpload.ts and apply Prettier’s
required layout to the UploadPhase union type so the lint gate passes.
- Around line 281-292: Update retry in useDropUpload to reset the row through
apply rather than calling patch directly, ensuring the pending retirement timer
is cleared when transitioning back to staging. Preserve the existing
cancellation, unbinding, job reset, and enqueue behavior, and add a test
covering retrying a cancelled committed row before the timer fires, verifying
the row remains and runs again.

---

Nitpick comments:
In `@apps/web/src/hooks/useDropUpload.ts`:
- Line 239: Remove the unused retire dependency from the dependency array
associated with run in useDropUpload, leaving apply and the other dependencies
unchanged since retirement is reached through apply.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 755ca538-4ead-4cdd-bbd0-387583ca69ec

📥 Commits

Reviewing files that changed from the base of the PR and between 21b124d and 450f789.

📒 Files selected for processing (11)
  • apps/web/src/components/file-browser/FileBrowser.tsx
  • apps/web/src/components/file-browser/UploadListItem.test.tsx
  • apps/web/src/components/file-browser/UploadListItem.tsx
  • apps/web/src/components/file-browser/UploadPanel.tsx
  • apps/web/src/components/file-browser/UploadZone.test.tsx
  • apps/web/src/components/file-browser/UploadZone.tsx
  • apps/web/src/hooks/useDropUpload.test.tsx
  • apps/web/src/hooks/useDropUpload.ts
  • apps/web/src/main.tsx
  • apps/web/src/styles/upload.css
  • apps/web/src/vault/useFolderNavigation.ts

Comment thread apps/web/src/components/file-browser/FileBrowser.tsx Outdated
Comment thread apps/web/src/hooks/useDropUpload.ts
Comment thread apps/web/src/hooks/useDropUpload.ts
@FSM1
FSM1 marked this pull request as draft August 5, 2026 17:53
…e retirement timer on retry

An upload panel gated on a non-null folder unmounted whenever the routed
folder had no snapshot yet, taking the in-flight rows, their cancel and
retry controls, and the engine subscription with it. Mount it always and
gate only the drop target, on a settled folder — which also stops a
malformed node route from accepting drops into the root behind its own
"that is not a folder id" error.

`retry` moved a settled row back to `staging` through `patch`, leaving the
retirement timer that phase had scheduled to delete the row and its job
mid-run. Route it through `apply`, which stops the timer for a phase that
is not retiring, and drop the stale `retire` dependency from `run`.

Entire-Checkpoint: aa776a71af92

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor finding; the two items from the previous run (the stale retire dependency and the silent abortWrite catch) are resolved.

Posted via Macroscope — Language idioms

Comment thread apps/web/src/components/file-browser/UploadListItem.tsx Outdated
…phase that reports one

The progress track only renders while the row is active, and `uploaded` is not
an active phase, so its arm of `measured` was unreachable.

Entire-Checkpoint: 246c9b26ac95
@FSM1
FSM1 marked this pull request as ready for review August 5, 2026 18:22
@FSM1
FSM1 merged commit c76eb60 into main Aug 5, 2026
24 checks passed
@FSM1
FSM1 deleted the feat/873-file-upload-through-facade-write-handles branch August 5, 2026 18:23
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.

web: wire file upload through the facade write handles

1 participant