Skip to main content

Module: file explorer

Tree view over the workspace for browsing, opening, and organizing files.

Status: VS Code-style explorer implemented. The files.tree panel lists the backend's workspace roots, lazy-loads directories on expand, and opens files as workspace-file: editor buffers. The view is a flattened, virtualized row list (state in the shared store, so it survives a dockview remount) with file icons, multi-selection (ctrl/shift-click), keyboard navigation (arrows, type-ahead, Enter, F2), inline rename, a right-click context menu, and git decorations (M/A/D/U badges + colors, folder change indicators, branch). Disk changes arrive live over the files watch channel (no manual refresh). Remaining VS Code parity — an "Open Editors" section, auto-reveal, and drag-and-drop — is tracked in Remaining parity.

Contributions to the layout shell

  • Panels: files.tree (singleton, default: left dock).
  • Commands: files.open, files.newFile, files.newFolder, files.rename, files.delete, files.refresh, files.openTerminalHere, files.revealActiveBuffer; files.revealInOS (desktop only — registered only when shell.revealInOS capability is present, so the palette never shows dead commands) is B6.
  • Interactions with other modules (B5): opening a file calls the editor's public openBuffer('workspace-file:...') service; "open terminal here" calls the terminal's openTerminal({ cwd }) rooted at the selected directory; the agent reads the tree selection via the panel's getAgentContext (B4). These go through each module's public exports — never another module's internals.

Backend surface

backend/modules/files/implemented (HTTP surface). All paths are rooted at configured workspace roots: every request resolves the target (collapsing .., following symlinks) and rejects anything that lands outside a root with 403. A relative path is anchored to a root first (a leading segment matching a root's name selects that root, else the first root) — so an agent that passes a bare notes.txt (it doesn't know absolute root paths) writes into the workspace instead of being rejected; the boundary check runs after anchoring, so a relative ../ escape is still 403. The path-traversal boundary lives here, not in the UI, so a remote backend can never be coaxed into serving paths outside its roots. Roots are configured in the settings module (files.roots; browser: paste a backend-host path; desktop: a native folder picker via fs.nativeDialogs), with an env override HORRIBLE_WORKSPACE_ROOTS for dev/test.

Out of the box: when neither setting nor env is configured, the backend defaults to a single root — its launch directory (Path.cwd()), which is the repo checkout under both the dev command and the Tauri desktop spawn (.current_dir). This is cross-platform and means the file explorer and every files.* agent/flow tool work immediately with no setup. A filesystem root (/, C:\) is skipped as too broad, and HORRIBLE_NO_DEFAULT_ROOT=1 restores the fail-closed boundary (empty roots → 400) for hardened/remote deployments. Explicit files.roots/env config always takes precedence over the default.

RoutePurpose
GET /files/rootsthe configured workspace roots
GET /files/lista directory's entries (dirs first)
GET /files/reada UTF-8 file's content (capped; binary → 415)
GET /files/git-statusworking-tree status of a root (git status --porcelain=v2; is_repo:false if not a repo)
POST /files/createnew file or directory
PUT /files/writeoverwrite/create file content
POST /files/renamerename/move within roots
POST /files/deletedelete (recursive required for non-empty dirs)

Live watch events ship: a backend watchfiles watcher (backend/modules/files/watcher.py) broadcasts changes on the files /ws channel, and the tree re-lists the affected directories on them (see Live watch below). Clients still re-list after their own mutations as a belt-and-braces fallback.

Agent integration

Status: implemented (B4). Declared on the files.tree panel, so they reach the capability manifest. Verified live: the agent wrote a file through gated files.write, which prompted with the rendered {path} specifier and wrote on approval. The tree exposes agent tools & getAgentContext:

  • Read (ungated): files.list(path), files.read(path), and files.gitStatus(path) (branch + each changed path's status); the tree's roots, the active selection, the full multi-selection (selectedPaths), and a git summary ({branch, changeCount}) are exposed through the pane's getAgentContext() snapshot.
  • Gated (side-effecting): files.create, files.write, files.rename, files.delete. These are matched by filesystem permission rules using gitignore-style anchors against the workspace roots — e.g. allow files.write(/src/**), deny files.delete(**). files.create and mkdir-style operations auto-allow under the acceptEdits mode; delete/rename still prompt. See the permission system.

The agent transfers out of the tree the same way a user does (it invokes the commands, never another module's internals): opening a file is editor.openBuffer('workspace-file:...'); "open terminal here" is terminal.new({ cwd }).

Live watch & a VS Code-like explorer

Status: Phase 1 (watch channel) and Phase 2 (the explorer UX) implemented.

Live file-watch /ws channel

The deferred "watch events" are the foundation everything else wants. A backend watcher (watchfiles, backend/modules/files/watcher.py) over the configured roots emits change events on a files /ws channel; the frontend store (initFilesWatch, store.ts) applies them (debounced) instead of manual re-listing. A single process-wide FileWatcher broadcasts to per-connection push_file_events tasks, mirroring telemetry's recorder/stream split. The one channel feeds three consumers:

Events carry the change type and the affected path's parent dir, so the tree re-lists only the directories that changed. Roots are read when the watch starts; a roots change (settings) restarts it.

Explorer UX

The store (store.ts) owns the whole tree model — roots, per-directory expansion, the children cache, the multi-selection, and the inline-rename target — and exposes a flattened, render-ready row list (visibleRows()). FileTree.tsx is a thin consumer that virtualizes those rows and wires the interactions:

FeatureHow
Flattened row modelvisibleRows() walks roots → expanded children into a flat Row[], which makes keyboard nav, multi-select, and virtualization fall out cheaply
File iconsicons.ts maps extension → a colored category tag (.file-icon/.ic-* in styles.css); folders use an open/closed glyph
Inline rename (F2)the active row becomes an <input>; Enter commits via renameEntry, Esc cancels
Context menuright-click → Open / New File / New Folder / Rename / Delete / Copy Path / Open Terminal Here (portaled, viewport-clamped)
Multi-selectCtrl/Cmd-click toggles, Shift-click selects a range along the visible order; the set drives multi-delete
Keyboard nav↑/↓ move the active row, ←/→ collapse/expand, Enter opens, F2 renames, Delete removes, and printable keys type-ahead-jump
Virtualized tree@tanstack/react-virtual over the flat rows, so large directories stay smooth

The palette commands (files.rename, files.delete) and the context menu share this behavior — rename starts the same inline edit, delete removes the whole selection.

Git decorations

A backend git source (git.py, parsing git status --porcelain=v2 --branch via GET /files/git-status) maps each changed path to a collapsed status; the store (reloadGit in store.ts) fetches per repo root on mount and on every watch event, and derives which directories contain changes. FileTree.tsx paints a colored name + letter badge per file (M/A/D/U/R/!), a change indicator on folders, and the branch in the toolbar. The same source is the ungated files.gitStatus agent read tool — "what's changed?". A non-git root simply renders undecorated (is_repo:false).

Remaining parity

  • "Open Editors" section at the top, listing open buffers (the editor tracks them via listBufferUris()).
  • Auto-reveal active file — make files.revealActiveBuffer fire automatically on buffer focus, with a toggle.
  • Drag-and-drop to move within the tree (and external drop = upload/native move).
  • Type-to-filter box reusing the pane-type-menu filter pattern.

Richer agent context

The tree's getAgentContext reports {roots, selection, selectedPaths, git:{branch, changeCount}}, and files.gitStatus exposes the full working-tree status; with the items above it grows to include the open editors — so the agent reasons about the project the way a developer does.

Browser vs desktop

The tree itself is identical — it always shows the backend's workspace roots. Desktop adds OS integration around the edges:

ConcernBrowserDesktop
Browse/CRUD workspace rootsfull, via backend FS APIfull, same path
Add a new workspace roottype/paste a backend-host pathnative folder picker (fs.nativeDialogs)
Reveal in Explorer/Findernot available (command not registered)yes, via Tauri shell API
Drag file out of the appnoyes (native drag out)
Drag file inuploads into the selected folder via backendmoves/copies natively, tree refreshes via watch events

As with the terminal: in the browser against a remote backend, this tree shows the backend host's files.