Skip to content

feat: add the vault browser selection model and bound the session check - #1122

Merged
FSM1 merged 4 commits into
mainfrom
feat/vault-selection-and-bounded-session-check
Aug 7, 2026
Merged

feat: add the vault browser selection model and bound the session check#1122
FSM1 merged 4 commits into
mainfrom
feat/vault-selection-and-bounded-session-check

Conversation

@FSM1

@FSM1 FSM1 commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Two file-disjoint items on the #642 web track.

Closes #1091
Closes #1087
Part of #642

1091 — the vault browser selection model

v2 had no selection at all: the per-row context menu covered the four mutations, so nothing in apps/web tracked a set of chosen rows. This adds one.

  • Selection state (apps/web/src/vault/selection.ts) — per-row toggle, select-all from the list header, clear. It is UI-owned state carrying node ids only, per the blueprint's UI state law: the selection is held as hex keys and the selected rows are read back out of the listing, so a row the engine has retired leaves the selection with it rather than lingering as a phantom in the count. A routed folder change starts over.
  • SelectionActionBar — rebuilt against ListingRow, not v1's FolderChild. Count, clear, and batch download / move / delete; download only appears when the selection contains a file.
  • Batch dispatchuseVaultActions.move and .remove now take a set of nodes and dispatch one facade command per node, sequentially, under a single busy/error envelope. A batch is not a command of its own, and there is no client-side listing patching — the snapshot reports which of them the engine took. Every node is attempted even after one is refused, and the first refusal is what surfaces.
  • Batch download reuses useFileDownload.
  • The move and delete dialogs now take a set of rows rather than one; the row menu drives the same dialogs with a single-row set, so batch and single share one code path instead of growing a second pair of dialogs. useFolderPicker correspondingly excludes every selected subtree instead of a single one — a batch move never offers one of the moved folders as its own destination.

1087 — an unauthenticated deep link that hangs

FilesPage gated its redirect on isReady, which only became true once Core Kit settled. If Core Kit is unconfigured or its network is unreachable, isReady stayed false forever and an unauthenticated tab rendered // CHECKING SESSION... indefinitely. It was never a security hole — the vault stayed gated on isAuthenticated — but the tab had no recovery path and no error.

An else branch cannot catch a promise that never settles, so the fix is a bound rather than a branch:

  • CoreKitProvider gives the mount-time restore a deadline and reports a 'checking' | 'ready' | 'unavailable' status. Silence past the deadline becomes unavailable — a verdict, not a stage on the way to ready. A restore that lands after the deadline still promotes the tab back to ready, so the tab self-heals rather than staying poisoned.
  • The deadline is generous, so it does not race a slow-but-working restore; a tab genuinely still checking keeps showing progress and does not redirect early.
  • useAuth exposes isSignedOut: the check settled signed out, or it could never be made. FilesPage gates the redirect on that instead of on isReady.
  • "Still checking" and "could not check" no longer render identically — the unresolvable case redirects to the front door, which names the reason and offers the way in. The wallet method is deliberately not offered as a Core-Kit-down fallback: siweLogin authenticates an account but does not cold-start a vault, so it would be a path that fails.

The two smoke assertions weakened by this bug are restored: the spec now asserts unconditionally that an unauthenticated deep link lands on the front door, and it fails if the redirect regresses.

This overlaps #914 (derive auth state from the engine instead of a tab-local store), which would subsume it — the root cause is that route authorisation depends on a third-party SDK's readiness rather than on the engine's own session state. #914 needs a new facade auth event and is not scheduled; this hang is live today, so it ships as the small fix and #914 can replace the mechanism wholesale later.

Tests

New behaviour, all in gates that already block a merge:

  • apps/web/src/routes/FilesPage.test.tsx (Test gate) — the stalled-provider case, asserted with the restore stalled rather than merely absent: a restore() that never settles still reaches the front door once the deadline passes, and a check that is genuinely in flight keeps showing progress and does not redirect early. Verified non-vacuous — the stall test fails when the deadline is moved out of reach.
  • apps/web/src/components/file-browser/FileBrowserActions.test.tsx (Test gate) — toggling rows updates the count and clear empties it; select-all takes and gives back the listing; batch delete dispatches one facade.delete per selected node; batch move dispatches one facade.relink per selected node, all to the picked destination; batch download reads every selected file and leaves folders alone; selection resets across a folder change.
  • tests/web-e2e/tests/smoke.spec.ts (Web E2E Smoke gate) — the restored unconditional redirect assertion.

No web unit tests were added for anything the SDK already covers.

Verified at runtime

Driven in Chrome against a live stack — local API, Postgres, the hermetic /routing/v1 record store — with the dev bundle's e2e hook cold-starting a real vault.

  • /files with no session and no Core Kit config redirects to / and the front door states why (browser absent, path /).
  • Selecting two files and a folder renders 2 files, 1 folder selected, the toolbar exposes download / move / delete / clear, selected rows highlight, and the header checkbox goes indeterminate.
  • Batch move: the dialog titles itself move 2 files, 1 folder, offers only the three non-selected folders as destinations (the selected folder is excluded from its own destination list), and on confirm all three nodes leave the source listing and the selection clears.
  • Selecting two rows and then opening another folder clears the selection.
  • Batch delete: the prompt reads delete 2 files, 1 folder and everything inside?, and confirming empties the folder and clears the selection.
  • With both rows selected, deleting one through its own row menu prompts delete "alpha" and everything inside it? and leaves the bar reading beta selected — the narrowed retire, confirmed in the browser and not only in the unit gate.
  • The [..] row's icon and a listed row's icon share an x-offset to the pixel, and the header checkbox aligns with the row checkboxes.

The stalled Core Kit case is asserted in the unit gate rather than in the browser — reaching it at runtime needs a configured Web3Auth client whose network is then held open, which would put a third-party dependency inside a merge gate.

Gates

pnpm install, pnpm typecheck, pnpm lint, pnpm test, pnpm lint:tracker-refs — all exit 0.

Review gates

/simplify (reuse, simplification, efficiency, altitude), /security-review, and /crypto-privacy-review all ran on main...HEAD. The crypto gate ran because the diff touches the session/trust boundary, even though it adds no crypto.

Security: no findings. The bound is fail-closed in the only direction it can move — isSignedOut is a strict conjunct on !isAuthenticated, so it can only ever fire the redirect more often, and unavailable always carries a null session so it can never satisfy isReady. Vault content is still gated on isAuthenticated, unchanged.

Crypto/privacy: no crypto in the diff, no key/seed/login-secret exposure, and the handoff path is untouched. The hex/Uint8Array split was checked rather than assumed: ListingRow.key (hex) is only ever compared against other hex, and every facade call takes ListingRow.id (Uint8Array).

Folded in:

  • A command raised from one row's context menu no longer wipes an unrelated multi-selection. The selection now retires exactly the rows a command acted on, and the new test fails under the previous blanket clear.
  • Dropped the useCallback wrappers and the toNodeId indirection the selection hook did not need, and collapsed its four spellings of "all selected" into one.
  • Hoisted the indeterminate ref callback, so the header checkbox stops detaching and re-attaching on every list render.
  • Sized the selection column from one CSS variable rather than three hand-matched literals.
  • Trimmed the comments that restated the code beneath them.

Pushed back on, with evidence:

  • "Replace the parent row's placeholder span with CSS padding." Applied and measured: it mis-aligns the [..] row by exactly the row's flex gap, because padding does not participate in it. Re-deriving the gap inside a calc() is the fragile version. Reverted to a flex-item placeholder, which inherits the gap by construction, with the width now coming from the shared variable — which was the reviewer's real complaint.
  • "Keep the session handle on the unavailable verdict so logout() stays reachable." Sound in principle, unreachable in fact: unavailable and isAuthenticated cannot coexist (authentication requires ready, and the status never returns to unavailable afterwards), so no logout can be dispatched in that state. Nulling the session is also the honest encoding — on the deadline path the restore has not settled, which is exactly what the field's own doc says null means.

Deliberately out of scope:

  • Bounding the live stream-ticket set that a large batch download can open — filed as web: bound the live stream-ticket set a batch download can open #1126, blocked by this issue, since the fix has to distinguish a finished transfer from one still being read and that is a change to useFileDownload's ticket lifetime, not to selection.
  • React.memo on the list rows. The two reviews pull opposite ways here — memoizing wants stable callback identities, simplifying wants fewer of them — and there is no measured problem on a listing bounded by one folder's direct children. Left alone rather than adding machinery for it.
  • Retrying a partially-refused batch re-dispatches the whole captured set. Narrow enough (it needs a confirm click after an error) that guarding it would add a concept for less than it costs.

Summary by CodeRabbit

  • New Features

    • Added multi-select support for files and folders.
    • Added bulk download, move, delete, and clear-selection actions.
    • Added select-all controls, selection counts, and selected-row highlighting.
    • Improved deletion and move dialogs with multi-item descriptions.
    • Failed batch operations remain selected for retry.
  • Bug Fixes

    • Unauthenticated deep links now redirect to the front door.
    • Session restoration timeouts now show an unavailable state and prevent indefinite loading.

Note

Add vault browser multi-select with batch move, delete, and download actions

  • Adds a selection model in selection.ts with useSelection and describeRows, tracking selected rows per folder and resetting on navigation.
  • Adds a SelectionActionBar toolbar for batch Download, Move, Delete, and Clear actions, disabled while an operation is in flight.
  • Updates FileList and FileListItem with per-row checkboxes and a select-all header; selected rows are visually highlighted without triggering navigation.
  • Updates useVaultActions to run move and delete in batches, reporting accepted nodes via BatchOutcome and retiring only accepted nodes on partial failure.
  • Bounds the CoreKit session restore to a 10s deadline in CoreKitProvider; sets status 'unavailable' on timeout and exposes isSignedOut from useAuth so FilesPage can redirect to / without waiting for a ready state.

Macroscope summarized 158760c.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 97fb7895-0bea-4c8d-b4a6-86a54b0da1e5

📥 Commits

Reviewing files that changed from the base of the PR and between 2ec86e5 and 158760c.

📒 Files selected for processing (21)
  • apps/web/src/auth/CoreKitProvider.tsx
  • apps/web/src/auth/useAuth.ts
  • apps/web/src/components/file-browser/ConfirmDeleteDialog.tsx
  • apps/web/src/components/file-browser/FileBrowserActions.test.tsx
  • apps/web/src/components/file-browser/FileBrowserActions.tsx
  • apps/web/src/components/file-browser/FileList.tsx
  • apps/web/src/components/file-browser/FileListItem.tsx
  • apps/web/src/components/file-browser/MoveDialog.tsx
  • apps/web/src/components/file-browser/ParentDirRow.tsx
  • apps/web/src/components/file-browser/SelectionActionBar.tsx
  • apps/web/src/hooks/useFolderPicker.test.tsx
  • apps/web/src/hooks/useFolderPicker.ts
  • apps/web/src/hooks/useVaultActions.ts
  • apps/web/src/routes/FilesPage.test.tsx
  • apps/web/src/routes/FilesPage.tsx
  • apps/web/src/styles/vault-actions.css
  • apps/web/src/test/authFakes.tsx
  • apps/web/src/vault/selection.ts
  • tests/web-e2e/README.md
  • tests/web-e2e/page-objects/files.page.ts
  • tests/web-e2e/tests/smoke.spec.ts
💤 Files with no reviewable changes (1)
  • tests/web-e2e/page-objects/files.page.ts
🚧 Files skipped from review as they are similar to previous changes (18)
  • apps/web/src/hooks/useFolderPicker.test.tsx
  • apps/web/src/test/authFakes.tsx
  • apps/web/src/routes/FilesPage.tsx
  • apps/web/src/vault/selection.ts
  • tests/web-e2e/tests/smoke.spec.ts
  • apps/web/src/components/file-browser/MoveDialog.tsx
  • apps/web/src/components/file-browser/FileListItem.tsx
  • apps/web/src/styles/vault-actions.css
  • apps/web/src/routes/FilesPage.test.tsx
  • apps/web/src/components/file-browser/ParentDirRow.tsx
  • apps/web/src/components/file-browser/FileList.tsx
  • apps/web/src/components/file-browser/ConfirmDeleteDialog.tsx
  • apps/web/src/components/file-browser/SelectionActionBar.tsx
  • apps/web/src/auth/useAuth.ts
  • tests/web-e2e/README.md
  • apps/web/src/components/file-browser/FileBrowserActions.tsx
  • apps/web/src/hooks/useFolderPicker.ts
  • apps/web/src/auth/CoreKitProvider.tsx

Walkthrough

The PR adds multi-row file selection with batch actions and updates Core Kit restoration to expose bounded readiness states. Authentication now redirects signed-out deep links, including unresolved restoration cases. Tests cover selection, batch commands, timeout handling, and redirects.

Changes

Vault browser selection

Layer / File(s) Summary
Selection model and controls
apps/web/src/vault/selection.ts, apps/web/src/components/file-browser/FileList.tsx, apps/web/src/components/file-browser/FileListItem.tsx, apps/web/src/components/file-browser/ParentDirRow.tsx, apps/web/src/styles/vault-actions.css
The file browser tracks selected rows, supports row and select-all controls, resets selection when the folder changes, and renders selection styles.
Batch actions and command dispatch
apps/web/src/components/file-browser/SelectionActionBar.tsx, apps/web/src/components/file-browser/FileBrowserActions.tsx, apps/web/src/components/file-browser/MoveDialog.tsx, apps/web/src/components/file-browser/ConfirmDeleteDialog.tsx, apps/web/src/hooks/useFolderPicker.ts, apps/web/src/hooks/useVaultActions.ts, apps/web/src/components/file-browser/FileBrowserActions.test.tsx, apps/web/src/hooks/useFolderPicker.test.tsx
Batch download, move, and delete operations process selected rows. Vault commands dispatch once per node. Dialogs describe single and multiple selections. Tests cover batch outcomes and selection behavior.

Authentication readiness

Layer / File(s) Summary
Core Kit status and auth state
apps/web/src/auth/CoreKitProvider.tsx, apps/web/src/auth/useAuth.ts, apps/web/src/test/authFakes.tsx
Core Kit exposes checking, ready, and unavailable statuses. Restore timeout handling derives Auth.isSignedOut.
Deep-link redirect behavior
apps/web/src/routes/FilesPage.tsx, apps/web/src/routes/FilesPage.test.tsx, tests/web-e2e/README.md, tests/web-e2e/page-objects/files.page.ts, tests/web-e2e/tests/smoke.spec.ts
FilesPage redirects when isSignedOut. Tests cover stalled restoration, active restoration, and unauthenticated /files navigation.

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

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 64.29% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both primary changes: the vault browser selection model and the bounded session check.
Linked Issues check ✅ Passed Selection features satisfy [#1091], and bounded restore states with deep-link redirect satisfy [#1087].
Out of Scope Changes check ✅ Passed All changes directly support the selection model, batch actions, or bounded session restoration objectives.
✨ 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/vault-selection-and-bounded-session-check

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
FSM1 force-pushed the feat/vault-selection-and-bounded-session-check branch from 7641407 to bfc79de Compare August 6, 2026 23:37
@FSM1
FSM1 marked this pull request as ready for review August 7, 2026 00:29

@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: 2

🧹 Nitpick comments (1)
apps/web/src/components/file-browser/SelectionActionBar.tsx (1)

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

Replace implementation-restating comments.

Lines 5, 7, and 15 repeat prop values and render behavior. Remove these comments unless they record a domain constraint that the type cannot express.

As per coding guidelines, comments must “explain why rather than what” and avoid “duplicated happy-path narration.”

🤖 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/components/file-browser/SelectionActionBar.tsx` around lines 4 -
15, Remove the implementation-restating comments from SelectionActionBarProps,
including the descriptions of rows, busy, and the batch commands. Keep the prop
declarations and any comments that document domain constraints not expressible
by the type.

Source: Coding guidelines

🤖 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/FileBrowserActions.tsx`:
- Around line 67-71: Track the in-progress state of downloadSelection
independently, set it for the full batch duration, and include it with
actions.busy when supplying the action bar’s busy prop. Ensure the state is
cleared after completion, including failures, so duplicate downloads and
conflicting actions remain disabled only while the batch is running.

In `@apps/web/src/hooks/useVaultActions.ts`:
- Around line 34-42: Update perNode to preserve per-node outcomes by returning
accepted and refused node IDs instead of throwing only the first refusal; update
FileBrowserActions.closeOnSuccess to retire only accepted rows, keep refused
rows selected, and surface refused-node failures. Ensure host UI state is
derived from the command result and no refusal is silently discarded.

---

Nitpick comments:
In `@apps/web/src/components/file-browser/SelectionActionBar.tsx`:
- Around line 4-15: Remove the implementation-restating comments from
SelectionActionBarProps, including the descriptions of rows, busy, and the batch
commands. Keep the prop declarations and any comments that document domain
constraints not expressible by the type.
🪄 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: 6227dc76-f329-45c9-ade0-506f54301935

📥 Commits

Reviewing files that changed from the base of the PR and between 77ad6a7 and bfc79de.

📒 Files selected for processing (21)
  • apps/web/src/auth/CoreKitProvider.tsx
  • apps/web/src/auth/useAuth.ts
  • apps/web/src/components/file-browser/ConfirmDeleteDialog.tsx
  • apps/web/src/components/file-browser/FileBrowserActions.test.tsx
  • apps/web/src/components/file-browser/FileBrowserActions.tsx
  • apps/web/src/components/file-browser/FileList.tsx
  • apps/web/src/components/file-browser/FileListItem.tsx
  • apps/web/src/components/file-browser/MoveDialog.tsx
  • apps/web/src/components/file-browser/ParentDirRow.tsx
  • apps/web/src/components/file-browser/SelectionActionBar.tsx
  • apps/web/src/hooks/useFolderPicker.test.tsx
  • apps/web/src/hooks/useFolderPicker.ts
  • apps/web/src/hooks/useVaultActions.ts
  • apps/web/src/routes/FilesPage.test.tsx
  • apps/web/src/routes/FilesPage.tsx
  • apps/web/src/styles/vault-actions.css
  • apps/web/src/test/authFakes.tsx
  • apps/web/src/vault/selection.ts
  • tests/web-e2e/README.md
  • tests/web-e2e/page-objects/files.page.ts
  • tests/web-e2e/tests/smoke.spec.ts
💤 Files with no reviewable changes (1)
  • tests/web-e2e/page-objects/files.page.ts

Comment thread apps/web/src/components/file-browser/FileBrowserActions.tsx
Comment thread apps/web/src/hooks/useVaultActions.ts Outdated
@FSM1
FSM1 marked this pull request as draft August 7, 2026 00:37
FSM1 and others added 3 commits August 7, 2026 10:12
Two file-disjoint web items.

Selection: a per-row and select-all selection model over the listing, a
SelectionActionBar with batch download, move and delete, and one facade
command per selected node dispatched through useVaultActions. Selection is
held as hex keys and read back through the listing, so a row the engine has
retired leaves the selection with it; a routed folder change starts over.
The move and delete dialogs now take a set of rows instead of one, which is
also how the row menu drives them, and the folder picker excludes every
selected subtree rather than a single one.

Session check: an unresolved Core Kit restore now has a deadline, so a tab
deep-linking to /files while the provider never answers is returned to the
front door instead of rendering CHECKING SESSION forever. A restore that
lands after the deadline still promotes the tab back to ready, and the front
door names why the check could not be made. The e2e assertion weakened by
the hang is restored to its unconditional form.

Closes #1091
Closes #1087
Part of #642

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WegkkQ3uhNREerTW4MMeY2
…n work

- Retire only the rows a command acted on instead of the whole selection, so
  a command raised from one row's menu leaves an unrelated multi-selection
  alone; covered by a test that fails under the previous blanket clear.
- Drop the useCallback wrappers and the toNodeId indirection the selection
  hook did not need, and unify its four spellings of all-selected into one.
- Hoist the indeterminate ref callback so the header checkbox stops
  detaching and re-attaching on every list render.
- Size the selection column from one CSS variable, and keep the parent row's
  placeholder a flex item so it inherits the row gap rather than restating it.
- Trim the comments that restated the code they sat above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WegkkQ3uhNREerTW4MMeY2
A batch move or delete dispatched every node, then reported one boolean.
A partly refused batch therefore retained every dialog row and every
selected row, so retrying the still-open dialog re-dispatched the nodes
the engine had already accepted. move and remove now report which nodes
were accepted, and the browser retires exactly those from both the
selection and the dialog while the refusal still reaches the banner.

The batch download loop was not reflected in the action bar's busy
state, so a second click started a second loop over the same rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WegkkQ3uhNREerTW4MMeY2
Entire-Checkpoint: cbbf6121fada
@FSM1
FSM1 force-pushed the feat/vault-selection-and-bounded-session-check branch from 1dd5707 to 1bdd2ca Compare August 7, 2026 08:13
@FSM1
FSM1 marked this pull request as ready for review August 7, 2026 08:14
runBatch kept its own copy of the busy/error handling, and cleared busy
outside a finally. Routing it through dispatchOrFail leaves one path with
one guarantee; the batch adds only the per-node accounting.

Entire-Checkpoint: 9a12f38082f4
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@FSM1
FSM1 merged commit 99a88ed into main Aug 7, 2026
33 checks passed
@FSM1
FSM1 deleted the feat/vault-selection-and-bounded-session-check branch August 7, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant