-
Notifications
You must be signed in to change notification settings - Fork 0
File Service & File Tree UI
This page covers two related layers:
-
src/core/file-service.ts— an Electron-free, guard-oriented set of file operations used by editor nodes and panels. Its API never throws; every call returns either a success shape or an error object. -
src/renderer/src/components/FileTree.tsx— the VS Code-style sidebar that browses the active project, opens editor nodes, embeds source control, and lazily renders directory listings throughwindow.termsprawl.files.list(root, rel, remote).
The core service is deliberately reusable by the Server Edition: it imports only node:fs and node:path, not Electron.
file-service.ts defines the shared result vocabulary for file access:
FileKind = 'text' | 'markdown' | 'image' | 'binary'FileErrorCode = 'MISSING' | 'IO' | 'UNSUPPORTED'FileReadResultFileWriteResult-
DirEntrywithname,path, andkind: 'dir' | 'file' -
DirListResult, whose error code additionally includesOUTSIDE
The main operations are:
| Function | Responsibility |
|---|---|
classifyFile(filePath) |
Classify by lowercased extension. Markdown extensions become markdown, image extensions become image, known archive/binary/media/font extensions become binary, and unknown extensions default to text. |
readProjectFile(filePath) |
Read text/markdown as UTF-8, report image paths without loading bytes, refuse known binary files with UNSUPPORTED, map ENOENT to MISSING, and map other failures to IO. |
writeProjectFile(filePath, content) |
Refuse directory paths, create parent directories recursively, write UTF-8 content, and return { ok: true } or an IO error. |
listProjectDir(root, rel = '.') |
List one folder under a project root. It resolves the requested relative path, guards it with resolveInside, filters hidden and skipped entries, stats each entry, and sorts directories before files. |
The list guard is the key containment boundary:
flowchart TD
FT[FileTree / TreeBranch] --> API[window.termsprawl.files.list]
API --> L[listProjectDir root, rel]
L --> G{resolveInside root, rel}
G -->|outside| OUT[DirListResult error OUTSIDE]
G -->|inside| S[statSync target]
S -->|not folder| IO[DirListResult error IO]
S -->|folder| RD[readdirSync target]
RD --> F[skip dot entries, .git, node_modules]
F --> ST[statSync each entry]
ST --> SORT[directories first, then name localeCompare]
SORT --> ENT[DirEntry list]
ENT --> API
API --> FT
FT -->|file click| OPEN[onOpenFile path]
FT -->|dir click| EXP[local expanded Set]
Key nodes in this flow:
-
resolveInsidecomputesresolve(root)andresolve(rootAbs, rel). It rejects anything that is neither the root itself nor prefixed byrootAbs + sep. -
listProjectDirtherefore only lists one folder inside the project root. It does not recursively walk directories itself. -
skipEntryhides every name starting with., plusnode_modulesand.git. - Directory rows are sorted before file rows; within each group, names use
localeCompare. - The renderer receives only
DirEntry[]or an error result. It never sees rawfsobjects.
FileTree.tsx renders the project sidebar. It is not just a file tree: it is a VS Code-style activity rail with three sections:
-
files— open editor tabs plus the lazy directory tree. -
source— embedsSourceControlPanel. -
plugins— placeholder text pointing users to Settings → Plugins.
The component props are:
cwd?: stringremote?: ProjectRemoteonOpenFile: (path: string) => voidopenEditors?: OpenEditorTab[]
For remote projects, the tree root is remote.path; otherwise it is cwd. The displayed root label uses remoteLabel(remote) for remote projects, or the basename of the local path.
FileTree uses a reducer from ../state/edge-reveal:
initialFileTreeChromeapplyFileTreeChromeSidebarSectionTreeSide
The reducer state tracks side, open, pinned, and section. Local refs and timers handle hover behavior:
PANEL_WIDTH = 264CLOSE_MS = 220-
closeTimerdelays close after mouse leave. -
ignoreLeavetemporarily suppresses close while flipping sides. -
cancelCloseclears the close timer. -
scheduleClosedoes nothing when pinned or whenignoreLeaveis set. -
flipSidesetsignoreLeave, cancels close, dispatchesflipSide, and clears the ignore flag after 400 ms. -
revealcancels close and dispatchesrevealfor the left or right side.
When the panel is closed, two hotspot elements render on the left and right canvas edges. Hovering either calls reveal(side). The <aside> itself cancels close on mouse enter and schedules close on mouse leave.
External section switching is handled through useSidebarRequests. When a request exists, the component dispatches switchSection, then consumes the request.
In the files section:
- A
tabslabel and list showopenEditors. - If no editor nodes are open, it renders
no open tabs. - Each open tab calls
onOpenFile(tab.path)and displays the basename. - A
fileslabel is followed by eitherthis project has no folderor a recursiveTreeBranch.
In the source section:
- If a root exists, it renders
<SourceControlPanel cwd={root} remote={remote} embedded />. - Otherwise it shows the same no-folder message.
In the plugins section:
- It renders a placeholder saying sidebar extensions are not available yet and directs users to Settings → Plugins.
TreeBranch is the recursive listing component. It receives:
rootreldepthonOpenFileremote
Its local state is:
entries: DirEntry[] | nullerror: string | nullexpanded: Set<string>
The effect depends on root, rel, and remote. It calls:
window.termsprawl.files.list(root, rel, remote)The effect uses a cancelled flag so late responses do not update unmounted branches. On error, it sets the error message and an empty entry list. On success, it clears the error and stores result.entries.
Rendering behavior:
- If
erroris set, render the error message. - If
entriesis null, renderloading…. - If entries are empty, render
empty. - Otherwise render a
<ul className="file-tree-list">.
Each row is a button:
- Directory click toggles the path in
expanded. - File click calls
onOpenFile(entry.path). - Directory rows show
▾or▸; file rows show·. - Padding is computed from
depth:8 + depth * 12.
When an expanded directory renders, it recursively creates another TreeBranch with:
- same
root rel={relFrom(root, entry.path)}depth + 1- same
onOpenFile - same
remote
relFrom converts an absolute entry path back into a root-relative path by stripping the normalized root prefix. basenameOf is used for open editor tab labels.
The tests in file-service.test.ts pin down several important contracts:
- Markdown, image, text, and known binary extensions classify correctly, including uppercase image extensions.
- Text and markdown files read as UTF-8 with the correct
kind. - Image reads return
{ kind: 'image' }and do not load bytes. - Known binary extensions return
UNSUPPORTED. - Missing files return
MISSING. - Writes create parent directories recursively.
- Writing to an existing directory path returns
IO. - Directory listings put directories first and then sort alphabetically.
-
node_modules,.git, and dot entries are skipped. - Nested listings are relative to the project root.
-
../outsideis rejected withOUTSIDE. - Missing folders return
MISSING.
One important scope boundary: readProjectFile and writeProjectFile accept a path directly. The root containment guard is implemented in listProjectDir through resolveInside. Callers that expose read/write to nodes or panels are responsible for supplying the intended project-scoped path.
Remote browsing is represented in the UI by passing remote through to window.termsprawl.files.list, and by using remote.path as the tree root. The comment in TreeBranch states that remote directory listing runs over SSH; the actual SSH-backed implementation is outside this page’s evidence.
Likely extension points in this module pair:
- Add file kinds or adjust classification by extending
IMAGE_EXT,MARKDOWN_EXT,BINARY_EXT, orclassifyFile. - Add new sidebar sections by extending
SidebarSectionin the edge-reveal state module and adding a tab/render branch inFileTree. - Change tree row behavior in
TreeBranch, especially how directories toggle and how files callonOpenFile. - Replace or extend the bridge method
window.termsprawl.files.listfor local versus remote listing. - Reuse
readProjectFile/writeProjectFilefrom editor nodes or panels without pulling in Electron or renderer code. - Add tests in
file-service.test.tsto lock down new classification, guard, or IO behavior.
Sources:
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance