Drive File Browsing#
Drive file browsing is the system for listing, filtering, and navigating files stored in agent mounts β S3-backed volumes attached to agent sessions (the session cwd and a per-artifact agent-files mount). The system was substantially rearchitected in PR #5400 to solve two compounding problems: opening a session whose agent had cloned an 11k-file repo froze the main thread (#5367), and the previous approach fetched the whole tree upfront, blocking for seconds on large mounts. The new design uses lazy per-directory loading, virtualised rendering, and three distinct backend view modes.
Primary entry points:
- Backend:
api/oss/src/core/mounts/service.pyβMountsService.list_files - Router:
api/oss/src/apis/fastapi/mounts/router.pyβGET /{mount_id}/files - Frontend hook:
web/oss/src/components/Drives/useLazyDriveTree.tsx - Frontend grid:
web/oss/src/components/Drives/VirtualTileGrid.tsx - Explorer component:
web/oss/src/components/Drives/DriveExplorer.tsx
Backend: Three View Modes#
MountsService.list_files multiplexes three unrelated views by parameter shape. A planned refactor (issue #5413) will split these into named per-view methods.
| View | Trigger | Behavior |
|---|---|---|
| Shallow | depth=1 | One delimiter listing of the immediate level. Optionally adds child counts (with_counts=true) via concurrent shallow lists. |
| Recency / Flat | order or limit set | Files-only listing sorted by recent, name, or path. Cursor-paginated. The sentinel limit=0 with no order returns count only (total + total_capped, empty files[]). |
| Browse | Default (no order/limit) | Full tree with synthesized folder entries. Enumerates all objects via flat listing. |
Route parameters : path, order (Literal["recent","name","path"]), limit (ge=0), depth (le=1; only depth-1 is implemented), with_counts, git_aware, include_gitignored. The depth and order constraints were hardened in PR #5412 to return 422 instead of silently falling into the most-expensive branch.
Response shape : { count, total, total_capped, files[] }. total counts the entries a view returns before the limit; its unit differs per view (leaf files only in recency, files-plus-folders in shallow/browse). The total / hidden-entry mismatch in flat view is a known cosmetic bug (issue #5419).
Archive export: POST /{mount_id}/files/export (renamed from /archive in PR #5412 to avoid collision with the mount soft-delete verb). Streams a zip of multiple mounts with zip-slip protection and safe Content-Disposition encoding for non-ASCII filenames.
Git-Aware Filtering#
Setting git_aware=true enables pruning at the storage layer before objects are enumerated. This is what makes browsing a cloned repo practical β the service jumps past node_modules and .git rather than paging through them.
Helper predicates :
_is_git_plumbing(path)β rejects any path containing a.gitsegment_is_hidden_path(path)β drops dotfile entries (recency view only)_is_internal_mount_path(path)β hides runner-owned paths (agents/,.agenta-*markers)_path_gitignored(rel_path, is_dir, specs)β tests a path and its ancestors against loaded.gitignorepathspecs
Spec loading : _load_gitignore_specs() reads .gitignore files shallow-to-deep, capped at 100 files to prevent pathological enumeration.
Level-by-level pruning : _list_pruned_files() descends the tree level-by-level with bounded concurrency (24 simultaneous store calls), reading each directory's .gitignore before deciding whether to descend into its children. This prevents node_modules from being touched at all. The include_gitignored toggle re-admits otherwise-pruned entries.
Known gaps (issue #5420): cross-file !negation is not honored (a deeper .gitignore's re-include cannot override a shallower ignore), and with_counts child counts use the parent's spec set rather than loading the child directory's own .gitignore. Whether these are bugs or documented best-effort behavior is an open product decision.
Planned extraction (issue #5413): the ~500 lines of git-view policy currently inside MountsService are targeted for extraction to core/mounts/git_view.py.
Frontend Architecture#
The frontend was rewritten in PR #5400, replacing two diverged drawer implementations (DriveDrawer.tsx, FilesWindow.tsx) with a single DriveExplorer shared across the build config Files section and chat session Files drawer.
Key components (all under web/oss/src/components/Drives/):
| File | Role |
|---|---|
DriveExplorer.tsx | Unified explorer with list / grid / flat view modes |
useLazyDriveTree.tsx | Lazy per-directory loading hook: sends depth=1 + child counts on expand, full tree only during search |
VirtualTileGrid.tsx | Windowed tile grid via @tanstack/react-virtual; 2D keyboard navigation with arrow keys, Cmd+Down/Cmd+Up (Finder-style) |
FilesDrawer.tsx | Controlled drawer wrapper, hosts DriveExplorer |
SessionFilesDrawer.tsx | Session-specific drawer entry point |
driveTree.ts | Tree state helpers (O(n) folder inference, replacing old O(nΒ²) approach) |
DriveFileRow.tsx | File row with loading mode for skeleton-to-content morphing |
DriveExplorerSkeleton.tsx | Initial full-panel skeleton |
Path validation was relaxed in PR #5412: the old character-allowlist ([\w. -]+ per segment) rejected real-world folder names like Next.js route groups (app/(auth)/[slug]), npm scopes (@scope/pkg), and non-ASCII names. The new denylist only rejects absolute paths, empty/./.. segments, NUL, and control characters. This was a latent bug promoted to first-class failure by the new lazy flow (every folder expand sends a path parameter that the old whole-tree call never did).
Loading, Error Handling, and agent-files#
Each session drive reads two mounts independently: the session cwd and a per-artifact agent-files mount. PR #5430 reworked useSessionDrive.ts so these resolve in parallel without blocking each other.
Resolution states :
isLoadingβ no mount has answered yet and nothing is displayable (initial blank only)reconcilingβ one mount answered but a sibling is still loading; shows content + "Loading moreβ¦" hinterroredβ session side failed with nothing to show; drives an inline error + Retry cardpartialErroredβ a mount failed but the drive still browses (e.g. only the agent mount broke); shows an amber warning badge on the drawer-trigger icon; "Try again" in the drawer header leaves the loaded file list intact
Skeleton rendering: DriveFileRow renders in a loading prop mode that morphs into the real row inside a single AnimatePresence, eliminating the skeleton-block β list layout jump .
agent-files symlink bug (issue #5480): as of v105.8, the "latest edited files" (recency) view shows the agent-files symlink entry itself instead of the files it points to. This is a runner-owned path that _is_internal_mount_path should suppress, but a regression in the recency branch lets it surface (AGE-3983, open).
Open Issues and Follow-ups#
| Issue | What | Status |
|---|---|---|
| #5413 (AGE-3974) | Split list_files into list_files_shallow / list_files_flat / browse; extract git policy to core/mounts/git_view.py; adopt query-object pattern for route params | Open chore |
| #5419 (AGE-3980) | Recency rollup fails single-batch case (just-cloned repo floods list); total field off by hidden-entry count; limit=0 sentinel undiscoverable | Open feat/fix |
| #5420 (AGE-3981) | git_aware semantics: promise Git-compatible or best-effort? Cross-file negation and with_counts spec-scope gaps | Needs product decision |
| #5416 | Regression and acceptance tests for mounts file and archive endpoints | Open chore |
| #5417 | Regenerate Fern client after /files/export rename; move mount calls off raw axios | Open chore |
| #5480 (AGE-3983) | agent-files symlink appears in recency view (v105.8 regression) | Open bug |