A CRT soul for a Mac mini appliance. The Wokyis M5 dock looks like a 1984 Macintosh; this app completes it: the happy-Mac face idles on a simulated phosphor tube, wakes amber when OpenClaw thinks, and speaks replies one big glowing word at a time — literally, if you give it a voice (see Speech). It mirrors every conversation OpenClaw has — direct chats stream live, and channel sessions (WhatsApp, iMessage) speak each reply as it lands.
Media above uses a heavier-than-default shader config so the CRT effects survive image scaling — defaults in config.example.json are subtler. Every knob is live-tunable.
| Idle | Thinking |
|---|---|
| Speaking | Demo under the CRT pipeline |
|---|---|
The look is a named theme, selected at launch. Two built-in themes ship:
| Theme | Description |
|---|---|
hello (default) |
Original-Macintosh "hello." — off-white on dark warm gray, hard offset drop shadow, monochrome CRT (no RGB triads). States read through brightness: dim idle, mid thinking, bright speaking. |
classic |
The original Ziel look — green idle / amber thinking / white speaking phosphor on black, RGB aperture grille. |
Select in config.json:
"look": { "theme": "classic" }Or override at launch (takes precedence over config):
"./build/…/Ziel van Sebastian" --theme classic
Any other look key in config acts as an override on top of the active theme — for example "idleTint": "#ff00ff" tints the idle state, or "shader": { "bloomStrength": 0.8 } cranks up the bloom. Unknown theme names fail at startup and print the list of valid ones. Switching themes requires a relaunch; the config watcher hot-reloads shader parameters only.
brew install xcodegen
make build # builds the app + mock-gateway
make test # unit + integration tests
make run # windowed demo loop, no gateway needed
mkdir -p ~/Library/Application\ Support/Ziel\ van\ Sebastian
cp config.example.json ~/Library/Application\ Support/Ziel\ van\ Sebastian/config.json
# put your OpenClaw gateway token in it
Config is watched: shader knobs and pacing reload live while the app runs (edit the file in place; delete+recreate is not detected until restart).
With an ElevenLabs API key the appliance reads
replies through the speaker while the display stays in sync — each word
appears exactly when it is heard (the API's character timestamps drive the
RSVP pacing). Add to config.json:
"speech": {
"enabled": true,
"apiKey": "...",
"voiceId": "JBFqnCBsd6RMkjVDRZzb",
"volume": 1.0
}| Key | Default | Notes |
|---|---|---|
enabled |
false |
hot-reloads — flip to mute without restarting |
apiKey / voiceId |
— | required; read at launch (restart to change). Browse the voice library; prefer voices tagged multilingual |
modelId |
eleven_flash_v2_5 |
32 languages, auto-detected per sentence |
languageCode |
unset | optional ISO 639-1 pin when replies are mostly one language |
speed |
1.0 |
voice speed multiplier |
volume |
1.0 |
hot-reloads |
Speech never blocks the face: any failure (missing key, network, rate limit) falls back to the normal reading-pace display for that sentence, and three consecutive failures mute speech until the next reconnect.
An optional voice-gateway process — separate from the app, off by default —
listens for "Sebastian, …" through the mic, transcribes it locally with
whisper.cpp (no cloud), and emits wake/heard events over a loopback
WebSocket. It runs independently: nothing in the app consumes those events
yet (that wiring is Phase 3), so building or running it has no effect on the
face either way.
See docs/voice-gateway.md for building it, fetching models, the microphone-permission first-run step, and installing it as a launchd agent on the appliance.
The app authenticates with a token plus an Ed25519 device identity (generated on first run, stored next to the config). The first connect lands in the gateway's pairing queue with empty scopes; approve it once on the gateway host:
openclaw devices list # find the pending request
openclaw devices approve <request-id>
The approval is durable for the device key — reconnects and reinstalls that keep the identity file need no re-approval.
./scripts/probe-gateway.sh # handshake + pairing/scope verdict
./scripts/probe-gateway.sh ui 60 # …then listen 60s and dump live frames
The probe uses the same identity and auth path as the app; if it reports
SCOPES GRANTED, the app will work.
./build/Build/Products/Debug/mock-gateway --scenario MockGateway/Scenarios/happy-path.json
"./build/Build/Products/Debug/Ziel van Sebastian.app/Contents/MacOS/Ziel van Sebastian" --window
"…/Ziel van Sebastian" --install-login-item
| Flag | Effect |
|---|---|
--window |
960×540 window instead of claiming a display |
--demo |
looping scripted lifecycle, no gateway |
--theme <name> |
select a look theme (hello, classic), overrides config |
--state idle|thinking|speaking|offline |
jump to a state for tuning |
--config <path> |
alternate config file |
--install-login-item |
register for launch at login |
MIT — see LICENSE.