editor
The editor worker holds a workspace: a folder, the buffers open against it,
and which folders are expanded. That record is shared, not private to you — a
file you open with editor::open appears in the user's tabs, and a file they
have open is one you can see with editor::workspace::get.
The unit is a folder, not a repository. Everything works in a plain directory;
git only adds a branch and change marks when there is one.
It performs no filesystem access itself: reads, writes, moves and listings are
delegated to shell, so anything shell refuses, editor refuses too.
The workspace
Everything is relative to one workspace: a root folder, the buffers open
against it, and which folders are expanded. It is shared, so it is also how you
tell the user what you are doing.
editor::workspace::get tells you the root and what is already open. Read it
before assuming anything about where you are.
editor::workspace::open repoints it. That changes what every surface sees,
including the user's screen, so do not do it casually mid-task.
editor::tree lists a folder and carries the expansion state; passing
expand or collapse persists it for both surfaces.
editor::buffers::list and ::close are the tab set. Closing one closes it
for the user too.
When to Use
- You are about to write a file and want to show the change first
(
editor::diff — pure, no path required).
- You want the user to see what you are working on:
editor::open puts it in
their editor, which is better than pasting the file into the conversation.
- You need to know what they are looking at (
editor::workspace::get).
- You are editing across several turns and must not clobber a concurrent edit
(
editor::open for the version, then editor::save with
expected_version).
- You are renaming or moving something (
editor::move — never shell::fs::mv
when buffers may be open; see below).
- You know roughly what a file is called but not where it lives
(
editor::find); you want to find it by its contents (editor::search).
- You are creating or removing files (
editor::create, editor::delete —
delete closes any buffer beneath the path, which shell::fs::rm does not).
- You are committing or syncing (
editor::git::commit, ::sync, ::stash,
::undo-commit).
- You want a file as it was at a revision rather than as it is now
(
editor::git::show, HEAD by default). Pair it with editor::open to
diff the two sides yourself rather than parsing a patch.
- You want the working tree as data rather than porcelain text
(
editor::git::status, editor::git::hunks).
Boundaries
editor::find matches paths; editor::search matches contents.
Listing a directory outside the workspace is still shell::fs::ls.
- Not a full git client. Status, hunks, a file at a revision, tracked paths,
commit, fetch/pull/push, stash and undo-last-commit are covered. Anything
else — branch, checkout,
rebase, cherry-pick, remote management — goes through
shell::exec.
editor::git::sync pulls --ff-only; a merge is deliberately not offered,
because a conflicted tree under open buffers is a mess an editor cannot
usefully show.
- Not a way around the jail. A path
shell rejects comes back as shell's
error, unchanged.
editor::save writes the whole file. It is not a patch applier — build
the complete new content, then save it.
- Binary files are refused, not mangled.
Functions
editor::workspace::open — point the workspace at a folder; returns the
buffers and expanded folders remembered for it.
editor::workspace::get — the active root, open buffers and expanded
folders, as every surface sees them.
editor::tree — list a folder, carrying and persisting expansion state.
editor::open — read a text file and record it as an open buffer.
editor::save — whole-file write, guarded against a concurrent change.
editor::buffers::list — the tab set.
editor::buffers::close — close one buffer; the file on disk is untouched.
editor::move — move or rename, rewriting every buffer and expanded folder
at or under the path.
editor::create — create a file or folder, parents included.
editor::delete — remove a path and close any buffer it held.
editor::find — fuzzy file finder over paths, ranked basename-first.
editor::search — search file contents, grouped by file.
editor::diff — unified patch between two texts; pure, nothing is read.
editor::git::status — branch, upstream, ahead/behind, one row per changed
path.
editor::git::hunks — what changed in one file, as ranges plus a patch.
editor::git::show — a file's contents at a revision, HEAD by default.
editor::git::commit — stage and commit.
editor::git::sync — fetch, fast-forward pull, or push.
editor::git::stash — stash the working tree, or pop the most recent stash.
editor::git::undo-commit — undo the last commit, keeping its changes
staged.
Every path is root-relative unless it is absolute. The editor::git::*
functions fail outside a repository, which is an absent overlay rather than a
broken workspace.
The two rules that prevent data loss
Save against the version you opened at.
editor::open returns version (an opaque version of the content) and
mtime.
- Pass
version back as expected_version on editor::save. Prefer it to
expected_mtime: mtime resolution is one second, so two writes inside the
same second are indistinguishable and the later one wins silently.
expected_mtime still works and is honoured when expected_version is
absent; when both are sent, expected_version is the guard.
- If the content changed in between, nothing is written: the response
carries
conflict: true, the current disk_version and disk_mtime, and
conflict_patch — a diff from what is on disk now to what you tried to
write.
Re-open, reconcile against that patch, and save again with the fresh version —
or use the version a successful editor::save returns as the guard for the
next one, without re-opening. Do not retry with the guard omitted to force it
through; that is exactly the clobber it exists to prevent. Omit it only when
creating a new file.
Move through editor::move, not shell::fs::mv.
editor::move rewrites every open buffer and expanded folder at or under the
path. shell::fs::mv does not, so buffers keep pointing at the old location
and the next save writes them back there — silently recreating the folder that
was just moved.
Reading a response
editor::diff — identical: true means the texts match. truncated: true
means a side was over max_diff_bytes and no diff was computed; it does
not mean "no changes".
editor::open — truncated: true means the file was over max_file_bytes
and you hold only its beginning. It is deliberately not recorded as a
buffer, and saving it back is refused, because that would delete the rest.
editor::find — from_git: false means the folder is not a repository and
candidates came from the directory walk. truncated: true means only the
first max_find_candidates paths were ranked; narrow the query.
editor::git::hunks — empty hunks with untracked: true means git has
never seen the file, so there was nothing to compare against.
editor::git::status failing with "not a git repository" is an absent
overlay, not a broken workspace. Everything else still works.
editor::search — paths come back root-relative, like every other
function here. truncated: true means the search stopped at
search_max_matches.
editor::git::commit — committed: false with a summary is "nothing to
commit", not a failure. Do not retry it.
editor::git::show — exists: false with empty content means the path is
absent at that revision, which is what a newly added file looks like. It
is not an error.
Reactive triggers
The worker publishes one custom trigger type, editor::changed, which fires
after a file in the workspace changes — whoever changed it. It does not require
the writer to have called this worker: a harness::hook::post-trigger hook on
the shell::* and coder::* write paths turns any filesystem call into an
event. The hook is advisory and fail-open, so it never delays or denies the
write that produced it.
Bind it when a different worker or surface should follow edits as they land:
mirroring the workspace into a viewer, reacting to an agent's writes without
polling editor::git::status, or annotating a file the moment it moves.
Do not bind when you made the write yourself — editor::save already returns
added, removed and the new version.
How to bind
- Register a handler:
registerFunction('my-worker::on-edit', handler).
- Register the trigger:
iii.registerTrigger({
type: 'editor::changed',
function_id: 'my-worker::on-edit',
})
Bindings take no config, and every subscriber gets every event. Delivery is
fire-and-forget: a slow or absent subscriber is logged and skipped rather than
retried. The event's patch is capped and sets truncated when it was cut —
call editor::git::hunks when you need the whole diff. For the payload shape,
call get function info on the trigger type.
1---2name: editor3description: A code workspace shared with the user — open buffers, a file tree, diffs, fuzzy find, and saves that refuse to clobber. Backed by the shell worker for files and the state worker for the workspace record.4---56# editor78The editor worker holds a **workspace**: a folder, the buffers open against it,9and which folders are expanded. That record is shared, not private to you — a10file you open with `editor::open` appears in the user's tabs, and a file they11have open is one you can see with `editor::workspace::get`.1213The unit is a folder, not a repository. Everything works in a plain directory;14git only adds a branch and change marks when there is one.1516It performs no filesystem access itself: reads, writes, moves and listings are17delegated to `shell`, so anything `shell` refuses, `editor` refuses too.1819## The workspace2021Everything is relative to one **workspace**: a root folder, the buffers open22against it, and which folders are expanded. It is shared, so it is also how you23tell the user what you are doing.2425- `editor::workspace::get` tells you the root and what is already open. Read it26 before assuming anything about where you are.27- `editor::workspace::open` repoints it. That changes what every surface sees,28 including the user's screen, so do not do it casually mid-task.29- `editor::tree` lists a folder and carries the expansion state; passing30 `expand` or `collapse` persists it for both surfaces.31- `editor::buffers::list` and `::close` are the tab set. Closing one closes it32 for the user too.3334## When to Use3536- You are about to write a file and want to show the change first37 (`editor::diff` — pure, no path required).38- You want the user to see what you are working on: `editor::open` puts it in39 their editor, which is better than pasting the file into the conversation.40- You need to know what they are looking at (`editor::workspace::get`).41- You are editing across several turns and must not clobber a concurrent edit42 (`editor::open` for the `version`, then `editor::save` with43 `expected_version`).44- You are renaming or moving something (`editor::move` — never `shell::fs::mv`45 when buffers may be open; see below).46- You know roughly what a file is called but not where it lives47 (`editor::find`); you want to find it by its *contents* (`editor::search`).48- You are creating or removing files (`editor::create`, `editor::delete` —49 delete closes any buffer beneath the path, which `shell::fs::rm` does not).50- You are committing or syncing (`editor::git::commit`, `::sync`, `::stash`,51 `::undo-commit`).52- You want a file as it was at a revision rather than as it is now53 (`editor::git::show`, HEAD by default). Pair it with `editor::open` to54 diff the two sides yourself rather than parsing a patch.55- You want the working tree as data rather than porcelain text56 (`editor::git::status`, `editor::git::hunks`).5758## Boundaries5960- `editor::find` matches **paths**; `editor::search` matches **contents**.61 Listing a directory outside the workspace is still `shell::fs::ls`.62- Not a full git client. Status, hunks, a file at a revision, tracked paths,63 commit, fetch/pull/push, stash and undo-last-commit are covered. Anything64 else — branch, checkout,65 rebase, cherry-pick, remote management — goes through `shell::exec`.66 `editor::git::sync` pulls `--ff-only`; a merge is deliberately not offered,67 because a conflicted tree under open buffers is a mess an editor cannot68 usefully show.69- Not a way around the jail. A path `shell` rejects comes back as `shell`'s70 error, unchanged.71- `editor::save` writes the **whole file**. It is not a patch applier — build72 the complete new content, then save it.73- Binary files are refused, not mangled.7475## Functions7677- `editor::workspace::open` — point the workspace at a folder; returns the78 buffers and expanded folders remembered for it.79- `editor::workspace::get` — the active root, open buffers and expanded80 folders, as every surface sees them.81- `editor::tree` — list a folder, carrying and persisting expansion state.82- `editor::open` — read a text file and record it as an open buffer.83- `editor::save` — whole-file write, guarded against a concurrent change.84- `editor::buffers::list` — the tab set.85- `editor::buffers::close` — close one buffer; the file on disk is untouched.86- `editor::move` — move or rename, rewriting every buffer and expanded folder87 at or under the path.88- `editor::create` — create a file or folder, parents included.89- `editor::delete` — remove a path and close any buffer it held.90- `editor::find` — fuzzy file finder over paths, ranked basename-first.91- `editor::search` — search file contents, grouped by file.92- `editor::diff` — unified patch between two texts; pure, nothing is read.93- `editor::git::status` — branch, upstream, ahead/behind, one row per changed94 path.95- `editor::git::hunks` — what changed in one file, as ranges plus a patch.96- `editor::git::show` — a file's contents at a revision, HEAD by default.97- `editor::git::commit` — stage and commit.98- `editor::git::sync` — fetch, fast-forward pull, or push.99- `editor::git::stash` — stash the working tree, or pop the most recent stash.100- `editor::git::undo-commit` — undo the last commit, keeping its changes101 staged.102103Every path is root-relative unless it is absolute. The `editor::git::*`104functions fail outside a repository, which is an absent overlay rather than a105broken workspace.106107## The two rules that prevent data loss108109**Save against the version you opened at.**1101111. `editor::open` returns `version` (an opaque version of the content) and112 `mtime`.1132. Pass `version` back as `expected_version` on `editor::save`. Prefer it to114 `expected_mtime`: mtime resolution is one second, so two writes inside the115 same second are indistinguishable and the later one wins silently.116 `expected_mtime` still works and is honoured when `expected_version` is117 absent; when both are sent, `expected_version` is the guard.1183. If the content changed in between, **nothing is written**: the response119 carries `conflict: true`, the current `disk_version` and `disk_mtime`, and120 `conflict_patch` — a diff from what is on disk now to what you tried to121 write.122123Re-open, reconcile against that patch, and save again with the fresh version —124or use the `version` a successful `editor::save` returns as the guard for the125next one, without re-opening. Do not retry with the guard omitted to force it126through; that is exactly the clobber it exists to prevent. Omit it only when127creating a new file.128129**Move through `editor::move`, not `shell::fs::mv`.**130131`editor::move` rewrites every open buffer and expanded folder at or under the132path. `shell::fs::mv` does not, so buffers keep pointing at the old location133and the next save writes them back there — silently recreating the folder that134was just moved.135136## Reading a response137138- `editor::diff` — `identical: true` means the texts match. `truncated: true`139 means a side was over `max_diff_bytes` and **no diff was computed**; it does140 not mean "no changes".141- `editor::open` — `truncated: true` means the file was over `max_file_bytes`142 and you hold only its beginning. It is deliberately *not* recorded as a143 buffer, and saving it back is refused, because that would delete the rest.144- `editor::find` — `from_git: false` means the folder is not a repository and145 candidates came from the directory walk. `truncated: true` means only the146 first `max_find_candidates` paths were ranked; narrow the query.147- `editor::git::hunks` — empty `hunks` with `untracked: true` means git has148 never seen the file, so there was nothing to compare against.149- `editor::git::status` failing with "not a git repository" is an absent150 overlay, not a broken workspace. Everything else still works.151- `editor::search` — paths come back root-relative, like every other152 function here. `truncated: true` means the search stopped at153 `search_max_matches`.154- `editor::git::commit` — `committed: false` with a summary is "nothing to155 commit", not a failure. Do not retry it.156- `editor::git::show` — `exists: false` with empty content means the path is157 absent at that revision, which is what a newly added file looks like. It158 is not an error.159160## Reactive triggers161162The worker publishes one custom trigger type, `editor::changed`, which fires163after a file in the workspace changes — whoever changed it. It does not require164the writer to have called this worker: a `harness::hook::post-trigger` hook on165the `shell::*` and `coder::*` write paths turns any filesystem call into an166event. The hook is advisory and fail-open, so it never delays or denies the167write that produced it.168169Bind it when a *different* worker or surface should follow edits as they land:170mirroring the workspace into a viewer, reacting to an agent's writes without171polling `editor::git::status`, or annotating a file the moment it moves.172173Do not bind when you made the write yourself — `editor::save` already returns174`added`, `removed` and the new `version`.175176### How to bind1771781. Register a handler: `registerFunction('my-worker::on-edit', handler)`.1792. Register the trigger:180181```typescript182iii.registerTrigger({183 type: 'editor::changed',184 function_id: 'my-worker::on-edit',185})186```187188Bindings take no config, and every subscriber gets every event. Delivery is189fire-and-forget: a slow or absent subscriber is logged and skipped rather than190retried. The event's `patch` is capped and sets `truncated` when it was cut —191call `editor::git::hunks` when you need the whole diff. For the payload shape,192call `get function info` on the trigger type.