Skip to content

feat: port the details panel, add a text editor, and stream file reads - #1105

Merged
FSM1 merged 3 commits into
mainfrom
feat/1093-details-panel-text-editor-and-streamed-preview
Aug 6, 2026
Merged

feat: port the details panel, add a text editor, and stream file reads#1105
FSM1 merged 3 commits into
mainfrom
feat/1093-details-panel-text-editor-and-streamed-preview

Conversation

@FSM1

@FSM1 FSM1 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Three issues on one file-actions surface. #1092 and #1080 both rework useFilePreview.ts, which is why they ship together.

Closes #1093
Closes #1092
Closes #1080

What landed

#1093 — the current-version details panel

DetailsDialog used to flatten every node into one label/value list. It now renders a file body or a folder body over the v2 snapshot row:

  • details/DetailsPrimitives.tsxDetailRow, DetailSection, DimValue, CopyableValue, plus the NodeRows/StateRows blocks every kind shares.
  • details/FileDetails.tsx adds the content section (formatted size and the exact byte count); details/FolderDetails.tsx omits it, because a folder carries no content.
  • details/copy-clipboard.ts — reports whether the write landed, so the button never confirms a copy the browser refused.
  • A size the projection has not resolved renders unknown, not undefined.

Every value is the engine's word verbatim; nothing is derived client-side.

#1092 — the text editor

An edit row action appears for exactly the types previewKind calls text. The dialog loads through useFilePreview, so it inherits the allow-list and the byte budget rather than restating them, and saves through the facade write handles: beginWrite({ node }, size) then pushChunk then commitWrite, with abortWrite releasing the reservation on failure. TypeScript frames nothing — the edited bytes go to the engine as bytes.

Modal busy blocks dismissal while the save is in flight, matching the busy-dialog rule. The draft is adopted from the load exactly once, so a snapshot landing mid-edit cannot throw away what has been typed.

#1080 — streamed preview and download

streamTicket (apps/web/src/lib/streamTicket.ts) mints a /stream/ ticket when this tab has a controlling Service Worker, and returns null otherwise, so both callers degrade to the buffered facade read on a browser without one.

  • Download hands the browser the ticket instead of an object URL over a fully materialised Blob. Nothing of the file is held in the tab.
  • Image preview renders straight off the ticket, so the byte budget no longer applies to it.
  • The budget still bounds the paths that genuinely buffer: the PDF preview, the text preview (now a decode budget), and every fallback.

Discrepancies against the issue bodies

Verified against the code, not the issue text.

  1. web: stream file preview and download through MediaService instead of buffering #1080 asks to stream the PDF preview too. It cannot, and this PR does not. packages/client/src/media/range.ts safeMimeType collapses anything that is not audio/*, video/*, or non-SVG image/* to application/octet-stream — deliberately, because a ticket URL is same-origin and top-level navigable. A PDF served through the pipe would arrive opaque and the sandboxed <iframe> would not render it. Loosening that check is a security decision that belongs with range.ts, which this PR does not own. The PDF preview keeps the buffered read and the cap.
  2. web: stream file preview and download through MediaService instead of buffering #1080 asks that the preview dialog's download stop re-reading the file. It still issues a second read, but that read is no longer buffered — it is a ticket the browser pulls. Handing the preview's own image ticket to the save would cross two different content types and two different revoke lifetimes for no real gain.
  3. web: port the current-version details panel onto the v2 snapshot #1093's version-download-guard.ts exclusion is correct, and nothing in v1's FileDetails/FolderDetails beyond the primitives survives the port: every remaining v1 row (fileMetaIpnsName, metadata CID, ECIES-wrapped keys, IPNS sequence numbers) names v1 crypto that v2's snapshot does not carry.
  4. v1's copy-clipboard.ts document.execCommand('copy') fallback is not ported. The app already requires a secure context to register its Service Worker, so the Clipboard API is always available; the fallback would be dead code.
  5. SnapshotChildDescriptor.contentVersion is the one genuine current-version field the panel could show, and does not. ListingRow drops it, and apps/web/src/vault/listing.ts is outside this PR's file set. Worth a follow-up.

Runtime verification

The Puppeteer MCP browser is shared and was being navigated out from under this session mid-run, so the verification was scripted against Playwright's Chromium instead — same browser, deterministic. Driven against a real stack: Kubo, mock IPNS routing and Postgres from docker/docker-compose.yml, an API on :3010, and the VITE_E2E_HOOK dev bundle on :5174, signed in through the introspection hook.

Verified:

  • Details panel. File body renders name / type / node id / modified / size / bytes / queued; folder body renders the same minus size and bytes. copy node id wrote the exact hex the row displays (read back off the clipboard) and flipped the button to its acknowledged state. Computed styles hold: .details-row grid 110px 344px, .details-section colour rgb(0, 102, 68).
  • Streamed image preview. <img src> is /stream/<uuid>. Probing that ticket with Range: bytes=0-15 answered 206 with content-type: image/png, x-content-type-options: nosniff, content-security-policy: default-src 'none'; sandbox, cache-control: no-store. Closing the dialog revoked it — the same URL then answers 404.
  • Streamed download. The anchor the save builds carries href="/stream/<uuid>" and download="cat.png". No Blob, no object URL.
  • Editor gating. No edit item on an image row. On a text row the dialog opens, and the save button stays disabled until there is a draft.

Not verified at runtime, and why:

  • The editor's save round-trip. The local stack never publishes an upload: the engine reaches GET /account/quota and then issues no POST /content/upload, so the op sits at pending: content indefinitely (watched for two minutes across several runs). Reads of unpublished content are refused, and the editor correctly reports the engine's own content unavailable: content not yet published fail-closed. This is a pre-existing local-stack gap — tests/web-e2e/README.md already records that no write-path slice can run here — and nothing in this diff touches upload. The save path is covered by unit tests: the edited bytes reaching beginWrite/pushChunk/commitWrite, the refusal to dismiss mid-save, abortWrite on rejection, and the over-budget refusal before any decrypt.

Manual steps to close that gap once uploads publish locally: upload a .txt, wait for pending: none, then edit, change the body, save, and re-open edit to read the new bytes back.

Worth a separate issue

The streamed read serves content the buffered read refuses. openContentStream/readStream answered 206 with real bytes for a file whose op was still pending: content, while facade.download on the same node rejected with content not yet published. Two read paths over one node disagree about whether it is readable.

Gates

  • pnpm typecheck, pnpm lint, pnpm lint:tracker-refs, pnpm test — green. 199 web tests, 374 client, 178 api. Working tree clean before and after.
  • /simplify — four findings folded in: the file and folder bodies were restating the same four rows (now NodeRows/StateRows, and StateRows.tsx is gone); MAX_BUFFERED_BYTES was exported with no consumer; streamTicket lived inside the download hook and was reverse-imported by the preview hook (now lib/streamTicket.ts, so no hook depends on a sibling hook); the editor pattern-matched a subset of FilePreview and rendered an empty dialog for the rest (now a named refusal). One finding skipped: TextEditorDialog repeats the beginWrite then chunk-loop then commitWrite/abortWrite shape from useDropUpload.ts. Factoring it out means either editing useDropUpload.ts or adding a shared write module, both outside this PR's file set, and the upload loop is entangled with cancellation and op-id tracking that an in-place edit does not need.
  • /security-review — nothing to fold in. The mime a ticket declares always comes from the fixed previewKind allow-list, never sniffed and never off the wire, and that allow-list is a strict subset of what safeMimeType honours; a hostile file body is answered under nosniff plus default-src 'none'; sandbox; the text decode is fatal: true, so non-UTF-8 is refused rather than mangled; and the editor puts no key material anywhere and does no framing of its own.
  • /crypto-privacy-review not run: the diff touches no crates/core primitive, no key or seal material, and no adoption-gate read.

Deliberately not done

Note on the file set

One file lands outside the literal set named for this PR: apps/web/src/lib/streamTicket.ts, created to resolve the /simplify finding above. It is net-new with a unique name, so it cannot collide with the sibling PR the way a shared edit could.

Note

Add text editor, stream file reads, and port the details panel in the file browser

  • Adds TextEditorDialog.tsx for in-place editing of text files; writes changes via the engine's streaming write API in 1 MiB chunks, blocks dismissal during save, and preserves the draft on error.
  • Adds an 'edit' menu item in FileBrowserActions.tsx for files recognized as text.
  • Updates useFileDownload.ts and useFilePreview.ts to prefer Service Worker streaming tickets over buffered downloads; falls back to buffered path when streaming is unavailable.
  • Ports the details panel to type-specific FileDetails and FolderDetails components with copyable name/node ID fields, dimmed unknown values, and contentVersion surfaced from listing rows.
  • Behavioral Change: files with unprojected sizes are now refused for preview with an explicit error; downloads and image previews use streaming by default when the Service Worker is present.

Macroscope summarized c5939a2.

Summary by CodeRabbit

  • New Features

    • Added text-file editing with preview, draft tracking, save, cancel, and validation states.
    • Improved file downloads and image previews with streaming support and automatic fallback handling.
    • Enhanced file and folder details with clearer metadata, status information, and copyable identifiers.
    • Added improved loading, saving, refusal, and error feedback.
  • Bug Fixes

    • Improved cleanup of temporary download and preview resources.
    • Prevented dismissing the editor during an active save operation.
  • Tests

    • Added coverage for editing, streaming, previews, downloads, clipboard actions, and details displays.

FSM1 added 2 commits August 6, 2026 01:19
Details panel #1093: DetailsDialog now renders a file or folder body built
from the v2 snapshot row, with copy-to-clipboard on the identifying fields
and an explicit empty state for a size the projection has not resolved.

Text editor #1092: an `edit` row action for the preview allow-list's text
types opens a dialog that loads through the preview path — same allow-list,
same byte budget — and saves through the facade write handles. TypeScript
frames nothing: the bytes go to beginWrite/pushChunk/commitWrite as-is.

Streaming reads #1080: a save and an image preview mint a `/stream/` ticket
when this tab has a controlling Service Worker, so the browser pulls ranges
instead of the tab buffering the plaintext. The byte budget now only bounds
what is genuinely buffered.

Closes #1093
Closes #1092
Closes #1080
/simplify: collapse the details bodies onto shared `NodeRows`/`StateRows`
primitives so a file and a folder no longer restate the same four rows, drop
the now-unused `StateRows.tsx`, and stop exporting the preview byte budget
that nothing outside the hook reads.

Altitude: `streamTicket` moves out of the download hook into `lib/` so the
preview hook no longer depends on a sibling hook, and the text editor now
names a non-text load as a refusal instead of rendering an empty dialog.

/security-review found nothing to fold in.
@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: 38 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: c0b449b9-29fc-4e9a-bd62-64f29b2d62e2

📥 Commits

Reviewing files that changed from the base of the PR and between be0a506 and c5939a2.

📒 Files selected for processing (4)
  • apps/web/src/components/file-browser/details/FileDetails.tsx
  • apps/web/src/components/file-browser/details/details.test.tsx
  • apps/web/src/vault/listing.test.ts
  • apps/web/src/vault/listing.ts

Walkthrough

The file browser now uses dedicated details components, media-service streaming with buffered fallbacks, and a text editor that saves through chunked facade writes. Tests cover metadata, clipboard behavior, streaming paths, preview limits, editor eligibility, save failures, and dismissal protection.

Changes

File browser details

Layer / File(s) Summary
Details panel components
apps/web/src/components/file-browser/DetailsDialog.tsx, apps/web/src/components/file-browser/details/*, apps/web/src/styles/dialogs.css
Details rendering now uses dedicated file and folder components. Shared rows handle metadata, queue state, unknown values, and clipboard acknowledgement.
Details validation
apps/web/src/components/file-browser/details/details.test.tsx
Tests cover file and folder fields, empty size values, queue states, and clipboard success or rejection.

Media-service read paths

Layer / File(s) Summary
Media-service preview and download paths
apps/web/src/lib/streamTicket.ts, apps/web/src/hooks/useFilePreview.ts, apps/web/src/hooks/useFileDownload.ts, apps/web/src/components/file-browser/FileBrowserActions.tsx
Previews and downloads use stream URLs when supported and fall back to buffered facade operations. Stream URLs receive lifecycle cleanup.
Streaming validation
apps/web/src/components/file-browser/FileBrowserActions.test.tsx
Tests cover stream tickets, revocation, unknown sizes, unsupported formats, buffered fallbacks, and large image previews.

Text editing

Layer / File(s) Summary
Text editor write flow
apps/web/src/components/file-browser/TextEditorDialog.tsx, apps/web/src/components/file-browser/FileBrowserActions.tsx, apps/web/src/styles/dialogs.css
Text files gain an edit action and dialog. Saves encode the draft, write 1 MiB chunks through the facade, commit successful writes, and abort failed writes.
Editor validation
apps/web/src/components/file-browser/FileBrowserActions.test.tsx
Tests cover type eligibility, writes, save-time dismissal blocking, failed-write handling, and size rejection.

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

Sequence Diagram(s)

sequenceDiagram
  participant FileBrowserActions
  participant TextEditorDialog
  participant Facade
  FileBrowserActions->>TextEditorDialog: open editable text file
  TextEditorDialog->>Facade: beginWrite
  TextEditorDialog->>Facade: pushChunk for each 1 MiB chunk
  TextEditorDialog->>Facade: commitWrite
  Facade-->>TextEditorDialog: write result
  TextEditorDialog-->>FileBrowserActions: close after successful save
Loading

Possibly related PRs

Suggested labels: release:web:feat

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The details and editor objectives are met, but #1080 is incomplete because PDF previews remain buffered and PDF streaming is explicitly excluded. Add MediaService streaming for PDF previews and retain buffering limits only for text and fallback paths.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed All implementation, test, and style changes support the linked details, text editor, or media streaming objectives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the three main changes: porting the details panel, adding text editing, and streaming file reads.
✨ 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/1093-details-panel-text-editor-and-streamed-preview

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 marked this pull request as ready for review August 6, 2026 07:10

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

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

5-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace or remove this implementation comment.

The comment states the timeout duration. It does not state why this duration is needed. State the user-facing rationale, or remove the comment.

As per coding guidelines, “Comments should explain why rather than what.”

🤖 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/details/DetailsPrimitives.tsx` around
lines 5 - 6, Update the comment associated with ACKNOWLEDGED_MS to explain the
user-facing reason for the acknowledgment timeout, or remove the comment if that
rationale is not needed; do not merely restate the 2000 ms duration.

Source: Coding guidelines

apps/web/src/components/file-browser/FileBrowserActions.test.tsx (1)

671-687: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering a multi-chunk write.

Every fixture in this suite is far below the 1 MiB CHUNK_BYTES threshold, so the loop in TextEditorDialog.save always runs exactly one iteration. The offset arithmetic and the slice upper bound are the parts most likely to regress, and no test pins them.

Exporting CHUNK_BYTES and asserting the concatenated pushChunk payloads for a draft that spans two chunks would close the gap.

🤖 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/FileBrowserActions.test.tsx` around
lines 671 - 687, Add coverage for a two-chunk edit in the TextEditorDialog save
flow by exporting and using CHUNK_BYTES, creating a draft larger than one chunk,
and asserting the concatenated pushChunk payloads preserve the full content and
chunk boundaries. Keep the existing facade write and commit assertions intact.
apps/web/src/styles/dialogs.css (1)

191-194: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep a focus indicator that survives forced colors.

outline: none removes the only indicator that forced-colors mode preserves. The border-color substitution is an author color, so a high-contrast user gets no visible focus state on the editor field.

♿ Proposed fix: replace the outline instead of removing it
 .text-editor-field:focus {
-  outline: none;
   border-color: var(--color-text-secondary);
+  outline: var(--border-thickness) solid var(--color-text-secondary);
+  outline-offset: 2px;
 }

The PR notes the editor save round trip was not runtime verified. Please record the manual verification steps for the editor dialog in VERIFICATION.md.

As per coding guidelines: "When application changes affect UI components, styles, layouts, pages, routes, or other user-facing behavior, attempt Puppeteer MCP verification; if unavailable, document manual verification steps and flag the work in VERIFICATION.md."

🤖 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/styles/dialogs.css` around lines 191 - 194, Update the
.text-editor-field:focus rule to preserve a focus indicator in forced-colors
mode by replacing outline removal with a suitable outline-based focus style. Add
manual editor-dialog save round-trip verification steps to VERIFICATION.md,
including opening the dialog, editing, saving, and confirming the persisted
result.

Sources: Coding guidelines, Path instructions

🤖 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/TextEditorDialog.tsx`:
- Around line 46-50: Update the write flow in TextEditorDialog’s
beginWrite/commitWrite path to enforce optimistic concurrency using the version
captured in row.contentVersion. Pass that expected version through the frontend
protocol and Engine::begin_write or validate it during commitWrite, rejecting
stale edits instead of overwriting newer content.

---

Nitpick comments:
In `@apps/web/src/components/file-browser/details/DetailsPrimitives.tsx`:
- Around line 5-6: Update the comment associated with ACKNOWLEDGED_MS to explain
the user-facing reason for the acknowledgment timeout, or remove the comment if
that rationale is not needed; do not merely restate the 2000 ms duration.

In `@apps/web/src/components/file-browser/FileBrowserActions.test.tsx`:
- Around line 671-687: Add coverage for a two-chunk edit in the TextEditorDialog
save flow by exporting and using CHUNK_BYTES, creating a draft larger than one
chunk, and asserting the concatenated pushChunk payloads preserve the full
content and chunk boundaries. Keep the existing facade write and commit
assertions intact.

In `@apps/web/src/styles/dialogs.css`:
- Around line 191-194: Update the .text-editor-field:focus rule to preserve a
focus indicator in forced-colors mode by replacing outline removal with a
suitable outline-based focus style. Add manual editor-dialog save round-trip
verification steps to VERIFICATION.md, including opening the dialog, editing,
saving, and confirming the persisted result.
🪄 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: d2440ab7-c5ab-4061-9fab-c989f912d1a3

📥 Commits

Reviewing files that changed from the base of the PR and between 3d087d6 and be0a506.

📒 Files selected for processing (13)
  • apps/web/src/components/file-browser/DetailsDialog.tsx
  • apps/web/src/components/file-browser/FileBrowserActions.test.tsx
  • apps/web/src/components/file-browser/FileBrowserActions.tsx
  • apps/web/src/components/file-browser/TextEditorDialog.tsx
  • apps/web/src/components/file-browser/details/DetailsPrimitives.tsx
  • apps/web/src/components/file-browser/details/FileDetails.tsx
  • apps/web/src/components/file-browser/details/FolderDetails.tsx
  • apps/web/src/components/file-browser/details/copy-clipboard.ts
  • apps/web/src/components/file-browser/details/details.test.tsx
  • apps/web/src/hooks/useFileDownload.ts
  • apps/web/src/hooks/useFilePreview.ts
  • apps/web/src/lib/streamTicket.ts
  • apps/web/src/styles/dialogs.css

Comment thread apps/web/src/components/file-browser/TextEditorDialog.tsx
The panel is documented as one file's current version and showed none:
toRow dropped SnapshotChildDescriptor.contentVersion, so the value never
reached it. Thread it onto ListingRow and render it under content, dim
'unknown' when the snapshot has not projected it, as size already does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WegkkQ3uhNREerTW4MMeY2
Entire-Checkpoint: f925b23aebf2
@FSM1
FSM1 marked this pull request as ready for review August 6, 2026 09:24
@FSM1
FSM1 merged commit a798ebf into main Aug 6, 2026
32 checks passed
@FSM1
FSM1 deleted the feat/1093-details-panel-text-editor-and-streamed-preview branch August 6, 2026 09:25
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