Skip to content

fix(runtime): render remote-server binary files (PDF/images) in the editor#6606

Merged
nwparker merged 3 commits into
stablyai:mainfrom
amondnet:amondnet/remote-server-binary-render
Jul 4, 2026
Merged

fix(runtime): render remote-server binary files (PDF/images) in the editor#6606
nwparker merged 3 commits into
stablyai:mainfrom
amondnet:amondnet/remote-server-binary-render

Conversation

@amondnet

Copy link
Copy Markdown
Contributor

Summary

Binary files (PDF, images) failed to render in the editor when connected to a remote-server runtime environment (the feature labeled "remote server" in the UI). They render fine locally and over SSH.

Root cause

The editor loads file content through readRuntimeFileContent (src/renderer/src/runtime/runtime-file-client.ts). Routing differs per target:

  • Local / SSH: window.api.fs.readFile(...) returns full binary metadata { content (base64), isBinary, isImage, mimeType } → PDFs/images render.
  • Remote server: uses the files.read RPC → readMobileFile, which throws binary_file for previewable binaries (.pdf/images are in MOBILE_BINARY_EXTENSIONS) and the client hardcoded isBinary: false. The error became a loadError, so the file never reached ImageViewer/PdfViewer.

A sibling RPC already does the right thing: files.readPreviewreadFileExplorerPreview returns { content (base64), isBinary, isImage, mimeType } for local, SSH, and environment runtimes.

Fix

When files.read rejects a binary path with binary_file, fall back to the binary-capable files.readPreview RPC and return its payload. This keeps the server as the single source of truth for binary detection (no duplicated extension list) and reuses the existing preview path — matching local/SSH behavior.

Non-previewable binaries (e.g. .zip, .mp4) now show the clean "Binary file — cannot display" message instead of a load error. Text editing is unchanged.

Test plan

  • runtime-file-client.test.ts: added a case asserting a remote binary read falls back to files.readPreview, and a case asserting non-binary_file errors do not fall back. All 38 tests pass.
  • typecheck:web clean, oxlint clean on changed files.
  • Manual: connect to a remote-server runtime environment; open a .pdf and .png (render), a .zip (clean binary message), and a text file (still editable).

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6b3f12bf-3f0d-4f78-af98-5e9d1c00c59c

📥 Commits

Reviewing files that changed from the base of the PR and between 34d4131 and cdd24f9.

📒 Files selected for processing (1)
  • src/renderer/src/runtime/runtime-file-client.test.ts

📝 Walkthrough

Walkthrough

readRuntimeFileContent in runtime-file-client.ts now wraps the remote files.read RPC in a try/catch. When the remote error is binary_file, it calls files.readPreview and returns that preview payload; other errors are rethrown. The truncated-content check remains in place. The test suite adds coverage for the binary fallback path, non-binary failures, and preview-call failures.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary and testing, but it omits required Screenshots, AI Review Report, Security Audit, and Notes sections. Add the missing template sections, including Screenshots/No visual change, a full AI Review Report, Security Audit, and Notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Title check ✅ Passed The title clearly summarizes the main change: rendering remote-server binary files in the editor.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/renderer/src/runtime/runtime-file-client.test.ts (1)

238-314: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the non-previewable binary regression case.

The new tests cover previewable binaries and non-binary failures, but the PR contract also promises that non-previewable binaries still end up with the standard binary_file result. A test where files.read rejects with binary_file and files.readPreview also rejects would lock that behavior in.

Suggested test shape
+  it('surfaces binary_file for non-previewable remote binaries', async () => {
+    runtimeEnvironmentCall.mockImplementation((args: { method: string }) => {
+      if (args.method === 'files.read') {
+        return Promise.resolve({
+          id: 'rpc-read',
+          ok: false,
+          error: { code: 'runtime_error', message: 'binary_file' },
+          _meta: { runtimeId: 'remote-runtime' }
+        })
+      }
+      return Promise.resolve({
+        id: 'rpc-preview',
+        ok: false,
+        error: { code: 'runtime_error', message: 'binary_file' },
+        _meta: { runtimeId: 'remote-runtime' }
+      })
+    })
+
+    await expect(
+      readRuntimeFileContent({
+        settings: { activeRuntimeEnvironmentId: 'env-1' },
+        filePath: '/remote/repo/archive.zip',
+        relativePath: 'archive.zip',
+        worktreeId: 'wt-1'
+      })
+    ).rejects.toThrow('binary_file')
+  })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d324d008-df34-428c-9e84-b2b9a1d82093

📥 Commits

Reviewing files that changed from the base of the PR and between c50391d and 34d4131.

📒 Files selected for processing (2)
  • src/renderer/src/runtime/runtime-file-client.test.ts
  • src/renderer/src/runtime/runtime-file-client.ts

amondnet and others added 3 commits July 3, 2026 14:30
Opening a binary file (PDF/image) on a remote-server runtime environment
fell back to the text-only files.read RPC, which rejects binaries with
'binary_file' and dropped isImage/mimeType, so the editor never reached
ImageViewer/PdfViewer. Fall back to the binary-capable files.readPreview
RPC when files.read rejects a binary path, matching local/SSH behavior.
Scope the try/catch to the files.read call so the truncated-size guard is no
longer caught-and-rethrown, and match RuntimeRpcCallError('binary_file')
instead of any Error whose message text happens to equal 'binary_file'. Adds a
regression test that a non-RPC error sharing the message does not trigger the
preview fallback.

Co-authored-by: Orca <help@stably.ai>
@nwparker nwparker force-pushed the amondnet/remote-server-binary-render branch from cdd24f9 to 8c9b605 Compare July 3, 2026 21:32
@nwparker

nwparker commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Thanks @amondnet for the clean, well-scoped fix and the thorough tests. A maintainer has taken over the PR, verified it end-to-end, rebased onto latest main, and pushed a small hardening commit on top. It now awaits final human review before merge.

Description

Bug: On a remote-server runtime environment, binary files (PDFs, images, etc.) never rendered in the editor — they showed a load error instead. They work fine locally and over SSH.

Root cause: The editor loads content through readRuntimeFileContent. On the remote-server path it calls the files.read RPC (readMobileFile), which throws binary_file for any previewable-binary extension, and the client then surfaced that as a loadError. So the payload never reached ImageViewer/PdfViewer. A sibling RPC, files.readPreview (readFileExplorerPreview), already returns the base64 { content, isBinary, isImage, mimeType } shape for local, SSH, and environment runtimes.

Fix (author): When files.read rejects with binary_file, fall back to files.readPreview and return its payload — keeping the server as the single source of truth for binary detection and matching local/SSH behavior.

Maintainer hardening (this takeover):

  • Verified the error contract end-to-end: server throws new Error('binary_file') (orca-runtime-files.ts) → mapRuntimeError passes the message through verbatim under the generic runtime_error code → the renderer receives a RuntimeRpcCallError whose .message is exactly binary_file. The string match is correct and this is the same message-matching pattern already used elsewhere in this file (runtimePathExists).
  • Tightened the discriminator from err instanceof Error to err instanceof RuntimeRpcCallError so only a genuine RPC failure — not any stray Error that happens to carry the same text — can trigger the second round-trip.
  • Scoped the try/catch to just the files.read call, so the existing truncated-size guard is evaluated outside the catch instead of being thrown-and-rethrown through it.
  • Added a regression test locking in the narrowed match, and confirmed the return type RuntimeFilePreviewResult is structurally identical to RuntimeReadableFileContent.

Evidence

Unit is the practical harness here (full e2e needs a live remote-server runtime). Reproduced by reverting only the source fix while keeping the new tests:

Before (fix reverted, tests kept) — the bug:

 ❯ runtime-file-client.test.ts (39 tests | 2 failed)
   × falls back to files.readPreview when a remote binary file is opened
   × propagates a files.readPreview failure during the binary fallback
AssertionError: promise rejected "RuntimeRpcCallError: binary_file { …(2) }" instead of resolving
 ❯ readRuntimeFileContent src/renderer/src/runtime/runtime-file-client.ts:158:18
Serialized Error: { code: 'runtime_error', response: { ok: false,
  error: { code: 'runtime_error', message: 'binary_file' } } }
 Test Files  1 failed (1)
      Tests  2 failed | 37 passed (39)

The binary_file RPC error rejecting instead of resolving is exactly what became a loadError in the editor.

After (fix + hardening, rebased on main):

 Test Files  1 passed (1)
      Tests  40 passed (40)
pnpm tc            exit=0   (tsgo node + cli + web, clean)
oxlint (changed)   exit=0
oxfmt --check      exit=0

ELI5

When you opened a PDF or image while connected to a "remote server", the app asked for the file the wrong way — the server replied "that's a binary file, I won't hand it over like text," and the app just showed an error. Now, when the app hears "that's binary," it re-asks using the request built for binaries, gets the picture/PDF back, and shows it — the same way it already worked on your own machine.

Trade-offs

No regressions. Behavior is unchanged for every other case: text files still read normally, oversized text still hits the "too large to open" guard, and non-binary_file errors (permission, missing, etc.) propagate as before. The only new cost is a single extra RPC round-trip on the binary path (one files.read failure → one files.readPreview), and only on remote-server runtimes. Non-previewable binaries (.zip, .mp4) fall through to { content: '', isBinary: true }, i.e. the clean "binary file — cannot display" message rather than a load error. The mobile-markdown bridge, which re-throws binary_file for binary results, keeps its existing behavior.

Not merging — leaving the final call to a human maintainer.

@nwparker

nwparker commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Thanks @amondnet and sorry for delay!

@nwparker nwparker merged commit 727c7ae into stablyai:main Jul 4, 2026
nwparker added a commit that referenced this pull request Jul 4, 2026
…fix garbled comments (#7322)

Quality pass on renderer/shared PRs merged 2026-07-03:

- WorktreeTitleInlineRename: skip the truncation measure + ResizeObserver in `wrapTitle`
  mode, where wrapped titles never truncate — it could only churn unused state (#7307).
- editor slice: reuse the `removeEditorStateForReplacedPreview` helper this PR added
  instead of a hand-rolled copy of the same six-field eviction (drops ~50 lines) (#6476).
- useFileExplorerTree: extract `readWorktreeDirectory` so the connectionId/settings
  assembly for `readRuntimeDirectory` lives in one place, not three (#6321).
- comment-markdown-github-attachment-media: extract a shared `AttachmentFallbackLink`
  for the image/video error-fallback link (#6759).
- repository-icon-github: fold the two near-identical live resolvers into one
  parameterized `resolveRepositoryIdentityLive`; trim a 3-line comment to 2 (#6507).
- resource-usage-open-slices: delete the `shouldReadPopoverSlices` identity wrapper and
  inline `open` at the four call sites (#7275).
- BrowserPane: drop the pointerEvents assignment already applied inside
  `ensureBrowserPageWebview` for the reused-webview path (#6958).
- github slice: fix two garbled "…a commit main confirmed…" comments (#7277).
- runtime-file-client: trim the binary-file fallback comment to its whys (#6606).
- composer-branch-selection: drop the inline comment that restated the JSDoc (#6748).
- TabBarQuickCommandsButton: correct the stale "+ Command" comment (button shows no +).

No behavior change (the editor-helper reuse is behavior-equivalent, only more
conservative on an edge case); typecheck, oxlint, react-doctor, oxfmt, and touched
unit suites all pass.
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.

3 participants