shell/ compatibility extension — a DROPPABLE classpath plug-in (drop the
jar, drop the feature). Bound only when the user-owned shell toggle is ON
(default ON; flip it OFF in Settings or in vis.yml via toggles: {shell: false}
to drop the tools). The OS process jail is the containment layer while active.
ONE model-facing binding — shell — bound BARE in the flat Python sandbox
next to git / cat / grep. There is no shell_run / shell_bg /
shell_logs / shell_send quartet any more: four names for ONE subsystem
meant four call shapes and four result shapes for what is a single process
lifecycle. One tool, one op grammar, one TOTAL result map:
RUN (default) await shell(cmd) / await shell(cmd, opts) — bash -lc in the
workspace root, waits up to a timeout. Output is bounded at READ time to a
head+tail budget per stream, so only the MIDDLE of a huge stream is
dropped, never its start or end (a chatty-then-killed command cannot
balloon the heap). A non-zero exit is DATA the model reads, not an error.
BACKGROUND await shell(cmd, {"op": "background", "id": "dev"}) — an id
makes it background (the op may stay implicit): spawned under a REAL pty,
its merged output pumped into a bounded ring buffer, registered as a session
RESOURCE in internal.resources (footer count, F4 dialog, resources ctx
block). Prefer this for long builds, test suites, servers, watchers, and
interactive commands; reserve run for short bounded work.
LOGS / SEND / STOP await shell({"op": "logs", "id": "dev"}) — tail the ring
buffer, type into the pty, or kill the tree and drop the resource. Shell stop
and await resource_stop(id) land on the same resources/stop!. An EXITED process
is not auto-pruned, so its output and exit code stay readable until it is
stopped.
Commands have ONE spelling in Python: the first positional argument for
run/background. Resource IDs also have ONE spelling: id inside the options map
for background/logs/send/stop. Native JSON still carries a cmd property; the symbol's
:call shape converts that transport field to the Python positional before
dispatch.
Every op answers with the SAME key set (shell-result-base): keys a stage
does not fill are nil / false / 0 instead of absent, so model Python indexes
any field without a KeyError.
The shell toggle is registered HERE, extension-owned under the vis namespace.
`shell/` compatibility extension — a DROPPABLE classpath plug-in (drop the
jar, drop the feature). Bound only when the user-owned `shell` toggle is ON
(default ON; flip it OFF in Settings or in `vis.yml` via `toggles: {shell: false}`
to drop the tools). The OS process jail is the containment layer while active.
ONE model-facing binding — `shell` — bound BARE in the flat Python sandbox
next to `git` / `cat` / `grep`. There is no `shell_run` / `shell_bg` /
`shell_logs` / `shell_send` quartet any more: four names for ONE subsystem
meant four call shapes and four result shapes for what is a single process
lifecycle. One tool, one `op` grammar, one TOTAL result map:
1. RUN (default) `await shell(cmd)` / `await shell(cmd, opts)` — `bash -lc` in the
workspace root, waits up to a timeout. Output is bounded at READ time to a
head+tail budget per stream, so only the MIDDLE of a huge stream is
dropped, never its start or end (a chatty-then-killed command cannot
balloon the heap). A non-zero exit is DATA the model reads, not an error.
2. BACKGROUND `await shell(cmd, {"op": "background", "id": "dev"})` — an `id`
makes it background (the op may stay implicit): spawned under a REAL pty,
its merged output pumped into a bounded ring buffer, registered as a session
RESOURCE in `internal.resources` (footer count, F4 dialog, `resources` ctx
block). Prefer this for long builds, test suites, servers, watchers, and
interactive commands; reserve run for short bounded work.
3. LOGS / SEND / STOP `await shell({"op": "logs", "id": "dev"})` — tail the ring
buffer, type into the pty, or kill the tree and drop the resource. Shell stop
and `await resource_stop(id)` land on the same `resources/stop!`. An EXITED process
is not auto-pruned, so its output and exit code stay readable until it is
stopped.
Commands have ONE spelling in Python: the first positional argument for
run/background. Resource IDs also have ONE spelling: `id` inside the options map
for background/logs/send/stop. Native JSON still carries a `cmd` property; the symbol's
`:call` shape converts that transport field to the Python positional before
dispatch.
Every op answers with the SAME key set (`shell-result-base`): keys a stage
does not fill are nil / false / 0 instead of absent, so model Python indexes
any field without a KeyError.
The `shell` toggle is registered HERE, extension-owned under the vis namespace.(shell cmd)(shell opts)(shell cmd opts)In python_execution, await every call:
await shell("git status")
await shell("npm run build", {"op": "background", "id": "build", "cwd": "web"})
await shell("npm run dev", {"op": "background", "id": "dev-server"})
await shell({"op": "logs", "id": "dev-server", "n": 500})
await shell({"op": "send", "id": "dev-server", "text": "y"})
await shell({"op": "stop", "id": "dev-server"})
THE one shell tool. Commands have one Python spelling: the first positional argument to run/background. Resource ids also have one spelling: "id" in the options map for background/logs/send/stop. op defaults to "run", or to "background" when an id is present.
Stages:
run — bash -lc in the workspace root; blocks until the command exits. Reserve it for short bounded commands. opts: timeout_secs (default 120, max 600), cwd. A non-zero exit is DATA to read, not a tool failure.
background — returns immediately with no timeout and makes the shell an OWNED session resource under id. PREFER it for commands that may take a while: builds, test suites, daemons, watchers, and interactive work. Poll with logs instead of blocking a run call. Re-starting a LIVE id does NOT spawn a second process and is not an error: you get that shell back with "already_running": true. Reusing an EXITED id discards its retained logs.
logs — last 200 lines, or n up to 2000.
send — write text to the pty stdin; enter (default true) appends the newline that SUBMITS the line.
stop — kill the process tree, discard retained logs, and drop the session resource. Uses the same stop path as resource_stop.
EVERY op returns the SAME keys — {"stage", "id", "cmd", "cwd", "stdout", "stderr", "exit", "duration_ms", "timed_out", "timeout_secs", "stdout_truncated", "stderr_truncated", "stdout_omitted_chars", "stderr_omitted_chars", "pid", "status", "uptime_ms", "attach", "socket", "already_running", "note", "lines", "line_count", "dropped", "sent", "stopped"} — always present, None/false/0 rather than missing, with "stage" naming the op that ran (r["op"] is the tool origin every native result carries, always "shell").
Gotcha: oversized run output is truncated in the MIDDLE with an inline "…[N chars omitted]…" marker, so a truncated stream is NOT parseable — check "stdout_truncated" before json.loads(r["stdout"]) and re-run with a narrower or aggregated command.
Gotcha: "lines" is [seq, text] pairs (not strings); shown count is len(lines), "line_count" is total-ever. Only a RUNNING shell accepts send. A step only a HUMAN can finish (browser OAuth, a device-code prompt) can't be typed by the agent — tell the user to run vis extension shell attach <id> in their own terminal, then detach with Ctrl-] (the child keeps running); the result carries the exact attach command.
In `python_execution`, await every call:
await shell("git status")
await shell("npm run build", {"op": "background", "id": "build", "cwd": "web"})
await shell("npm run dev", {"op": "background", "id": "dev-server"})
await shell({"op": "logs", "id": "dev-server", "n": 500})
await shell({"op": "send", "id": "dev-server", "text": "y"})
await shell({"op": "stop", "id": "dev-server"})
THE one shell tool. Commands have one Python spelling: the first positional argument to run/background. Resource ids also have one spelling: `"id"` in the options map for background/logs/send/stop. op defaults to "run", or to "background" when an id is present.
Stages:
run — bash -lc in the workspace root; blocks until the command exits. Reserve it for short bounded commands. opts: timeout_secs (default 120, max 600), cwd. A non-zero exit is DATA to read, not a tool failure.
background — returns immediately with no timeout and makes the shell an OWNED session resource under id. PREFER it for commands that may take a while: builds, test suites, daemons, watchers, and interactive work. Poll with logs instead of blocking a run call. Re-starting a LIVE id does NOT spawn a second process and is not an error: you get that shell back with "already_running": true. Reusing an EXITED id discards its retained logs.
logs — last 200 lines, or n up to 2000.
send — write text to the pty stdin; enter (default true) appends the newline that SUBMITS the line.
stop — kill the process tree, discard retained logs, and drop the session resource. Uses the same stop path as resource_stop.
EVERY op returns the SAME keys — {"stage", "id", "cmd", "cwd", "stdout", "stderr", "exit", "duration_ms", "timed_out", "timeout_secs", "stdout_truncated", "stderr_truncated", "stdout_omitted_chars", "stderr_omitted_chars", "pid", "status", "uptime_ms", "attach", "socket", "already_running", "note", "lines", "line_count", "dropped", "sent", "stopped"} — always present, None/false/0 rather than missing, with "stage" naming the op that ran (r["op"] is the tool origin every native result carries, always "shell").
Gotcha: oversized run output is truncated in the MIDDLE with an inline "…[N chars omitted]…" marker, so a truncated stream is NOT parseable — check "stdout_truncated" before json.loads(r["stdout"]) and re-run with a narrower or aggregated command.
Gotcha: "lines" is [seq, text] pairs (not strings); shown count is len(lines), "line_count" is total-ever. Only a RUNNING shell accepts send. A step only a HUMAN can finish (browser OAuth, a device-code prompt) can't be typed by the agent — tell the user to run `vis extension shell attach <id>` in their own terminal, then detach with Ctrl-] (the child keeps running); the result carries the exact `attach` command.(shell-attach-command _parsed residual)vis extension shell attach <id> — the human-side passthrough: join a live
background shell's PTY in your OWN terminal (finish a browser OAuth, answer a
prompt only a person can), then Ctrl-] to detach with the child untouched.
--socket PATH targets an explicit socket; otherwise the newest shell whose
id matches. Returns the attach exit code.
`vis extension shell attach <id>` — the human-side passthrough: join a live background shell's PTY in your OWN terminal (finish a browser OAuth, answer a prompt only a person can), then Ctrl-] to detach with the child untouched. `--socket PATH` targets an explicit socket; otherwise the newest shell whose id matches. Returns the attach exit code.
CLI surface mounted under vis extension shell. Only attach for now — the human
passthrough onto a background PTY the agent spawned.
CLI surface mounted under `vis extension shell`. Only `attach` for now — the human passthrough onto a background PTY the agent spawned.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |