@tkreuziger/pi-office
A cozy pixel-art office for your Pi agents: one avatar per pi process, driven by the pi-events bridge in real time.
Package details
Install @tkreuziger/pi-office from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@tkreuziger/pi-office- Package
@tkreuziger/pi-office- Version
0.4.5- Published
- Aug 28, 2026
- Downloads
- 235/mo · 235/wk
- Author
- tkreuziger
- License
- MIT
- Types
- extension
- Size
- 1.2 MB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
🏢 pi-office
A cozy pixel-art office for your Pi agents: one avatar per pi process,
living in a shared 2D office. Watch your agents — tmux panes, terminal
windows, mesh agents, parallel subagents — walk to their desks, type, grab
coffee, break down on errors, and leave — in real time.
Live-rendered from the pi-events bridge, which this package consumes as a pure protocol (reader client): no shared code, only the documented wire format.
Pixel-art sprites are temporarily vendored from the MIT-licensed opencode-visualiser project (see
THIRD_PARTY_NOTICES); planned to be replaced with original art.
How it works
pi process A ──┐ pi-events bridge (leader on :8765)
pi process B ──┤ │ ws (reader) office page host (leader on :5173)
└────────┼──────────────────────────────► browser ──► http://127.0.0.1:5173
└─ roster / instance_* + envelopes (instanceId) ─► avatars
- pi-events does the data: one bridge per host/port, every pi process
contributes envelopes tagged with its
instanceId, plusroster/instance_joined/instance_leftlifecycle envelopes. - pi-office does the picture: exactly one pi process serves the office page
(leader election via bind race +
/healthzpolling; followers take over when the leader exits). The page connects to the bridge as a plain reader and renders one avatar perinstanceId.
Event → behavior mapping
| pi-events envelope | Avatar behavior |
|---|---|
roster / instance_joined |
spawn at the door, walk to the rest area |
instance_left |
walk out, fade |
turn_start, message_start (assistant), tool_execution_start |
run to a free desk, sit, work |
message_update, tool_execution_update |
keep working (typing pulse) |
turn_end |
stay at the desk (turn boundaries aren't breaks) |
| silence (~15s, no envelopes) | leave the desk → rest area (go idle) |
| long silence (~75s idle) | coffee break at the coffee nook |
tool_execution_end (isError: true) |
error animation |
tool_execution_end (edit/write, ok) |
desk flash ("saved") |
agent_end |
back to the rest area (process still registered) |
Idle (rest area and the coffee nook) is driven purely by silence: an avatar
only leaves its desk when the process has stopped emitting envelopes, and any
fresh activity pulls it straight back to work. Desk labels and the activity
log reduce tool calls to prose by tool name — "reading code…", "writing
code…", "editing code…", "running bash…" — with leading cd <cwd> && chains
stripped as navigation noise.
Skins are assigned per instanceId and persisted in the browser
(localStorage) — no files are written anywhere.
- Live tool labels: each desk shows what its agent is doing right now
(
$ command,read path,write file (N lines), … ortyping…while text streams without a tool). - Idle drift: resting agents occasionally wander to a new spot in the rest area — life continues even between turns.
- Up to 8 agents (desks); beyond that, agents queue in the rest area.
- A status chip in the corner shows agent count and bridge connectivity.
- A settings gear (top-right) opens a panel to force a skin for all avatars, switch the bridge URL live, or hide the status chip.
Skins & pinning
Skins: person1–person5. Assignment order: ?skin=personN query override >
per-project pin > per-instance stored skin > free random.
?skin=person2— force every avatar to one skin (nice for demos).- Pinning a project to a skin (power users): open the office devtools and run
__pinSkin("/absolute/project/path", "person3");__pinSkin(path, null)clears it. Pins live inlocalStorageunderpi-office-pins. - Otherwise each
instanceIdkeeps its skin across page reloads.
Install
Both packages, globally (so every pi process — including subagent children — joins the same bridge and office):
pi install -l pi-events # event bridge (or path/git until published)
pi install -l pi-office # the office page host
Local development in this repo: run pi with this directory (or the
pi-events repo) as cwd — .pi/extensions/ auto-loads the extension.
Run
Just start pi — the office comes up at session start:
pi
[pi-office] office ready at http://127.0.0.1:5173
Open http://127.0.0.1:5173 (or set PI_OFFICE_OPEN_BROWSER=1 to have it
opened automatically in a Chrome app-style window). Each connected pi process
appears in the office within seconds.
Configuration
Same layered pattern as pi-events — last one wins: defaults < global
pi-office.json < project pi-office.json < env vars.
Config file example (.pi/pi-office.json or ~/.pi/agent/pi-office.json):
{
"host": "127.0.0.1",
"port": 5173,
"openBrowser": false,
"debug": false
}
| Env var | Default | Purpose |
|---|---|---|
PI_OFFICE_HOST |
127.0.0.1 |
Page host bind address (0.0.0.0 for LAN) |
PI_OFFICE_PORT |
5173 |
Page host port |
PI_OFFICE_OPEN_BROWSER |
(off) | Open a browser window when this process wins the election (1/true) |
PI_OFFICE_DEBUG |
(off) | Verbose logging (1/true) |
The page's bridge URL follows PI_EVENTS_HOST / PI_EVENTS_PORT
(e.g. ws://127.0.0.1:8765, exposed at http://127.0.0.1:5173/bridge), or
override per-view with ?bridge=ws://127.0.0.1:9000 in the URL.
LAN / another machine
The office and the bridge default to loopback. To watch from another machine on the same network, bind both to all interfaces on the pi machine:
PI_EVENTS_HOST=0.0.0.0 PI_OFFICE_HOST=0.0.0.0 pi
Then open http://<pi-machine-ip>:5173 from the other machine. The page's
/bridge endpoint automatically advertises the machine's LAN IP (never
0.0.0.0), so a remote browser connects to the right WebSocket with no extra
configuration. Peers on the pi machine itself still dial loopback — wildcard
binds never break multi-instance or office failover.
Security: binding
0.0.0.0exposes the live session-event stream (tool outputs, file contents) to every device on the network — the bridge has no auth by design. Only do this on networks you trust, and consider a firewall rule permitting just your own devices.
Project config is only honored for trusted projects (use /trust or
--approve), matching pi's project-trust model.
Troubleshooting
- The page says "Upgrade required". That's the pi-events bridge port
(8765 by default) answering a plain HTTP request — it's a WebSocket-only
endpoint, not the office. The office page is served on port 5173
(
http://127.0.0.1:5173). Opening the bridge port in a browser now shows a notice pointing to the office instead of the bare 426 text; make surePI_OFFICE_HOST=0.0.0.0(andPI_EVENTS_HOST=0.0.0.0) are set on the process that wins the office election when you want LAN access — a loopback-bound office is not reachable from other machines even when the bridge is. - Agents visible on the LAN machine but not the office — see above: the bridge may be wildcard-bound while the office stays loopback-only.
Behavior notes
- Page host leader election: first process to bind
PI_OFFICE_PORTserves; followers poll/healthzand take over when the leader exits. - The page reconnects to the bridge with a 10s grace period; if the bridge is gone, everyone walks home (the office empties).
- The office host caches assets in memory (and browsers keep them for 1h via
max-age=3600); to refresh a texture/art file mid-session, bump a query string (textures/background.png?v=2) — the server's cache key includes it, so both caches bust without renaming files or restarting pi. - One avatar per pi process: the bridge already folds duplicate registrations from the same OS process (extension loaded by both the CLI wrapper and the session runtime) into a single instance. As a display-level safety net, the page additionally prunes any instance that never emitted a single envelope while a same-cwd twin is active.
- Display names: set
namein the pi-events config (pi-events.jsonin the global or trusted-project config dir, orPI_EVENTS_NAME) to give an instance a friendly name — the office shows it on the desk label and in the activity log instead of the cwd folder name. Purely visual; unset keeps the cwd default (older bridges that omitnameare unaffected). - No persistence, replay, auth, or multi-user features.
Development
npm install
npm run typecheck # tsc --noEmit
npm test # standalone page-host + config tests (no pi needed)
npm run live # dev loop: office URL + live bridge stream
End-to-end check (real pi, both packages, from two separate projects):
node test/live.mjs 5181 18778 &
cd ../some/project && PI_EVENTS_PORT=18778 PI_OFFICE_PORT=5181 pi
Package layout
src/config.ts layered config resolution (pi-office.json + PI_OFFICE_*)
src/server.ts OfficeHost (static server + /healthz + /bridge) and
OfficeElection (bind race + health polling failover)
src/browser.ts best-effort Chrome app-window launcher
src/index.ts pi extension factory (process-lifetime election)
office/ the page (zero-build canvas app) + vendored pixel art
test/ standalone tests + live dev loop
Roadmap
- Replace vendored pixel art with original sprites
- Per-desk activity sparkline (turns/tools per minute)
- Optional per-project skin pinning UI (currently via
__pinSkinconsole helper) - Since-we-last-saw-you summaries on long-running sessions
License
MIT. Sprites: see THIRD_PARTY_NOTICES (vendored, MIT, © 2026 Fail —
upstream: opencode-visualiser).