RemoteControl is a ~150-line HTTP bridge that runs inside a Cuis image and exposes two endpoints — evaluate Smalltalk, and screenshot the screen. With it, an external coding agent (Claude Code, Cursor, a shell script, anything that can curl) can drive a live image: define classes, recompile methods, inspect objects, and see the result on screen, all without restarting. It's the "Dan Ingalls in a box" loop — the agent works the way a Smalltalker does, against a running system.
This is the bridge from the Blueprint project, extracted to stand alone.
POST /eval— send Smalltalk source, get the result'sprintStringback. Errors come back as HTTP status codes (200 ok / 400 undeclared variable / 500 runtime error + stack trace).GET /screenshot— PNG of the world, optionally cropped to a morph or a rectangle.ClassDescription>>safelyCompile:classified:— compile one method robustly over the bridge (auto-resumes noisy notifications, stamps it, flushes the changes file).
Everything lives on the RemoteControl class (class var Server); no Smalltalk globals are added.
- A Cuis Smalltalk image (tested on Cuis 7.x). https://cuis.st
- The WebClient feature package (provides
WebServer), which ships with Cuis underPackages/Features/WebClient.pck.st. Install it first if it isn't already:Feature require: 'WebClient'.
File in RemoteControl.st (World menu → Open… → Workspace, or drag the file onto the window, or):
Feature require: 'WebClient'. "dependency"
(DirectoryEntry currentDirectory // 'RemoteControl.st') fileIn.That adds the RemoteControl class and the safelyCompile:classified: extension. (The !requires: 'WebClient' header documents the dependency.)
RemoteControl start- Listens on port 2347 (
RemoteControl port). RemoteControl stopshuts it down ·RemoteControl isRunningqueries ·RemoteControl serverreturns theWebServer.- On macOS the first run triggers the network-permission prompt — grant it.
⚠ Never call RemoteControl start through the bridge. It tears down the very listener serving the request, and the port sits in TIME_WAIT ~30 s before it can rebind. Start/restart it from a Workspace.
Smoke test from your shell:
curl -s -X POST -H 'Content-Type: text/plain' --data-binary '3 + 4' http://localhost:2347/eval
# -> 7Content-Type: text/plain, body is the Smalltalk source (a single expression-sequence; the last expression's value is printString-ed and returned).
| status | meaning |
|---|---|
200 |
success; body is the result printString |
400 |
the source referenced an undeclared variable; body names it (the classic "I typed a Squeak/Pharo class that doesn't exist in Cuis" mistake) |
500 |
the call raised an Error; body is ClassName: message + a short stack trace trimmed at your DoIt frame |
curl -s -X POST -H 'Content-Type: text/plain' --data-binary '1 + 1' http://localhost:2347/eval # 200 -> 2
curl -s -X POST -H 'Content-Type: text/plain' --data-binary 'NoSuchClass new' http://localhost:2347/eval # 400 -> Undeclared variable: NoSuchClass
curl -s -X POST -H 'Content-Type: text/plain' --data-binary '1 foo' http://localhost:2347/eval # 500 -> MessageNotUnderstood: ...Results are UTF-8 (non-ASCII printStrings come back intact). Binary values arrive as their printString (e.g. #[137 80 78 …]) — for images use /screenshot; for other binary, have the doIt write to disk and return the path.
No params → full world PNG. Crop with:
| query | behavior |
|---|---|
?morph=ClassName |
first morph of that class (its displayFullBounds) |
?id=<identityHash> |
one specific morph, unambiguous |
?x=&y=&w=&h= |
an explicit rectangle |
?pad=<n> |
margin around a morph crop |
?raw=1 |
capture the live Display buffer (real on-screen pixels — shows drag trails / stale regions); default re-renders cleanly via imageForm: |
curl -s -o /tmp/world.png 'http://localhost:2347/screenshot'
curl -s -o /tmp/win.png 'http://localhost:2347/screenshot?morph=SystemWindow&pad=20'Single-quote any URL containing ? so your shell doesn't glob it. Don't poll /screenshot in a tight loop — each call renders the whole world and will starve the UI process.
curl -s -X POST -H 'Content-Type: text/plain' --data-binary @- http://localhost:2347/eval <<'EOF'
Integer safelyCompile: 'double
^ self * 2' classified: 'arithmetic'
EOF
# -> #doubleClass-side methods: send to the metaclass — Integer class safelyCompile: '…' classified: '…'.
Prefer one safelyCompile:classified: per /eval round-trip over batching many compile: calls in one doIt — Cuis's parser has an intermittent bug that fires on long compile sequences, and per-method round-trips dodge it.
The whole interface is curl. A few conventions make the loop reliable:
- Send Smalltalk via a file, not inline. Write the source to
/tmp/x.standcurl --data-binary @/tmp/x.st …. This sidesteps two compounding quoting layers (your shell's quotes and Smalltalk's''…''); shell(…)can even open a subshell that kills the command beforecurlruns. A file body also gives you a re-runnable artifact. - Declare all temps at the top of each doIt (
| a b c |). Cuis's parser hits an intermittentTrue>>isUndefTempbug more readily with temps declared mid-block. - Verify by reading the new source back, not by checking the symptom is gone:
(Foo >> #bar) sourceCode includesSubString: '…'. - Watch curl's exit code. Non-zero = the change didn't apply, even if a later probe looks fine. (Note: saving the image —
Smalltalk snapshot:…— freezes the VM and drops the socket;curlexits 52. That's not a failure; the snapshot completed.)
For Claude Code specifically, add a permission rule so it can call the bridge without prompting, e.g. in .claude/settings.json:
{ "permissions": { "allow": ["Bash(curl *)"] } }…and a CLAUDE.md note telling the agent the bridge exists, the port, and the four rules above. Then it can develop against your running image: write a method, compile it over /eval, screenshot the result, iterate — with your eyes as the visual judge.
Cuis is small, clean, and reproducible — a vector-graphics Morphic UI, a real package system, and an image you can snapshot. An agent operating inside a live image gets the Smalltalk superpower (everything is live, inspectable, and recompilable on the fly) without the agent needing to be a Smalltalk expert. The bridge is the thin seam that lets an outside agent reach in.
RemoteControl.st is offered freely for the Cuis community — adapt the port, add endpoints, harden it as you like. It is intentionally tiny so you can read the whole thing in one sitting.
The
PNGReadWriterclass is located in theGraphics-Files-Additionalpackage, so it also needs to be loaded with:Additionally, the following dependency should be added to
RemoteControl.st:Without this package, the code fails because
PNGReadWriteris not available.See https://gist.github.com/thiagoslino/814f313c46480a5888da2c2ca5d67d62