@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.

Packages

Package details

extension

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, plus roster / instance_joined / instance_left lifecycle envelopes.
  • pi-office does the picture: exactly one pi process serves the office page (leader election via bind race + /healthz polling; followers take over when the leader exits). The page connects to the bridge as a plain reader and renders one avatar per instanceId.

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), … or typing… 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: person1person5. 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 in localStorage under pi-office-pins.
  • Otherwise each instanceId keeps 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.0 exposes 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 sure PI_OFFICE_HOST=0.0.0.0 (and PI_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_PORT serves; followers poll /healthz and 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 name in the pi-events config (pi-events.json in the global or trusted-project config dir, or PI_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 omit name are 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 __pinSkin console 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).