Skip to main content

workspace

The workspace module is the agent's file API for live-app canvases (Lovable-style React sandboxes, LaTeX editors, slides, custom builders). Files live in memory, stream live to the client over Socket.IO, optionally mirror to disk, and optionally lint on every write.

PropertyValue
Module idworkspace
Action count6 LLM-exposed + 7 internal (UI / REST only)
Typeper-app instance, per-session state
Pip depsnone (stdlib).
Dependencieswraps preview (transport) + lsp (diagnostics, optional)

The 6 actions

ToolFQNVisible paramsPurpose
WsWriteworkspace.writepath, contentCreate / overwrite.
WsReadworkspace.readpathRead.
WsEditworkspace.editpath, old_string, new_stringSurgical text replacement (same fuzzy cascade as filesystem).
WsGlobworkspace.globpatternPattern match.
WsGrepworkspace.greppatternContent regex search.
WsDeleteworkspace.deletepathRemove.

Internal actions (not LLM-exposed)

The workspace module also ships 7 internal actions marked internal=True on the module. They are called by the daemon's REST endpoints and by the UI for the validation / approval flow, and are deliberately invisible to the agent so the LLM cannot self-approve its own writes:

ActionPurpose
approve_fileStage the whole file - baseline = current content.
reject_fileRevert to baseline (or delete if never approved).
approve_file_hunksPartial stage by hunk index or 12-char hash.
reject_file_hunksPartial revert by hunk index or hash.
writeback_fileUser writeback (manual edit / drag-drop import).
commit_sessiongit add + git commit over approved files.
git_statusRefresh git_status flags on every tracked file.

These are wired into the /api/apps/{app_id}/sessions/{sid}/workspace/files/... routes documented in Validation workflow.

Visible vs hidden params

ActionVisibleHidden
writepath, content-
readpathoffset, limit
editpath, old_string, new_stringreplace_all, insert_at_line, fuzzy_threshold, max_suggestions
globpatternsort_by
greppatternglob, case_insensitive, multiline, before, after, max_results
deletepath-

Auto-detection of render_mode

When render_mode: auto, the daemon picks the renderer from the first file's extension:

ExtensionResolved render_mode
.tsx, .jsxreact
.texlatex
.mdmarkdown
.htmlhtml
slides.md / *.slides.mdslides
anything elsecode

Configuration

tools:
modules:
workspace:
config:
render_mode: react # auto | react | html | markdown | slides | code | latex | builder
entry_file: src/App.tsx # main file to render first
title: "My App"
sync_to_disk: false # mirror writes to real filesystem (Lovable-style)
sync_path: null # fixed disk dir (overrides auto-isolation)
lint: true # diagnostics on every write/edit
auto_approve: false # bypass validation; every write lands approved
agent_root: "" # whitelist-style agent scope (see below)
instructions: | # prepended to all workspace tool prompts
You are building a React app...
tool_instructions: # per-tool override
write: "Custom write instructions..."

agent_root - scope lock for attachments mode

When non-empty, every workspace path the agent tries to touch must start with agent_root. Anything outside is treated as hidden: WsRead returns "file not found", WsGlob skips it, WsGrep ignores it. The SDK iframe and the HTTP workspace routes (/api/apps/.../workspace/files/...) are NOT affected, they keep seeing the full tree. This is not a sandbox, it is a convenience guardrail to keep the agent focused on the directory it should be reading from.

The canonical use is the chat attachments tool-mode (see app.attachments_mode): attachments land under attachments/<name>, and agent_root: "attachments" ensures the agent can read them via WsRead but cannot reach app-private files via .. or absolute paths.

tools:
modules:
workspace:
config:
agent_root: "attachments" # agent locked to attachments/
auto_approve: true # uploads land pre-approved
lint: false

With this config, WsRead("attachments/report.pdf") works, WsRead("config.json") returns not-found even when the file exists in the workspace.

Top-level ui.workspace: block

Separate from tools.modules.workspace.config, the ui.workspace: block at the top level is what the client reads to pick a renderer (see Workspace & Preview + Client Manifest):

ui:
workspace:
render_mode: react
entry_file: src/App.tsx
title: "My App"
# Layout + open-on-mount (added 2026-05-04 / 2026-05-14)
position: right # right|bottom|hidden|overlay
width_pct: 50 # 10..90 split ratio
auto_open_on_first_tool: true # open on first file write
default_open: false # open immediately on session mount
# View routing - which tab opens, which tabs are reachable
default_view: auto # auto|code|preview|changes|activity
hidden_views: [] # subset of [code, preview, changes, activity]
# Preview iframe chrome (toolbar above the live preview)
preview_chrome:
enabled: true
refresh: true
open_in_new_tab: true
viewport_toggle: false
url_bar: auto # auto|always|never

See Client manifest → ui.workspace for the per-field reference (defaults, valid values, when each one fires).

The two blocks coexist - tools.modules.workspace enables the actions for the agent; ui.workspace tells the client how to display the resulting files. Both are needed for a fully functional live workspace.

File payload sent to preview

Every mutation publishes to the files channel of the preview module:

{
"content": "...",
"language": "tsx",
"size": 1234,
"lines": 42,
"status": "modified",
"operation": "edit",
"insertions": 5,
"deletions": 2,
"total_insertions": 47,
"total_deletions": 12,
"diff": "...",
"unified_diff": "...",
"updated_at": 1776297401.5,
"validation": "pending",
"insertions_pending": 5,
"deletions_pending": 2
}
FieldDescription
statusadded / modified / deleted.
operationwrite / edit / delete.
insertions / deletionsLines changed in the last op.
total_insertions / total_deletionsCumulative since session start.
unified_diffWell-formed (parseable by difflib.PatchSet).
validationpending (default) / approved (after approve, or when auto_approve: true).
insertions_pending / deletions_pendingDelta vs the last-approved baseline, NOT cumulative - reset to 0 after approve.

Validation workflow

Every WsWrite / WsEdit ships with validation: "pending" unless auto_approve is on. The daemon exposes a per-session workspace surface with operations grouped as:

OperationPurpose
Read summaryFile list, render mode, entry file, dirty flag.
Read file (with ?include_baseline=true)Content + baseline + unified_diff_pending.
Read file historyRevision list (revision, approved_at, approved_by, tokens_delta_ins/del).
Code snapshotFile tree + metadata only (validation, language, lines, status, pending-diff flags). Does not include content - fetch each file individually.
Preview snapshotLive preview state (resources, channels, events).
Changes diffDiff vs baseline across the whole session - pending hunks per file.
Export / import / forkPortable workspace dump + restore + new-session-from-export.
Approve / reject (whole file)Stage = current content / revert to baseline (or delete if never approved).
Approve / reject hunksPartial stage / revert by hunk index OR 12-char hash.
User writebackManual edit, conflict resolution, drag-drop.
Commitgit add + git commit over approved files.
Refresh git statusRefresh git_status flags on every tracked file.

The exact route shapes are not documented publicly - the Python testing SDK and the React Preview SDK expose these operations as typed methods.

Hunks have stable 12-char SHA-256 ids (header + body) - the client can approve by hash instead of index to survive races with concurrent agent writes. The approve-hunks implementation applies hunks in reverse position order so earlier indices aren't perturbed by later length changes.

Baseline + history persist to:

{ws}/.digitorn/sessions/{sid}/baselines/{path}             # baseline
{ws}/.digitorn/sessions/{sid}/baselines/{path}.history/ # revisions
rev-NNNN
_index.json

Survives daemon restart.

auto_approve: true - bypass validation

config:
auto_approve: true

Every write / edit lands with validation: "approved", pending counters always zero, baseline = current on each mutation. For sandbox apps / trusted-agent pipelines / CI. Per-call override via PUT /workspace/files/{path} {auto_approve: true} for a single writeback without flipping the module-level flag.

sync_to_disk: true - mirror to real filesystem

When set, every workspace mutation is mirrored to disk:

OpEffect
write / editWrites updated content to {sync_dir}/{path}.
deleteRemoves the file from disk.
readRead-through - if the file isn't in memory but exists on disk, loads it.
glob / grepScans disk for files not yet in memory, then searches the union.

Replaces the need for a separate filesystem module in apps that generate real code (Lovable-style sandboxes, React, LaTeX).

sync_dir resolution order

  1. sync_path in YAML - fixed, never overridden.
  2. ctx.workspace set by the user (the user picked a project folder in the UI).
  3. Auto-isolated: ~/.digitorn/workspaces/{app_id}/{session_id}/.

This prevents concurrent sessions from clobbering each other's files.

Lint on write

When lint: true (default), every write / edit returns diagnostics inline:

  1. LSP module (when loaded) - lsp.notify_change(path, content) → real language server (texlab, pyright, ruff, eslint, ...).
  2. Built-in content validators - JSON, YAML, TOML, Python syntax, LaTeX (unmatched braces + environments). Work in-memory, no external tools.

Diagnostics appear as {lint: [{line, severity, message, source}, ...]}.

The agent never needs to call lsp.diagnostics separately.

Bootstrap wiring

At app bootstrap:

  • workspace._preview = preview_module - Socket.IO transport.
  • workspace._lsp = lsp_module - diagnostics provider (when loaded).
  • Top-level ui.workspace: block fields injected (render_mode, entry_file, title).

Cross-references