Skip to content

feat(frontend): browse and preview mount files in the session inspector - #5204

Merged
mmabrouk merged 2 commits into
big-agentsfrom
feat/mount-file-viewer
Jul 13, 2026
Merged

feat(frontend): browse and preview mount files in the session inspector#5204
mmabrouk merged 2 commits into
big-agentsfrom
feat/mount-file-viewer

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Context

The playground's SessionInspector drawer has a Mounts tab. For a session with a durable
cwd mount full of files, the tab showed only each mount's name / slug / id. Users could
see that a mount existed but not what the agent wrote into it, let alone read a file. The
backend already exposed everything needed (GET /mounts/{id}/files for listing, ?read=
for text, /files/download for raw bytes), so the tab just never called it.

Before: expanding a mount showed three lines of metadata.
After: expanding a mount shows its files, folders first, with breadcrumb navigation.
Clicking a file previews it inline (text or image) or offers a Download button.

Changes

api.ts gained three fetchers: fetchMountFiles, fetchMountFileText (both through the
Fern mounts client), and fetchMountFileBlob. The blob fetcher goes through the shared
axios instance instead of Fern, because the generated downloadMountFile always
text/JSON-parses the response body, so a Blob is unreachable through it. The function
carries a one-line comment explaining why.

MountsTab.tsx replaced the flat metadata List with a Collapse, one panel per mount.
Expanding a panel queries the root listing; clicking a folder row re-queries with that
folder's path. Clicking a file opens an inline preview: text extensions render through
read=, image extensions fetch a blob and render it via an object URL, everything else
(and anything over 2 MB) shows a Download button instead.

The listing endpoint is recursive and flat: it returns every file under a path in one
response, with paths relative to the mount root, and folders only show up as is_folder
marker entries with no trailing slash. deriveRows (new file,
assets/mountBrowser.ts) derives the one-level view the UI needs: it groups entries by
their first path segment past the current folder into synthetic folder rows, merges those
with any explicit folder markers so a folder never appears twice, and returns folders
before files, both sorted alphabetically. It is unit-tested (9 tests,
assets/mountBrowser.test.ts, colocated per repo precedent like
TemplateStrip/assets/pagerMath.test.ts), including regression tests for the two bugs
review caught (see below).

Scope / risk

Frontend only. Files touched: SessionInspector/api.ts, SessionInspector/tabs/MountsTab.tsx,
and the new SessionInspector/assets/mountBrowser.ts + mountBrowser.test.ts. No backend
changes, no Fern regen, no new endpoints.

dump.ts (the markdown export of a session's mounts) is untouched and still shows metadata
only; it does not gain a file listing in this PR. Nothing outside the Mounts tab changes,
so the only realistic regression surface is that tab itself: mounts with zero files, mounts
that fail to load, and sessions with zero mounts all need to keep working (all three are
covered in QA below).

The blob-fetch axios exception is intentional, not a drift from the Fern convention:
web/AGENTS.md requires Fern for new endpoint calls, and fetchMountFiles /
fetchMountFileText follow that. fetchMountFileBlob calls the same endpoint Fern already
wraps; it bypasses Fern only because Fern's generated method parses every response body as
JSON before handing it back, which makes a binary Blob unreachable no matter how the
call is made. The listing and text fetchers stay on Fern.

One known v1 limit, tracked in open-issues.md: the listing endpoint has no one-level or
delimiter mode, so a very large mount's root view transfers its full recursive tree in one
response. The frontend derivation bounds what renders, not what is fetched.

Tests / notes

  • npx --yes vitest@4.1.10 run src/components/SessionInspector/assets/mountBrowser.test.ts
    from web/oss: 9/9 passing.
  • Eslint clean, tsc clean on the touched files.
  • Colocated web/oss/src/**/*.test.ts vitest files, this one included, are not wired into
    the CI unit-test harness (it runs package test:<layer> scripts only). Logged as a
    deferred item in open-issues.md; same status as the existing colocated tests.
  • Review caught two real bugs before this landed: a first cut flattened nested files at
    the mount root and duplicated them inside folders (the listing is recursive, not
    one-level); a second cut keyed folder detection off a trailing slash the backend never
    sends, so empty folders rendered as files and 404'd on preview, and a later one-line fix
    for that unconditionally sliced a marker entry's last character, producing a phantom
    folder row. All three are pinned as regression tests in mountBrowser.test.ts.

How to QA

Prerequisites: local dev stack (run.sh with your usual OSS or EE flags), and a
session whose cwd mount has nested files. If you don't have one, run a playground agent
that writes a few files into its cwd, including a subfolder, a text file, an image, and a
file over 2 MB.

Steps:

  1. Open the playground, run or pick a session with that mount, and open the
    SessionInspector drawer.
  2. Go to the Mounts tab and expand the mount panel.
  3. Click into a subfolder, then use the breadcrumb to navigate back to root.
  4. Click a .md or .txt file.
  5. Click a .png or other image file.
  6. Click a file over 2 MB.
  7. Click a file type with no preview support (for example .mp3 or .pdf).
  8. Toggle dark mode and repeat steps 2-4.

Expected result:

  • Step 2: the panel shows folders first, then files, each file with a human-readable size.
  • Step 3: the folder's children show; the breadcrumb takes you back to the same root view.
  • Step 4: the file's text content renders inline in a scrollable panel.
  • Step 5: the image renders inline.
  • Step 6: no preview; a Download button appears and downloads the file.
  • Step 7: "No preview available" plus a working Download button.
  • Throughout: no console errors, and the drawer still opens fine for sessions with zero
    mounts.
  • Step 8: the tab and preview panel look correct in dark mode too.

Automated tests:

npx --yes vitest@4.1.10 run src/components/SessionInspector/assets/mountBrowser.test.ts

(run from web/oss)

Edge cases: a mount with an explicit empty-folder marker must show a folder row, not a
file row and not a phantom row with a truncated name (both were real bugs, now pinned in
mountBrowser.test.ts). A mount with a same-named file and folder at the same path (for
example a file a and a folder a/) must show both as separate rows. Re-check dark mode
on the preview panel specifically, not just the file list.

https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 13, 2026 3:30am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 75d2c707-2e2c-40b7-b755-617b17b329f4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The SessionInspector Mounts tab now browses mount files through new frontend fetchers, derives one-level folder/file rows, previews supported text and image files, and downloads unsupported or oversized files. Documentation records the design, implementation status, research, tests, QA steps, and deferred issues.

Changes

Mount file viewer

Layer / File(s) Summary
Mount file API contract
docs/design/mount-file-viewer/*, web/oss/src/components/SessionInspector/api.ts
Documents the frontend-only scope and adds typed listing, text, and Blob fetchers for mount files.
Browser row derivation
web/oss/src/components/SessionInspector/assets/mountBrowser.ts, web/oss/src/components/SessionInspector/assets/mountBrowser.test.ts, docs/design/mount-file-viewer/pr-body.md
Adds one-level folder/file row derivation, size formatting, sorting, marker handling, and regression tests.
Mount browsing and preview UI
web/oss/src/components/SessionInspector/tabs/MountsTab.tsx, docs/design/mount-file-viewer/{status.md,open-issues.md,research.md,pr-body.md}
Replaces the flat mount list with collapsible panels, lazy navigation, text/image previews, size guards, and download fallbacks; supporting documentation records validation and deferred work.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MountsTab
  participant SessionInspectorAPI
  participant MountsRouter
  User->>MountsTab: expand mount or select file
  MountsTab->>SessionInspectorAPI: fetch listing or preview data
  SessionInspectorAPI->>MountsRouter: call mount file endpoint
  MountsRouter-->>SessionInspectorAPI: return entries, text, or bytes
  SessionInspectorAPI-->>MountsTab: provide data
  MountsTab-->>User: render rows, preview, or download
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main frontend change: browsing and previewing mount files in SessionInspector.
Description check ✅ Passed The description is directly about the mount file browser/preview changes and matches the documented code changes.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mount-file-viewer

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.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Author notes on the non-obvious hunks, for reviewers. Live QA evidence: all 8 acceptance checks pass on the dev box (root listing shape, folder nav + breadcrumb, text preview, image preview, 2 MB cap on a 3.3 MB avi, download fallback for mp3, console clean, dark mode), verified on a real session whose cwd mount holds nested test assets.

* `is_folder` marker rows for a direct child folder are merged into the same synthetic set
* so a folder never appears twice.
*/
export const deriveRows = (files: MountFileEntry[], currentPath: string): BrowserRow[] => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why this function exists at all: the backend has no one-level listing mode. GET /mounts/{id}/files walks the prefix recursively (list_objects(recursive=True), no delimiter) and returns a flat list with every path relative to the mount root. Folder rows only appear for explicit marker objects, and agent-written files (geesefs) mostly have none. All three bugs found during review and live QA lived in this derivation, and each is pinned as a regression test in mountBrowser.test.ts.

if (!relative) continue // marker entry for the current path itself

const isMarker = entry.is_folder || relative.endsWith("/")
const cleanRelative = relative.endsWith("/") ? relative.slice(0, -1) : relative

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This line pair is the subtle part. is_folder: true entries arrive WITHOUT a trailing slash, so folder detection must read the flag (an earlier cut keyed it on the slash alone: empty folders rendered as files and 404'd on click). The slice, though, must stay conditional on an actual trailing slash — a cut that sliced whenever isMarker was true chopped the last character off every folder name and produced a phantom file_explorer_test_asset row next to file_explorer_test_assets in live QA. Keep the two conditions distinct.

projectId: string | null | undefined,
path: string,
): Promise<Blob> {
const res = await axios.get<Blob>(`${getAgentaApiUrl()}/mounts/${mountId}/files/download`, {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Deliberate Fern bypass, scoped to this one fetcher. The generated downloadMountFile never sets a responseType, so the shared fetcher's getResponseBody falls through to response.text() + JSON.parse on every body — binary bytes come back mangled, and .withRawResponse() can't recover them because the stream is already consumed. fetchMountFiles and fetchMountFileText stay on Fern per web/AGENTS.md; the axios interceptor already special-cases Blob responses, so no extra wiring.

"css",
])
const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "svg", "webp"])
const MAX_PREVIEW_BYTES = 2 * 1024 * 1024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Cap rationale: the ?read= endpoint loads the whole object into API memory and utf-8-decodes it, so previews should stay small. The cap gates both preview queries via enabled (checked against the listing's size, which the backend always populates), and bigger files fall back to Download, which streams bytes instead. 2 MB is a judgment call; happy to tune.

const [objectUrl, setObjectUrl] = useState<string | null>(null)
// Create the object URL as an effect (not in useMemo) so StrictMode's double-render
// can't orphan one; revoke the previous URL on every re-run and on unmount.
useEffect(() => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Object-URL lifecycle: created in an effect (not useMemo) so StrictMode's double-invoked render can't orphan a URL, and the cleanup revokes on file switch, preview close, and unmount. The download path (triggerDownload above) defers its revoke by a tick instead, because a synchronous revoke after a synthetic click can cancel the save in Firefox.

expect(rows.map((row) => row.name)).toEqual(["notes", "src", "tests"])
})

it("collapses a geesefs mkdir marker plus its contents into one full-name folder row", () => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This test and the two after it are the regression pins for the three real bugs found during review + live QA: recursive-listing flattening, is_folder markers without trailing slashes rendering as files, and the truncated-name phantom folder row. Note the colocated oss/src vitest files (this one included) are not yet wired into the CI harness — tracked in docs/design/mount-file-viewer/open-issues.md; they run locally via the command in the PR body.

@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (1)
web/oss/src/components/SessionInspector/api.ts (1)

49-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add runtime validation before returning these Fern payloads.
fetchMountFiles and fetchMountFileText still cast the response directly to local types; safeParseWithLogging is already available from @agenta/entities/shared and should be used here with local Zod schemas so backend drift fails loudly instead of leaking bad data into the UI.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f9515c3-3cb4-4d13-b6f4-e575398935cf

📥 Commits

Reviewing files that changed from the base of the PR and between 5d21324 and a31dce2.

📒 Files selected for processing (11)
  • docs/design/mount-file-viewer/README.md
  • docs/design/mount-file-viewer/context.md
  • docs/design/mount-file-viewer/open-issues.md
  • docs/design/mount-file-viewer/plan.md
  • docs/design/mount-file-viewer/pr-body.md
  • docs/design/mount-file-viewer/research.md
  • docs/design/mount-file-viewer/status.md
  • web/oss/src/components/SessionInspector/api.ts
  • web/oss/src/components/SessionInspector/assets/mountBrowser.test.ts
  • web/oss/src/components/SessionInspector/assets/mountBrowser.ts
  • web/oss/src/components/SessionInspector/tabs/MountsTab.tsx

Comment thread docs/design/mount-file-viewer/pr-body.md
Comment thread docs/design/mount-file-viewer/pr-body.md Outdated
Comment thread docs/design/mount-file-viewer/status.md
Comment thread web/oss/src/components/SessionInspector/assets/mountBrowser.test.ts Outdated
Comment thread web/oss/src/components/SessionInspector/tabs/MountsTab.tsx
The SessionInspector Mounts tab showed only a mount's name, slug, and id.
It now lists the mount's files (one-level view derived client-side from
the flat recursive listing, folders first, breadcrumb navigation) with
click-to-preview: text inline via the read endpoint, images via blob
object URLs, and a Download fallback for other types and files over 2 MB.

Frontend-only; uses the existing /mounts/{id}/files endpoints. The blob
fetch uses the shared axios instance because the generated Fern client
JSON-parses binary bodies. deriveRows lives in assets/mountBrowser.ts
with a colocated vitest suite (9 tests).

Design workspace: docs/design/mount-file-viewer/

Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj
@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says: Addressed the remaining review feedback before merge.

  • Breadcrumbs are native buttons. File rows expose button semantics and activate on Enter or Space. Live :8380 QA confirmed keyboard activation opens the file preview.
  • Added the missing shell fence language.
  • Renamed the marker test so its name matches the branch it exercises.
  • Confirmed the current suite contains and passes 9 tests; the bot's 8-test count was stale.

Focused Vitest: 9/9 passed. Frontend lint and Prettier passed.

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. Frontend labels Jul 13, 2026
@mmabrouk
mmabrouk force-pushed the feat/mount-file-viewer branch from 0bdb6ec to daae705 Compare July 13, 2026 03:26
@CLAassistant

CLAassistant commented Jul 13, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@mmabrouk
mmabrouk force-pushed the feat/mount-file-viewer branch from daae705 to b6c4947 Compare July 13, 2026 03:29
@mmabrouk
mmabrouk merged commit f2b56dd into big-agents Jul 13, 2026
23 checks passed
@mmabrouk
mmabrouk deleted the feat/mount-file-viewer branch July 13, 2026 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Frontend size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants