-
Notifications
You must be signed in to change notification settings - Fork 0
Workspace Store & Project File Layout
The workspace store is the disk-backed half of workspace state. The renderer keeps React Flow as the live source of truth; WorkspaceStore is the main-process service addressed over IPC that owns project metadata and node persistence. It is constructed with a CorePlatform seam (platform.userDataPath) and does not import Electron, so the same store can run under the server entrypoint.
src/core/workspace-files.ts is the pure file-layout and serialization layer. src/core/workspace-store.ts is the stateful orchestration layer that calls it.
WorkspaceStore holds two pieces of in-memory state:
-
index: WorkspaceIndex— project metadata plus cleanup/tombstone queues. -
revs: Map<string, number>— per-project revision numbers. The constructor seeds each entry from the project file’srev(0when no file exists).addProjectsets0;deleteProjectdeletes the entry. The excerpt does not show thesaveNodesbody, so the exact read/update ofrevsduring node saves is outside this page.
Nodes are not cached in the store. snapshot() reloads every project file and returns { index, projects }; a project whose file is missing or unreadable comes back as [].
IPC is the boundary to the renderer. Channel names and handler wiring live in the preload/IPC contract; this page covers the store side.
sequenceDiagram
participant R as Renderer (React Flow live state)
participant S as WorkspaceStore
participant F as workspace-files
participant D as Disk
Note over S: constructor(platform)
S->>F: loadIndex(userDataPath)
F->>D: read workspace.json
D-->>F: WorkspaceIndex or empty fallback
loop each indexed project
S->>F: loadProjectFile(userDataPath, project)
F->>D: read folder/inline project file
D-->>F: ProjectFile or null
F-->>S: file.rev or 0
end
R->>S: snapshot()
S->>F: loadProjectFile per project
F-->>S: nodes or []
S-->>R: { index, projects }
R->>S: save path over IPC
S->>F: saveProjectFile(..., baseRev, links?)
F->>D: atomic temp write, then rename
F-->>S: new rev
Key nodes: loadIndex is the first disk read and silently falls back to an empty index on any parse/validation error. loadProjectFile seeds revs during construction and later supplies nodes to snapshot. snapshot is intentionally reload-based, not cache-based. saveProjectFile is the file-level write path; the store’s saveNodes wrapper is referenced elsewhere but its body is outside the excerpt.
| Path | Format | Writer | Shareable |
|---|---|---|---|
<userData>/workspace.json |
WorkspaceIndex |
saveIndex |
No — machine-local |
<cwd>/.termsprawl/project.json |
ProjectFile |
saveProjectFile |
Yes — inside the project repo |
<userData>/projects/<id>.json |
ProjectFile |
saveProjectFile |
No — for cwd-less/remote projects |
<path>.<pid>-<timestamp>-<random>.tmp |
temporary complete file | atomicWriteFile |
No — transient |
<basename>.*.delete |
staged deletion |
stageProjectFileRemoval (not shown) |
No — transient/crash-recovery |
Folder projects store nodes inside the project folder so the canvas layout can be committed and shared with the repository. Cwd-less projects, including remote projects whose cwd is null, use the inline userData path instead.
WorkspaceIndex is version 1 and contains projects, plus optional pendingTerminalCleanup, pendingTerminalNodeCleanup, and terminalTombstones arrays.
ProjectMeta stores id, name, cwd (null means cwd-less inline canvas), optional remote, closed, optional archived, and optional settings. ProjectSettings currently carries accent.
ProjectFile is version 1 and contains rev, nodes, and optional links. Links are optional so pre-link files load unchanged.
SerializedNode has required id, type, and position, with optional width, height, style, and an opaque data: Record<string, unknown> payload.
Startup and snapshot() both use the same file layer.
-
loadIndex(userDataPath)reads<userData>/workspace.json. It accepts onlyversion === 1with aprojectsarray. Missing optional cleanup arrays are defaulted; projects and cleanup entries with unsafe ids are filtered out. Any parse or validation error returns{ version: 1, projects: [] }. - For each indexed project, the constructor calls
loadProjectFile(userDataPath, project)to seedrevs.snapshot()calls it again to collect nodes. -
loadProjectFilechooses<cwd>/.termsprawl/project.jsonwhenproject.cwdis set, otherwise<userData>/projects/<id>.json. - If the chosen file is missing,
loadProjectFilelooks for staged deletion files in the same directory whose name starts with${basename(path)}.and ends with.delete, picks the last name after sorting, and renames it back to the live path. This is crash recovery for the window after deletion is staged but beforeworkspace.jsonis committed. It only restores when the index still references the project. - The file must parse as
version === 1with anodesarray. Links are optional and normalized throughparseNodeLinksfromshared/node-links. Any error returnsnull;snapshot()turnsnullinto[].
This parsing is migration-tolerant only in a narrow sense: optional fields can be absent and invalid ids are dropped. There is no version migration. A future format version is treated as a bad file: loadIndex returns an empty index and loadProjectFile returns null.
saveIndex and saveProjectFile are the two file-level writers.
-
saveIndexvalidates every project id and every cleanup/tombstone terminal id againstisSafeProjectId, createsuserDataPathif needed, and writesworkspace.jsonthroughatomicWriteFile. -
saveProjectFilecomputesrev = baseRev + 1. Whenlinksisundefined, it reads the existing file and preserves its links; if that read fails, it falls back to[]. It then writesProjectFileas JSON.- Folder project: creates
<cwd>/.termsprawland writes<cwd>/.termsprawl/project.json.mkdirfailures are wrapped withCannot save the project into <cwd>: ...so the user sees the location, not a rawEACCES/EROFS. - Inline project: creates
<userData>/projectsand writes<userData>/projects/<id>.json. - Returns the new
rev.
- Folder project: creates
-
atomicWriteFilewrites the entire content to a sibling temp path (<path>.<pid>-<timestamp>-<random>.tmp), thenrenameSyncs it over the live path. On failure it removes the temp file and rethrows the original error. The temp writer is injectable for tests.
Index-only lifecycle methods (closeProject, archiveProject, reopenProject, renameProject, updateSettings) mutate WorkspaceIndex and persist it; they do not touch project node files. addProject only writes the index; the first node save creates the project file.
updateSettings merges a patch into project.settings, deletes keys whose value is undefined, and collapses an empty settings object to undefined. This keeps in-memory state equal to what a relaunch loads.
deleteProject is the only multi-file mutation in the store excerpt. It builds a nextIndex that:
- removes the project from
projects; - appends the supplied
pendingTerminalIdsplus anypendingTerminalNodeCleanupentries for that project intopendingTerminalCleanup, deduplicating(projectId, terminalId); - drops
pendingTerminalNodeCleanupandterminalTombstonesentries for that project.
It then stages removal of the project file (stageProjectFileRemoval), saves the new index, and, on success, commits the staged removal. If saveIndex or commit fails, it rolls back the index when it was already saved, rolls back the staged removal, and throws the original error or an AggregateError if rollback also failed.
flowchart TD
A[deleteProject(id)] --> B[find ProjectMeta]
B --> C[build nextIndex: remove project, merge cleanup queues]
C --> D{project found?}
D -->|yes| E[stageProjectFileRemoval]
D -->|no| F[stagedRemoval = null]
E --> G[saveIndex(nextIndex)]
F --> G
G -->|success| H[stagedRemoval.commit]
G -->|failure| I[rollback index if it was saved]
H -->|success| L[set in-memory index, delete rev]
H -->|failure| I
I --> J[stagedRemoval.rollback]
J --> K[throw original error or AggregateError]
Key nodes: stageProjectFileRemoval is the staged deletion seam; its implementation is outside the excerpt. saveIndex commits metadata removal. commit deletes the staged file; rollback restores it. The rollback path aggregates errors if both the original operation and rollback fail.
Note: deleteProject does not early-return for an unknown id. It still writes nextIndex; project-file removal is only staged when project was found.
The index carries three optional arrays used to keep terminal teardown consistent across restarts:
-
pendingTerminalCleanup: terminal sessions that still need cleanup after a project is deleted.pendingTerminalIdsForProjectreads it;completeTerminalCleanupremoves completed terminal ids and persists. -
pendingTerminalNodeCleanup: terminal nodes that still need to be removed from a project file.stageTerminalNodeClosevalidates the terminal id and project, adds an entry if missing, and persists.removeTerminalNodereads the project viasnapshot(), filters out the node, and callssaveNodes.completeTerminalNodeCloseremoves the entry and persists. -
terminalTombstones: tombstones for closed terminal nodes.stageTerminalNodeClosewrites them alongsidependingTerminalNodeCleanup.retireCompletedTerminalTombstonesis intended for fresh-process startup: it keeps only tombstones whose node/session cleanup is still pending, because a fresh process cannot receive delayed renderer saves from the prior run.
- Unsafe ids:
isSafeProjectIdis^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$. It is enforced inaddProject,stageTerminalNodeClose,inlineProjectPath,saveIndex, and filtered inloadIndex. This prevents path traversal in inline project files. - Duplicate ids:
addProjectthrows if the id already exists.closeProject,archiveProject,reopenProject,renameProject, andupdateSettingssilently no-op when the project is not found. - Corrupt index:
loadIndexswallows all errors and returns an empty index. Project files may still exist on disk, but the store will not surface them. - Corrupt project file:
loadProjectFilereturnsnull;snapshot()reports that project as[]. A subsequent node-only save can also fall back tolinks: []if it cannot read the existing file to preserve links. - Version handling: both
workspace.jsonand project files requireversion === 1. Adding fields is safe only if they are optional or have a defaulting/filtering path in load. - Atomicity: temp files are sibling files and are renamed into place. A crash leaves
.tmpfiles that readers ignore. Staged.deletefiles are recovered only when the index still references the missing project. - Remote projects:
addProjectstorescwd: nulland aremotedescriptor. BecauseloadProjectFilebranches onproject.cwd, a remote project’s nodes use the inline<userData>/projects/<id>.jsonpath, not a remote.termsprawl/project.json. - Folder projects:
saveProjectFilecreates.termsprawlon save.folderHasProject(cwd)detects an existing project file for adoption.ensureFolderProjectRoot(cwd)is the pre-add validation seam for typed folder paths; the excerpt only shows its documented purpose and the start of its implementation.
- Add project metadata: extend
ProjectMetawith an optional field.loadIndexpasses unknown fields through from JSON, so old files remain loadable; add validation only if the field is security-sensitive. - Add per-project settings: extend
ProjectSettings.updateSettingsalready handles undefined-valued patch keys and empty-object collapse. - Add node-type state:
SerializedNode.dataisRecord<string, unknown>. Most node-specific state can live there without changing the file format; required geometry isposition, with optionalwidth,height, andstyle. - Add node links: follow the optional
linksfield pattern. Parse/normalize rules belong inshared/node-linksso the renderer can import them without pulling innode:fs;workspace-filesre-exports the parse helpers for callers that go through the file layer. - Add workspace-level bookkeeping: add an optional array to
WorkspaceIndex, then updateloadIndexfiltering andsaveIndexvalidation. Missing arrays must default to empty. - Add project-file fields: add an optional field to
ProjectFile, validate/normalize it inloadProjectFile, and decide whethersaveProjectFileshould preserve it when a partial save omits it.linksis the existing example. - Test atomic writes:
atomicWriteFileaccepts anAtomicTempWriter, so tests can inject write failures without touching real rename behavior.
This page is based on the provided excerpts of src/core/workspace-store.ts and src/core/workspace-files.ts. It does not cover the IPC channel table, the private persistIndex/saveNodes bodies, stageProjectFileRemoval implementation, shared/node-links parsing rules, SSH remote file operations, bundle export/import, or worktree registry. Where those are referenced, they are treated as boundaries of this module rather than described in detail.
Sources: src/core/workspace-store.ts, src/core/workspace-files.ts
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