feat(webui): file preview endpoints + viewer (webui-parity slice 02) - #47
Merged
Merged
Conversation
Right-panel file preview (slice 02 of webui-parity): the files panel
opens a read-only viewer for the clicked path. Renders markdown via
the existing lib/markdown renderer (so the threat model and allow-list
match chat output), source code as monospace pre with a language
badge from the server hint, and images via /api/fs/raw.
Server:
GET /api/fs/read-file?path=<file>
JSON content (≤512 KiB), 4xx with mime/language/binary preserved
on the error body so the client can still route. Containment gate
is the shared assertWorkspacePath from server/lib/workspace.js —
no new escape hatch; symlink / '..' attempts go through the same
realpath containment check as /api/fs/read.
GET /api/fs/raw?path=<file>
Stream bytes (≤20 MiB), mime mapped from extension, Cache-Control:
no-store. The Hono layer wraps the streaming via rawStreamToWebResponse
because createResponseCapture is sync-only (header comment in
server/app.js calls out streaming as P2; this slice is the first
consumer). The legacy handleFsRaw(req, res) keeps the streaming
shape for non-Hono callers / test fixtures.
Webapp:
lib/file-preview.ts — pure pickPreviewKind + helpers, separated so
unit tests can pin the type→renderer routing without spinning up
React (Node loader does not honour the Next.js '@/lib/...' alias).
components/file-preview.tsx — the React view; renders header +
error/markdown/image/code states. Uses raw fetch for getFsFile
so the structured 4xx body is preserved (request() helper throws
on non-OK and would discard mime/binary).
styles/official-utilities.css — .file-preview-markdown / -codeblock
rules (headings, paragraphs, lists, table, hr; the chat codeblock
shell is NOT used here, the chat shell styles would clobber our
padding to 0).
Tests:
routes/fs-read-file.test.js — containment, size cap (413), binary
(415), directory (415), missing path, full success shape.
routes/fs-raw.test.js — containment, mime mapping, stream round-trip,
directory (400), missing path.
test/server/app-hono.test.js — extended to assert the two new
routes are in OWNED_ROUTES.
webapp/test/file-preview.test.ts — 25 cases pinning the pickPreviewKind
mapping (markdown, image mime tiebreaker, code catch-all,
case-insensitive extension, exhaustive kind set).
Live self-check (isolated 18144/18145, MCODE_WEBUI_DATA_DIR=/tmp/dev-fp/data):
opened FilePreview against README.md (markdown), api.ts (typescript),
and 02-workspace-shell.jpg (image) — screenshot at
/tmp/dev-fp/screenshots/file-preview-final.png shows all three branches
rendering correctly with no console errors. The panels.tsx wiring that
mounts this component into the files panel tree is the follow-up
(parent agent owns it); this slice is the read-only viewer + endpoints.
The rebase conflict on release/public-source.json was resolved by
regenerating the inventory in this worktree so the file records both
slices: slice 01's files-tree.ts / files-tree.test.ts and slice 02's
file-preview.{ts,tsx} / file-preview.test.ts.
check:source: 4599 files pass. test:webapp: 396/396.
fengzhi09
force-pushed
the
feat/file-preview
branch
from
September 27, 2026 11:04
baadf0d to
ce2f14b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Slice 02 of the webui-parity program (
.tickets/webui-parity/02-file-preview.md): the read-only file viewer foundation.GET /api/fs/read-file?path=— JSON text content, ≤512 KiB (structured 413 over), binary sniff → 415 withmime/languagepreserved so the client can still route images.GET /api/fs/raw?path=— streamed bytes, ≤20 MiB, mime from extension map,Cache-Control: no-store.safePath→assertWorkspacePath); no new escape hatch. Registered inOWNED_ROUTES;docs/API.mdupdated.components/file-preview.tsx+lib/file-preview.ts(pure type→renderer mapping): markdown via the repo's existinglib/markdown.ts(same sanitizer/allow-list as chat), source as monospace + language badge, images via<img src="/api/fs/raw?path=…&cid=…">.getFsFileuses a rawfetch(idiomatic here, as elsewhere inapi.ts) so the structured 4xx body survives the preview's image branch.This slice deliberately does not mount the viewer in the panel — that wiring plus the shared
open.file.in.webaction is tracked in slice 12, which the acceptance explicitly required before this feature is called user-visible.Acceptance (independent agent)
PASS-WITH-CONCERNS, all slice scope verified live:
..to a real/etc/hostname, absolute/etc/*, symlink → /etc/hostname, symlinked dir → /etc, and a dangling symlink all refused 403 before any filesystem touch (error names the resolved target). Controls pass:..inside roots and a/tmp→/tmpsymlink both return 200 — the gate resolves rather than blanket-refusing..mdrendered with<script>fully stripped;.tscode view +typescriptbadge;.pngvia/api/fs/raw(verified 200, byte-identical,no-store) — notfile://; binary → 415 clear state; oversize → clear message./api/fs/readand/api/stateunaffected.Open policy item (deliberately left to a follow-up, non-blocking): credential-shaped files (
.env,credentials.ini,~/.ssh/*) preview as full plaintext — consistent with the existing/api/fs/readgate (home is an allowed root). A mask/warn policy is recommended as follow-up; noting the server advertises a LAN URL, so these GETs are LAN-reachable (pre-existing property).Full
pnpm verifydeferred to CI.