editor
v0.1.3A shared code workspace — open buffers, a file tree, unified diffs, fuzzy find and conflict-safe saves that an agent and a person see the same view of, plus a console editor page.
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
- Windows: arm64 · x64 · x86
exact versions are immutable; binary and bundle artifacts are digest-pinned.
skill doc
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::gettells you the root and what is already open. Read it before assuming anything about where you are.editor::workspace::openrepoints it. That changes what every surface sees, including the user's screen, so do not do it casually mid-task.editor::treelists a folder and carries the expansion state; passingexpandorcollapsepersists it for both surfaces.editor::buffers::listand::closeare 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::openputs 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::openfor theversion, theneditor::savewithexpected_version). - You are renaming or moving something (
editor::move— nevershell::fs::mvwhen 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, whichshell::fs::rmdoes 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 witheditor::opento 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::findmatches paths;editor::searchmatches contents. Listing a directory outside the workspace is stillshell::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::syncpulls--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
shellrejects comes back asshell's error, unchanged. editor::savewrites 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::openreturnsversion(an opaque version of the content) andmtime.- Pass
versionback asexpected_versiononeditor::save. Prefer it toexpected_mtime: mtime resolution is one second, so two writes inside the same second are indistinguishable and the later one wins silently.expected_mtimestill works and is honoured whenexpected_versionis absent; when both are sent,expected_versionis the guard. - If the content changed in between, nothing is written: the response
carries
conflict: true, the currentdisk_versionanddisk_mtime, andconflict_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: truemeans the texts match.truncated: truemeans a side was overmax_diff_bytesand no diff was computed; it does not mean "no changes".editor::open—truncated: truemeans the file was overmax_file_bytesand 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: falsemeans the folder is not a repository and candidates came from the directory walk.truncated: truemeans only the firstmax_find_candidatespaths were ranked; narrow the query.editor::git::hunks— emptyhunkswithuntracked: truemeans git has never seen the file, so there was nothing to compare against.editor::git::statusfailing 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: truemeans the search stopped atsearch_max_matches.editor::git::commit—committed: falsewith a summary is "nothing to commit", not a failure. Do not retry it.editor::git::show—exists: falsewith 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.