Skip to content

web: classify a recoverable engine refusal instead of blanking the vault browser #1069

Description

@FSM1

What happened

#1006 asked for a stream-ceiling refusal to read as recoverable rather than as a generic failure. It was closed by PR #1065 (4820773ad), which delivered the engine and packages/client halves only. The vault browser's consumer was never written, so the behaviour #1006 describes still does not exist for a user browsing a folder.

This is not a regression. It is a residual: the plumbing landed, and the vault browser is the consumer that still does not read it.

Verified state on main (3d087d673)

The engine classifies the refusal and the client carries the classification to the UI boundary:

  • crates/engine/src/facade.rs:1368-1398StreamSlot::acquire enforces MAX_OPEN_STREAMS and refuses with a distinct, recoverable error rather than a generic one.
  • apps/web/src/engine/snapshotStore.ts:12-17code?: string on the failure type, documented verbatim as "carrying the engine's stable code so the UI can classify it."
  • apps/web/src/engine/snapshotStore.ts:154EngineRequestError is unwrapped into { message, code }.

The upload surface now reads the code (landed with #1071):

  • apps/web/src/hooks/useDropUpload.ts:237 captures error.code onto the upload row.
  • apps/web/src/components/file-browser/UploadListItem.tsx:32 branches on it — upload.code === 'overBudget' renders as transient rather than terminal red.

The vault browser still does not — this is the whole of what remains:

  • apps/web/src/components/file-browser/FileBrowser.tsx:20-24 renders every failure identically, a flat <p role="alert">{error.message}</p>. It never looks at error.code.
  • FileBrowser.tsx:14 gates the listing on const settled = !isLoading && error === null, so any error hides the file list (:33, :42) and the upload drop target (:32). A recoverable refusal blanks the vault browser exactly as a fatal one does.

The net effect is unchanged: a user who opens one file too many sees their entire file listing disappear, replaced by a raw error string with no indication that retrying will work.

Scope

  1. Classify on code, not on presence of an error. Split the failure surface into recoverable and fatal. FileBrowser.tsx should branch on the code that snapshotStore already provides — the field exists, is documented for this purpose, and UploadListItem already sets the precedent for consuming it.
  2. Keep the last-known-good listing on a recoverable failure. Decouple settled from error === null so a recoverable refusal renders as a notice over the existing rows rather than instead of them. A fatal error may keep the current blanking behaviour.
  3. Make the notice actionable. A recoverable refusal is retryable by construction — the ceiling clears when a stream closes — so it needs a retry affordance, not just a message.
  4. Do not invent a code vocabulary in the web layer. The codes are the engine's (crates/wasm/src/host.rs maps each EngineError variant to its stable string); consume those and fail closed — treat an unrecognised one as fatal — so a new engine code cannot silently render as recoverable.

Gate

  • A stream-ceiling refusal renders as a recoverable notice with the file rows still on screen — asserted on the rows, not only on the notice, so a blanked listing cannot pass.
  • A fatal failure still blanks the listing and renders as an error. The two paths are distinct and separately asserted.
  • An unrecognised code is treated as fatal, proving the classification is closed rather than open.
  • Retrying from the recoverable notice re-drives the pull.

web unit. tests/web-e2e/ now exists (#809, #1078), so the classification can additionally be asserted at the e2e tier once its write-path blocker clears (#1096).

Notes


Corrected 2026-08-06 against main @ 3d087d673. Stale before this edit: the claim that rg '\.code\b' apps/web/src returns no hits (it now returns useDropUpload.ts:237 and UploadListItem.tsx:32, landed with #1071, so the upload half of the classification is delivered); every FileBrowser.tsx line reference (the file was rewritten by #1071/#1081); the facade.rs line range for StreamSlot; and the note claiming apps/web has no e2e suite (tests/web-e2e/ landed with #809/#1078). The core defect — FileBrowser.tsx blanking the listing on any error — is unchanged and still real.

Metadata

Metadata

Assignees

No one assigned

    Labels

    comp:webpackages/client + apps/web — WASM host, browser seams, React UIv2-buildv2 rewrite build slice

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions