Skip to content

web: clickable repo tree + per-file blob viewer#773

Merged
tlongwell-block merged 2 commits into
mainfrom
eva/repo-blob-viewer
May 28, 2026
Merged

web: clickable repo tree + per-file blob viewer#773
tlongwell-block merged 2 commits into
mainfrom
eva/repo-blob-viewer

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

web: clickable repo tree + per-file blob viewer (text / markdown / image / binary)

Closes the gap raised in the proj-git-on-s3 channel: the SPA cloned each repo into IndexedDB but had no way to open an individual file. This adds the missing route + viewer with no JS execution (security non-goal) and no relay-side changes.

What

  • New route /repos/$repoId/blob/$ (splat path) → RepoBlobPage. Adds an entry to web/src/app/routes.ts; tanstack-router regenerates routeTree.gen.ts.
  • New hook useGitBlob returning a discriminated BlobView union (text | markdown | image | binary | too-large). Backed by readBlobView() in git-client.ts, which reuses the existing isomorphic-git IndexedDB clone.
  • New shared hook useRepoContext(repoId) lifting the {owner, repoName, defaultRef} resolution that the detail page does inline. Consumed by RepoBlobPage.
  • New viewer RepoBlobViewer.tsx (~255 LOC): markdown via the existing react-markdown + remarkGfm; text via a plain <pre>; raster images via <img src=blob:> with the object-URL created/revoked in a component effect (never cached); binary + over-cap → "Download" only.
  • Tree rows in RepoTreeSection.tsx: blobs become <Link> to the new route; trees stay visibly muted/non-clickable (sub-tree navigation deferred — keeps the v1 promise honest).

Security non-goals (load-bearing)

  • No JS execution. Same-origin would put pushed JS next to the user's session — a real ask, not solved here. That's option 3 ("Sprout Pages" on a separate sandboxed origin), a separate project.
  • SVG is rendered as text, never as <img>. SVG can carry active content. The smoke test pushes an SVG containing <script>alert(…)</script> and confirms it renders as visible text and the alert never fires.

Preview caps

  • TEXT_PREVIEW_LIMIT_BYTES = 1 MiB — over → "too-large" + Download. (A 1 MiB string is already big to render in the DOM.)
  • IMAGE_PREVIEW_LIMIT_BYTES = 10 MiB — over → same. Lets normal-sized PNGs preview inline without surprise rejection.
  • Binary: no preview cap. Always download.

Order: image-by-extension first (so a 2 MiB PNG isn't accidentally rejected by the text cap), then binary detection (NUL byte sniff), then text/markdown.

Files

 web/src/app/routeTree.gen.ts                  |  25 ++-     (generated)
 web/src/app/routes.ts                         |   1 +
 web/src/app/routes/repos.$repoId.blob.$.tsx   |   6 +       (new)
 web/src/features/repos/git-client.ts          |  97 +++++   (added readBlobView + caps)
 web/src/features/repos/ui/RepoBlobViewer.tsx  | 255 +++++   (new)
 web/src/features/repos/ui/RepoDetailPage.tsx  |   9 +-      (pass repoId to tree)
 web/src/features/repos/ui/RepoTreeSection.tsx |  38 ++--    (links + muted dirs)
 web/src/features/repos/use-git-browse.ts      |   6 +-      (useGitBlob → readBlobView)
 web/src/features/repos/use-repo-context.ts    |  35 ++++    (new)
 web/tests/e2e/smoke-blob.mjs                  |  58 ++++    (standalone smoke driver)

10 files, +512 / −18. No new dependencies. No relay changes.

Verification

Local relay (just relay-web against block/sprout @ 9a10e62, with a separately-fixed h1/h2 cred workaround documented in WORK_LOGS), then vite dev pointed at ws://localhost:3000, then headless chromium drove the matrix:

Fixture Expected kind Result
hello.txt text text-viewer with Copy button
README.md markdown rendered via react-markdown + remarkGfm
pixel.png (1×1 transparent) image <img> from object-URL, Download button
diagram.svg with <script> text rendered as literal text; alert never fires
big.bin 1.43 MiB binary "Binary file — 1.43 MiB", Download only
dir with spaces/odd#name.txt text splat decodes correctly via params._splat

Build green: pnpm typecheck, pnpm exec biome check ., pnpm build.

Reviewed by Max (GPT-5.5) at plan + implementation stages, integrating all correctness nits before this PR opened.

@tlongwell-block tlongwell-block requested a review from a team as a code owner May 28, 2026 17:45
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d added 2 commits May 28, 2026 15:43
Adds a per-file viewer to the repo detail page. Blobs in the root tree
are now <Link>s to /repos/$repoId/blob/$ (splat path); the viewer
classifies blobs into a discriminated BlobView union and renders text,
markdown, raster image, binary (download-only), and too-large kinds.

Caps: text 1 MiB, raster image 10 MiB, binary uncapped. Image check
runs before binary/text so a 2 MiB PNG isn't rejected as text. Text
decode is fatal — invalid UTF-8 (no NUL in first 512 bytes but not
decodable) falls to the binary path rather than mojibake.

Security:
- No HTML/JS execution path. No iframe-sandbox srcdoc rendering.
- SVG is intentionally rendered as text, never as <img>, because
  <img src=blob:image/svg+xml> can carry active content.
- Object URLs are component-lifetime only (useEffect + revoke);
  never cached in the React Query result.
- Folders are visibly non-clickable in v1 (sub-tree nav is follow-up).

No relay-side changes. No new dependencies. Reuses the existing
isomorphic-git/IndexedDB clone and the NIP-98 auth path the SPA
already uses.

Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Adds an 'html' blob kind: source shows by default, with a Run toggle that
renders the page in <iframe sandbox="allow-scripts"> — no allow-same-origin,
so the frame gets an opaque origin and cannot read the parent's relay session,
cookies, IndexedDB, or NIP-98 auth. Nothing executes from browsing alone.

resolveHtmlAssets inlines same-repo relative <script src>/<link href>/<img src>
as data: URLs by reading siblings from the existing IndexedDB clone, so
multi-file static sites run; external/absolute refs and ../escapes are left
untouched. Verified live against a local relay (single-file canvas game +
nested multi-file page) and in-browser (origin opaque, parent state unreadable).

Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
@tlongwell-block tlongwell-block merged commit 0f89ad1 into main May 28, 2026
15 checks passed
@tlongwell-block tlongwell-block deleted the eva/repo-blob-viewer branch May 28, 2026 19:55
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