Skip to content

# feat(file-browser): add PDF support with viewer and file type checks#254

Open
gauravprasadgp wants to merge 8 commits intodaggerhashimoto:masterfrom
gauravprasadgp:master
Open

# feat(file-browser): add PDF support with viewer and file type checks#254
gauravprasadgp wants to merge 8 commits intodaggerhashimoto:masterfrom
gauravprasadgp:master

Conversation

@gauravprasadgp
Copy link
Copy Markdown

@gauravprasadgp gauravprasadgp commented Apr 9, 2026

What

This MR extends the file-browser feature to support PDF rendering and viewing. PDFs can now be opened directly in the UI via the browser's native PDF viewer (iframe), with appropriate file size validation and security headers updated to allow same-origin framing.

Files changed

  • server/middleware/security-headers.ts — Changed X-Frame-Options to 'SAMEORIGIN', updated frame-ancestors to 'self', and added 'self' to frame-src for PDF viewer iframe support.
  • server/routes/file-browser.ts — Added .pdf MIME type, increased file size limit to 50 MB for PDFs (vs. 10 MB for images), and replaced fs.readFile() with fs.createReadStream() to stream large files instead of buffering them in memory. Implemented HTTP Range request support for PDFs (status 206) with proper Content-Range and Accept-Ranges headers. Uses Node's built-in Readable.toWeb() for efficient stream conversion to Web Streams.
  • server/middleware/security-headers.test.ts — Updated tests to expect frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN.
  • src/features/file-browser/types.ts — Added optional viewerVersion property to OpenFile for triggering raw asset viewer remounts.
  • src/features/file-browser/PdfViewer.tsx — New component that renders PDFs using browser's built-in viewer; uses viewerVersion key to trigger remount on external file changes.
  • src/features/file-browser/ImageViewer.tsx — Updated to use viewerVersion key for triggering image reloads on external changes.
  • src/features/file-browser/TabbedContentArea.tsx — Integrated PdfViewer into the tabbed content switcher.
  • src/features/file-browser/FileTreeNode.tsx — Fixed precedence bug in canOpen logic to ensure PDFs are only openable if not a directory; updated file open logic to allow PDFs.
  • src/features/file-browser/hooks/useOpenFiles.ts — Updated hook to handle PDF file loading state, fixed restore logic to skip /api/files/read for image and PDF files, and updated handleFileChanged to bump viewerVersion for raw assets instead of calling reloadFile.
  • src/features/file-browser/utils/fileTypes.ts — Added isPdfFile() utility and PDF_EXTENSIONS constant.

Why

Users can now view PDF files directly in the file-browser UI without leaving the application, improving workflows for document review and reference. This is a non-breaking enhancement that extends existing image-viewing functionality to a new file type.

How

Implementation approach:

  1. File type detection: Added isPdfFile() utility to fileTypes.ts, similar to existing isImageFile() pattern.

  2. Backend changes:

    • Added application/pdf MIME type mapping.
    • Increased file size limits: PDFs allow up to 50 MB (configurable), while images remain at 10 MB.
    • Updated CSP headers to allow iframe embedding from same-origin (required for the browser's built-in PDF viewer).
    • Replaced fs.readFile() with fs.createReadStream() in the /api/files/raw endpoint to stream large files (PDFs, large images) instead of buffering them in memory, avoiding memory spikes for files up to 50 MB.
    • Added HTTP Range request support for PDFs to allow partial content delivery (status 206): parses Range: bytes=start-end header, validates against file size, streams the requested byte range with proper Content-Range and Accept-Ranges: bytes headers. Returns 416 (Range Not Satisfiable) for invalid ranges.
  3. Frontend components:

    • New PdfViewer.tsx renders PDFs via <iframe> pointing to /api/files/raw, mirroring the ImageViewer pattern.
    • TabbedContentArea.tsx conditionally renders PdfViewer for PDF files.
    • FileTreeNode.tsx allows PDFs to be opened (fixed precedence bug in canOpen logic to ensure directories cannot be opened as PDFs).
    • useOpenFiles.ts treats PDFs like images for loading state management and skips text file reads during restore.
    • Both PdfViewer and ImageViewer now use a viewerVersion key on their elements (iframe/img) to trigger remount when external file changes are detected, ensuring the viewer fetches updated content without attempting text-file reloads.
  4. File change detection: Updated handleFileChanged() to detect raw assets (images, PDFs) and bump their viewerVersion instead of calling reloadFile(). This forces the viewer component to remount and refetch the binary resource from /api/files/raw.

  5. Security: Changed X-Frame-Options from 'DENY' to 'SAMEORIGIN', updated CSP frame-ancestors directive to 'self', and added 'self' to frame-src (all required for the native PDF viewer iframe). Frame sources remain restricted to same-origin and trusted trading view origins.

Trade-offs:

  • Used browser's native PDF viewer (via iframe) rather than a library like pdfjs-dist, avoiding additional dependencies and keeping the implementation lightweight.
  • 50 MB PDF limit is reasonable for most documents; can be adjusted in server/routes/file-browser.ts if needed.
  • Implemented file streaming using fs.createReadStream() to handle large files efficiently without buffering entire file contents in memory. Uses Node's built-in Readable.toWeb() for clean, standard stream conversion.
  • Added HTTP Range request support (RFC 7233) for PDFs to enable efficient seeking and partial downloads, improving user experience for large PDF viewers that request byte ranges.
  • PDF restoration and viewing delegates workspace locality checks to the /api/files/raw endpoint, which validates and returns 501 (Not Supported) for remote workspaces. Both images and PDFs are handled consistently at the client level, with endpoint-level validation ensuring proper isolation.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to change)
  • 📝 Documentation update
  • 🔧 Refactor / chore (no functional change)

Checklist

  • npm run lint passes
  • npm run build && npm run build:server succeeds
  • npm test -- --run passes
  • New features include tests
  • UI changes include a screenshot or screen recording

Screenshots

image

Notes for reviewers:

  • The security header changes (frame-ancestors and X-Frame-Options) are necessary for the iframe-based PDF viewer to work; they remain restrictive by allowing only same-origin requests.
  • The PdfViewer component follows the same pattern as ImageViewer for consistency.
  • File size validation distinguishes between PDFs and images to accommodate larger documents.

Summary by CodeRabbit

  • New Features

    • Built-in PDF viewer in the file browser; PDFs open in tabs and via double-click/Enter.
  • Improvements

    • PDFs served with larger per-file size allowance, streamed delivery and partial (Range) loading for efficient access.
    • Raw asset viewers (images/PDFs) now force remounts on updates via a viewer version counter; PDFs skip full read-on-open for faster display.
  • Security

    • Framing policy relaxed to allow same-origin framing (SAMEORIGIN).

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end PDF support (type detection, viewer component, backend streaming with Range support and larger size cap) and relaxes framing protections to allow same-origin embedding (CSP/frame-ancestors and X-Frame-Options changed).

Changes

Cohort / File(s) Summary
Backend security & file streaming
server/middleware/security-headers.ts, server/middleware/security-headers.test.ts, server/routes/file-browser.ts
Relaxed clickjacking policies (frame-src/frame-ancestors'self', X-Frame-OptionsSAMEORIGIN); added .pdf MIME; increased PDF size cap; replaced full-file buffering with streamed Web ReadableStream responses; added nodeStreamToWebStream(...); implemented HTTP Range handling for PDFs (206/416 responses, Content-Range, Accept-Ranges).
File type utilities
src/features/file-browser/utils/fileTypes.ts
Added PDF_EXTENSIONS and exported isPdfFile(name: string): boolean.
UI: file open / restore / remount behavior
src/features/file-browser/FileTreeNode.tsx, src/features/file-browser/hooks/useOpenFiles.ts, src/features/file-browser/types.ts
Treat PDFs like raw assets/images: allow opening without read API, set loading=false early, bump viewerVersion on external changes, avoid read calls for raw assets. Added optional viewerVersion?: number to OpenFile.
PDF viewer & routing in UI
src/features/file-browser/PdfViewer.tsx, src/features/file-browser/TabbedContentArea.tsx
Added exported PdfViewer component rendering an iframe for /api/files/raw and wired TabbedContentArea to render PdfViewer for PDFs.
Viewer remounting tweak
src/features/file-browser/ImageViewer.tsx
Added dynamic key using file.viewerVersion to force remount when viewerVersion changes.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client UI
    participant Tab as TabbedContentArea / PdfViewer
    participant Browser as Browser (iframe)
    participant Server as Server /api/files/raw
    participant FS as Filesystem (fs.createReadStream)

    UI->>Tab: open PDF file
    Tab->>Browser: render iframe src="/api/files/raw?path=...&agentId=..."
    Browser->>Server: GET /api/files/raw (optional Range header)
    Server->>FS: fs.createReadStream(filePath, {start, end})
    FS-->>Server: stream chunks
    Server-->>Browser: stream response (ReadableStream), 206 + Content-Range if partial
    Browser-->>Tab: iframe loads PDF content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I nibble bytes and open the door,
PDFs stream in, a fifty-meg roar.
I hop through frames, same-origin cheer,
Streams and keys make remounts clear.
Tiny paws tap — the viewer's here.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding PDF support with a viewer component and file type detection utilities to the file-browser feature.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all required template sections with clear details about what, why, and how the PDF support feature is implemented.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/file-browser/hooks/useOpenFiles.ts (1)

518-523: ⚠️ Potential issue | 🟠 Major

PDF tabs still won’t restore from persisted state.

Line 518 correctly skips text-fetch for PDFs on open, but restore logic (Line 357+) still uses /api/files/read for all persisted tabs. PDF entries will be dropped on reload because binary reads are non-ok.

🛠️ Suggested fix in restore loop
   for (const path of persistedPaths) {
+    if (isImageFile(basename(path)) || isPdfFile(basename(path))) {
+      files.push({
+        path,
+        name: basename(path),
+        content: '',
+        savedContent: '',
+        dirty: false,
+        locked: false,
+        mtime: 0,
+        loading: false,
+      });
+      continue;
+    }
+
     const { scopedPathKey, token } = nextReadRequestToken(targetAgentId, path);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/file-browser/hooks/useOpenFiles.ts` around lines 518 - 523, The
restore path drops PDF/image tabs because the persisted-restore loop still calls
the text-only endpoint (/api/files/read) for every persisted tab; update the
restore logic that processes persistedOpenFiles/persistedTabs (the loop that
currently calls the read endpoint) to detect isImageFile(basename(path)) ||
isPdfFile(basename(path)) and skip the /api/files/read call for those
entries—just add them back into openFiles via setOpenFiles (or the existing
accumulator) with loading:false and the same metadata (path, name, type) so
binary tabs persist unchanged; leave text-file behavior unchanged and only call
/api/files/read for non-image/non-pdf files.
🧹 Nitpick comments (1)
server/middleware/security-headers.ts (1)

59-60: Consider limiting framing relaxation to /api/files/raw instead of all responses.

This change is valid for PDF embedding, but applying it globally broadens clickjacking exposure for unrelated pages. Route-scoped overrides would preserve tighter defaults elsewhere.

Also applies to: 75-75

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/middleware/security-headers.ts` around lines 59 - 60, The CSP
currently relaxes frame-src globally; change it so the default
Content-Security-Policy keeps "frame-ancestors 'self'" and the restrictive
frame-src without tradingview hosts, and add a route check in the middleware
(inspect the request URL/path in the same middleware function that builds the
headers) to only append the relaxed frame-src string ("frame-src 'self'
https://s3.tradingview.com https://www.tradingview.com
https://www.tradingview-widget.com https://s.tradingview.com") when the request
path is "/api/files/raw". Update both occurrences of the relaxed directive (the
one at the shown diff and the similar one at the other occurrence) so unrelated
routes keep the tighter policy. Ensure the header key (Content-Security-Policy)
is set once per response based on this conditional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/middleware/security-headers.ts`:
- Line 60: Update the tests in server/middleware/security-headers.test.ts to
match the new framing policy: change any assertion expecting
Content-Security-Policy frame-ancestors 'none' to expect "frame-ancestors
'self'" and change any assertion expecting X-Frame-Options: DENY to expect
X-Frame-Options: SAMEORIGIN (or remove the X-Frame-Options check if the
middleware no longer sets it). Locate the assertions that reference
CSP/frame-ancestors and X-Frame-Options in the test and update the expected
header values to the new policy used by the security-headers middleware.

In `@server/routes/file-browser.ts`:
- Around line 793-797: The handler in file-browser.ts currently sets maxSize for
PDFs to ~50MB but still uses fs.readFile which buffers the whole file; change
the logic in the request handler that reads the file (replace the fs.readFile
usage) to stream the file with fs.createReadStream (or stream.pipeline) to the
response instead, set appropriate headers (Content-Type, Content-Length) and
handle stream errors and client aborts; keep the maxSize check using the
existing maxSize variable but stop using full-buffer reads when sending PDF (and
large images) to avoid memory spikes.

In `@src/features/file-browser/FileTreeNode.tsx`:
- Line 70: The precedence bug in FileTreeNode.tsx makes isPdfFile bypass the
directory check; update the canOpen expression so isPdfFile(entry.name) is only
considered when the entry is not a directory — i.e., ensure the entire file-type
checks are grouped under the !isDir condition used in the component (the const
canOpen variable), so directories (isDir === true) cannot be treated as openable
even if their name matches *.pdf.

---

Outside diff comments:
In `@src/features/file-browser/hooks/useOpenFiles.ts`:
- Around line 518-523: The restore path drops PDF/image tabs because the
persisted-restore loop still calls the text-only endpoint (/api/files/read) for
every persisted tab; update the restore logic that processes
persistedOpenFiles/persistedTabs (the loop that currently calls the read
endpoint) to detect isImageFile(basename(path)) || isPdfFile(basename(path)) and
skip the /api/files/read call for those entries—just add them back into
openFiles via setOpenFiles (or the existing accumulator) with loading:false and
the same metadata (path, name, type) so binary tabs persist unchanged; leave
text-file behavior unchanged and only call /api/files/read for non-image/non-pdf
files.

---

Nitpick comments:
In `@server/middleware/security-headers.ts`:
- Around line 59-60: The CSP currently relaxes frame-src globally; change it so
the default Content-Security-Policy keeps "frame-ancestors 'self'" and the
restrictive frame-src without tradingview hosts, and add a route check in the
middleware (inspect the request URL/path in the same middleware function that
builds the headers) to only append the relaxed frame-src string ("frame-src
'self' https://s3.tradingview.com https://www.tradingview.com
https://www.tradingview-widget.com https://s.tradingview.com") when the request
path is "/api/files/raw". Update both occurrences of the relaxed directive (the
one at the shown diff and the similar one at the other occurrence) so unrelated
routes keep the tighter policy. Ensure the header key (Content-Security-Policy)
is set once per response based on this conditional.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: de52e38a-2bc2-404c-860a-2aed322cca91

📥 Commits

Reviewing files that changed from the base of the PR and between a5f7973 and 9bf834e.

📒 Files selected for processing (7)
  • server/middleware/security-headers.ts
  • server/routes/file-browser.ts
  • src/features/file-browser/FileTreeNode.tsx
  • src/features/file-browser/PdfViewer.tsx
  • src/features/file-browser/TabbedContentArea.tsx
  • src/features/file-browser/hooks/useOpenFiles.ts
  • src/features/file-browser/utils/fileTypes.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/routes/file-browser.ts (1)

790-813: ⚠️ Potential issue | 🟠 Major

Handle Range requests for PDF responses.

The native browser PDF viewer typically requests byte ranges for incremental rendering and page seeks. This handler always returns 200 with the full body, so large PDFs will be downloaded end-to-end before later pages can render, and seeks can re-transfer the whole file. Please add Range parsing and return 206 with Content-Range / Accept-Ranges for .pdf.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/file-browser.ts` around lines 790 - 813, Add proper HTTP Range
support for PDF responses: check c.req/c.request.headers.get('range') when ext
=== '.pdf', parse "bytes=start-end", validate against stat.size, and if valid
create a ranged fsSync.createReadStream(resolved, { start, end }), set status
206 and headers 'Content-Range', 'Accept-Ranges: bytes', and 'Content-Length'
(use end-start+1) and stream that (convert with nodeStreamToWebStream if
needed); if no Range header keep the existing full-response behavior, and if the
Range is invalid return 416 with 'Content-Range: bytes */<totalSize>' — reuse
variables resolved, stat, fileStream creation logic, and
nodeStreamToWebStream/webStream conversion for the actual streaming.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/file-browser/hooks/useOpenFiles.ts`:
- Around line 367-386: The PDF/image branch in useOpenFiles (where
isImageFile/isPdfFile and createSnapshotBackedOpenFile are used) needs to opt
out of text reloads: when an external change is detected by handleFileChanged
(which currently calls reloadFile -> /api/files/read), do not call reloadFile
for raw assets; instead trigger a raw-asset refresh path (e.g., increment a
viewerVersion or query param stored on the open-file entry and signal the file
viewer to remount the iframe) so the viewer reloads the binary resource; apply
the same change for the other image/PDF handling location noted around the
second reference (lines ~539-543) so both places use the viewerVersion bump
instead of the text reload flow.

---

Outside diff comments:
In `@server/routes/file-browser.ts`:
- Around line 790-813: Add proper HTTP Range support for PDF responses: check
c.req/c.request.headers.get('range') when ext === '.pdf', parse
"bytes=start-end", validate against stat.size, and if valid create a ranged
fsSync.createReadStream(resolved, { start, end }), set status 206 and headers
'Content-Range', 'Accept-Ranges: bytes', and 'Content-Length' (use end-start+1)
and stream that (convert with nodeStreamToWebStream if needed); if no Range
header keep the existing full-response behavior, and if the Range is invalid
return 416 with 'Content-Range: bytes */<totalSize>' — reuse variables resolved,
stat, fileStream creation logic, and nodeStreamToWebStream/webStream conversion
for the actual streaming.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ccea93e5-c365-4d84-b77c-f709cb552145

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf834e and a6614b0.

📒 Files selected for processing (4)
  • server/middleware/security-headers.test.ts
  • server/routes/file-browser.ts
  • src/features/file-browser/FileTreeNode.tsx
  • src/features/file-browser/hooks/useOpenFiles.ts
✅ Files skipped from review due to trivial changes (1)
  • server/middleware/security-headers.test.ts

… and trigger remount on viewer version change
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 (1)
server/routes/file-browser.ts (1)

833-884: Collapse this to a single stream body path.

Line 836 checks fileStream.readable, but a freshly created fs.ReadStream is already readable, so nodeStreamToWebStream() never runs. Keeping both paths is misleading and leaves dead code to maintain. Prefer one explicit conversion strategy here.

♻️ Proposed simplification
-import { pipeline } from 'node:stream/promises';
+import { Readable } from 'node:stream';
@@
-    const fileStream = fsSync.createReadStream(resolved, { start, end });
-    
-    // Convert Node.js stream to Web Stream for Response compatibility
-    const webStream = fileStream.readable ? fileStream as any : nodeStreamToWebStream(fileStream);
+    const fileStream = fsSync.createReadStream(resolved, { start, end });
+    const webStream = Readable.toWeb(fileStream) as ReadableStream<Uint8Array>;
@@
-/**
- * Convert a Node.js ReadableStream to a Web Stream (ReadableStream).
- * Handles error events and proper cleanup.
- */
-function nodeStreamToWebStream(nodeStream: fsSync.ReadStream): ReadableStream<Uint8Array> {
-  return new ReadableStream({
-    start(controller) {
-      nodeStream.on('data', (chunk: string | Buffer) => {
-        const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
-        controller.enqueue(new Uint8Array(bytes));
-      });
-      nodeStream.on('end', () => {
-        controller.close();
-      });
-      nodeStream.on('error', (err: Error) => {
-        console.error('Stream read error:', err);
-        controller.error(err);
-      });
-    },
-    cancel(reason) {
-      nodeStream.destroy();
-    },
-  });
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/file-browser.ts` around lines 833 - 884, The code currently
branches on fileStream.readable (fileStream) but fs.createReadStream is always
readable, so remove the conditional and always convert the Node ReadStream to a
web-compatible ReadableStream via nodeStreamToWebStream(fileStream); update the
Response to use that single webStream variable and remove the unreachable branch
and any dead checks (e.g., the fileStream.readable check), keeping the
nodeStreamToWebStream function as the single conversion strategy and ensuring
error handling/cleanup remains intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/file-browser/hooks/useOpenFiles.ts`:
- Around line 367-386: The PDF shortcut branch treats PDFs as viewable
everywhere but PdfViewer uses /api/files/raw which is only allowed for local
workspaces; update the isPdfFile check to require local-workspace support (e.g.,
add a condition like && isLocalWorkspace() or workspace.supportsLocalFiles)
before restoring a PDF tab so that non-local/gateway workspaces fall back to the
supported-state error path; apply the same gating change to the other PDF
shortcut occurrence around the 539-543 area and keep usage of getDirtySnapshot,
createSnapshotBackedOpenFile, and the existing image branch unchanged.

---

Nitpick comments:
In `@server/routes/file-browser.ts`:
- Around line 833-884: The code currently branches on fileStream.readable
(fileStream) but fs.createReadStream is always readable, so remove the
conditional and always convert the Node ReadStream to a web-compatible
ReadableStream via nodeStreamToWebStream(fileStream); update the Response to use
that single webStream variable and remove the unreachable branch and any dead
checks (e.g., the fileStream.readable check), keeping the nodeStreamToWebStream
function as the single conversion strategy and ensuring error handling/cleanup
remains intact.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c7082e05-dfb5-43c7-a0be-3ac07684f488

📥 Commits

Reviewing files that changed from the base of the PR and between a6614b0 and e597a02.

📒 Files selected for processing (5)
  • server/routes/file-browser.ts
  • src/features/file-browser/ImageViewer.tsx
  • src/features/file-browser/PdfViewer.tsx
  • src/features/file-browser/hooks/useOpenFiles.ts
  • src/features/file-browser/types.ts
✅ Files skipped from review due to trivial changes (1)
  • src/features/file-browser/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/file-browser/PdfViewer.tsx

Comment on lines +367 to +386
// Skip /api/files/read for image and PDF files; restore them directly
const fileName = basename(path);
if (isImageFile(fileName) || isPdfFile(fileName)) {
const dirtySnapshot = getDirtySnapshot();
if (dirtySnapshot) {
files.push(createSnapshotBackedOpenFile(path, dirtySnapshot));
} else {
files.push({
path,
name: fileName,
content: '',
savedContent: '',
dirty: false,
locked: false,
mtime: 0,
loading: false,
});
}
continue;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Gate the PDF raw-asset path behind local-workspace support.

These branches now treat PDFs as immediately viewable everywhere, but PdfViewer loads them from /api/files/raw, and server/routes/file-browser.ts, Line 775 still rejects raw requests for non-local workspaces. In a gateway-backed workspace, restoring or opening a PDF tab will therefore land in a broken viewer instead of a supported-state error. Either keep the PDF shortcut local-only here or add remote raw-file support first.

Also applies to: 539-543

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/file-browser/hooks/useOpenFiles.ts` around lines 367 - 386, The
PDF shortcut branch treats PDFs as viewable everywhere but PdfViewer uses
/api/files/raw which is only allowed for local workspaces; update the isPdfFile
check to require local-workspace support (e.g., add a condition like &&
isLocalWorkspace() or workspace.supportsLocalFiles) before restoring a PDF tab
so that non-local/gateway workspaces fall back to the supported-state error
path; apply the same gating change to the other PDF shortcut occurrence around
the 539-543 area and keep usage of getDirtySnapshot,
createSnapshotBackedOpenFile, and the existing image branch unchanged.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
server/routes/file-browser.ts (2)

22-22: Unused import: pipeline is never used.

The pipeline import from node:stream/promises is not referenced anywhere in this file. Remove it to keep imports clean.

🧹 Proposed fix
-import { pipeline } from 'node:stream/promises';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/file-browser.ts` at line 22, The import pipeline from
'node:stream/promises' in file-browser.ts is unused; remove the named import
pipeline (and the corresponding import entry) so the file no longer imports an
unused symbol and imports remain clean.

861-884: Consider using Readable.toWeb() instead of a custom implementation, but note the error logging trade-off.

Node.js provides Readable.toWeb() (available since v17, compatible with your >=22.0.0 requirement) which handles stream conversion with proper cleanup. However, your custom implementation includes console.error logging for stream errors that Readable.toWeb() does not provide. If this error visibility is important for debugging or monitoring, keep the current implementation; otherwise, you could simplify to:

import { Readable } from 'node:stream';
const webStream = Readable.toWeb(fileStream) as ReadableStream<Uint8Array>;

Then remove the nodeStreamToWebStream function entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/file-browser.ts` around lines 861 - 884, The
nodeStreamToWebStream function currently reimplements stream conversion and logs
errors via console.error; replace it with Node's built-in conversion to simplify
the code unless the explicit error logging is required. Either remove
nodeStreamToWebStream and use Readable.toWeb(fileStream) where file streams are
converted, or if you need to keep error visibility, keep nodeStreamToWebStream
but change console.error to use the project's logger and ensure
nodeStream.destroy(reason) is called in cancel with the provided reason; update
all call sites that used nodeStreamToWebStream to use Readable.toWeb(fileStream)
if you remove the helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/routes/file-browser.ts`:
- Line 22: The import pipeline from 'node:stream/promises' in file-browser.ts is
unused; remove the named import pipeline (and the corresponding import entry) so
the file no longer imports an unused symbol and imports remain clean.
- Around line 861-884: The nodeStreamToWebStream function currently reimplements
stream conversion and logs errors via console.error; replace it with Node's
built-in conversion to simplify the code unless the explicit error logging is
required. Either remove nodeStreamToWebStream and use Readable.toWeb(fileStream)
where file streams are converted, or if you need to keep error visibility, keep
nodeStreamToWebStream but change console.error to use the project's logger and
ensure nodeStream.destroy(reason) is called in cancel with the provided reason;
update all call sites that used nodeStreamToWebStream to use
Readable.toWeb(fileStream) if you remove the helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a178830-c2b9-4637-9761-b5b83e3c1a32

📥 Commits

Reviewing files that changed from the base of the PR and between e597a02 and 96560be.

📒 Files selected for processing (1)
  • server/routes/file-browser.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 (2)
server/routes/file-browser.ts (2)

833-836: Consider handling stream errors for better observability.

The Node.js stream created at line 833 can emit errors asynchronously (e.g., if the file is deleted mid-read). While Readable.toWeb() propagates these errors and the connection will close, there's no logging for debugging failed transfers.

🔧 Optional: Add error logging
     // Stream file with optional range support
     const fileStream = fsSync.createReadStream(resolved, { start, end });
+    fileStream.on('error', (err) => {
+      console.error(`[file-browser] Stream error for ${filePath}:`, err.message);
+    });
     
     // Convert Node.js stream to Web Stream using Node's built-in conversion
     const webStream = Readable.toWeb(fileStream);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/file-browser.ts` around lines 833 - 836, The Node.js file
stream created as fileStream and converted with Readable.toWeb(...) can emit
asynchronous 'error' events; add an 'error' listener on fileStream that logs the
error and context (e.g., the resolved path and byte range start/end) using the
module's logger, and ensure you remove or ignore duplicate logging after the
stream finishes/close to avoid leaks; this will surface failures during reads
(like file deletion mid-read) for easier debugging while leaving
Readable.toWeb(fileStream) behavior intact.

801-830: Range parsing handles the common case but omits suffix ranges.

The regex ^bytes=(\d+)-(\d*)$ doesn't match suffix-length ranges like bytes=-500 (meaning "last 500 bytes") which is valid per RFC 7233. Most PDF viewers use explicit start positions, so this is unlikely to cause issues in practice.

The validation and 416 response handling are correctly implemented.

🔧 Optional: Support suffix ranges
     const rangeHeaderValue = c.req.header('range');
     if (rangeHeaderValue && ext === '.pdf') {
-      const rangeMatch = rangeHeaderValue.match(/^bytes=(\d+)-(\d*)$/);
+      const rangeMatch = rangeHeaderValue.match(/^bytes=(\d*)-(\d*)$/);
       if (rangeMatch) {
-        const rangeStart = parseInt(rangeMatch[1], 10);
-        const rangeEnd = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : stat.size - 1;
+        const hasSuffix = rangeMatch[1] === '';
+        let rangeStart: number;
+        let rangeEnd: number;
+        
+        if (hasSuffix) {
+          // Suffix range: bytes=-500 means last 500 bytes
+          const suffixLen = parseInt(rangeMatch[2], 10);
+          rangeStart = Math.max(0, stat.size - suffixLen);
+          rangeEnd = stat.size - 1;
+        } else {
+          rangeStart = parseInt(rangeMatch[1], 10);
+          rangeEnd = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : stat.size - 1;
+        }

         // Validate range
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/file-browser.ts` around lines 801 - 830, The Range parsing
currently only matches `bytes=start-end` and misses suffix ranges like
`bytes=-500`; update the regex and handling around rangeHeaderValue/rangeMatch
so suffix-length ranges are supported: change the pattern to accept empty start
(e.g. /^bytes=(\d*)-(\d*)$/), then if the captured start is empty but the end
(suffix length) exists compute start = Math.max(0, stat.size -
parseInt(suffixLength,10)), end = stat.size - 1, set statusCode = 206 and
rangeHeader accordingly; keep the existing logic for explicit start/end parsing
and the same validation that returns 416 with Content-Range `bytes
*/${stat.size}` when invalid. Ensure this logic runs only when ext === '.pdf'
and continues to set start, end, statusCode, and rangeHeader variables used
later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/routes/file-browser.ts`:
- Around line 832-855: The /api/files/raw endpoint is being compressed which
breaks Range semantics; update the compression middleware (the compress() usage
in server/app.ts) to exclude this route by adding '/api/files/raw' to the
exclusion condition so responses from the file streaming handler
(file-browser.ts where Readable.toWeb, Content-Range and Content-Length are set)
are not compressed; ensure the compress() config or its conditional wrapper
checks the request.url/path and returns next() without applying compression for
'/api/files/raw' (and any equivalent route variants) so partial content (206),
Content-Range and Content-Length remain correct.

---

Nitpick comments:
In `@server/routes/file-browser.ts`:
- Around line 833-836: The Node.js file stream created as fileStream and
converted with Readable.toWeb(...) can emit asynchronous 'error' events; add an
'error' listener on fileStream that logs the error and context (e.g., the
resolved path and byte range start/end) using the module's logger, and ensure
you remove or ignore duplicate logging after the stream finishes/close to avoid
leaks; this will surface failures during reads (like file deletion mid-read) for
easier debugging while leaving Readable.toWeb(fileStream) behavior intact.
- Around line 801-830: The Range parsing currently only matches
`bytes=start-end` and misses suffix ranges like `bytes=-500`; update the regex
and handling around rangeHeaderValue/rangeMatch so suffix-length ranges are
supported: change the pattern to accept empty start (e.g.
/^bytes=(\d*)-(\d*)$/), then if the captured start is empty but the end (suffix
length) exists compute start = Math.max(0, stat.size -
parseInt(suffixLength,10)), end = stat.size - 1, set statusCode = 206 and
rangeHeader accordingly; keep the existing logic for explicit start/end parsing
and the same validation that returns 416 with Content-Range `bytes
*/${stat.size}` when invalid. Ensure this logic runs only when ext === '.pdf'
and continues to set start, end, statusCode, and rangeHeader variables used
later.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1f0e0fa-316f-42da-ad80-dcc969d98856

📥 Commits

Reviewing files that changed from the base of the PR and between 96560be and 60ca526.

📒 Files selected for processing (1)
  • server/routes/file-browser.ts

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.

1 participant