A web front end for the pi coding agent, built to be
left alone.
Give it a task, close the browser, come back later and read what it did.
Documentation · Deploying · Write a channel
Runs are owned by the server, not by your tab. Every event pi emits is appended to a log, so reconnecting replays exactly what you missed and then continues live.
A fresh agent session starts at roughly 3.8k tokens of system context — the agent's identity and memory, its tool schemas, and a one-line listing of every installed skill. Skill bodies are read when the agent reaches for one, not loaded up front. Measured rather than budgeted, and it grows with what you install: each MCP server registered as direct tools adds 150–300 tokens per tool.
Quick start
cp .env.example .env # set PORTAL_PASSWORD and your provider key
docker compose up -d --build
Then open http://.
The container uses host networking, so pi and its extensions reach services on
the box at 127.0.0.1 — a llama.cpp server on :8080, for example — exactly as
they would outside a container.
How it works
Browser ──SSE (replay + tail)──▶ portal ──JSONL over stdio──▶ pi --mode rpc
│
└─▶ SQLite: sessions + full event log
The browser never drives the agent. Submitting a prompt returns as soon as pi accepts it;
the run continues server-side. The client reconnects with the last event id it saw
(?since=), so nothing is lost and nothing is duplicated.
Execution modes
| EXECUTOR | What it does |
|---|---|
| host (default) | pi runs inside the portal container, working directly on the repos mounted at /projects. Fast, real git, full access to those directories. |
| container | Each task gets its own container with only its project mounted, dropped capabilities, no-new-privileges, and memory/CPU/PID caps. Needs the Docker socket mount. |
pi has no approval prompts — by design it runs with the permissions of its process
("real isolation needs to come from the OS or a container boundary"). That is what makes
unattended runs possible, and also why PORTAL_PASSWORD is required and why the portal
should stay on Tailscale/LAN rather than the public internet.
Config panel
The Config button in a task opens the web equivalent of pi's TUI slash commands, in three tabs:
- Session — model (searchable across the whole provider catalogue), thinking level, live
- Global — provider, default model and default thinking level applied to every **newly
- Packages — install, remove and update pi packages (extensions, skills, prompts, themes)
Configuration
| Variable | Default | Purpose |
|---|---|---|
| PORTAL_PASSWORD | — | Required. Shared password for the UI. |
| PORTAL_SECRET | random | HMAC key for the auth cookie. Set it so logins survive restarts. |
| WORKSPACES_DIR | /root/repos | Host directory holding the workspaces pi may work in. |
| EXECUTOR | host | host or container. |
| PI_PROVIDER / PI_MODEL | openrouter / anthropic/claude-sonnet-5 | Passed through to pi. |
| OPENROUTER_API_KEY etc. | — | Provider credentials, forwarded to pi. |
| TASK_MEMORY_MB / TASK_CPUS / TASK_PIDS_LIMIT | 2048 / 2 / 512 | Per-task caps in container mode. |
Sessions and workspaces
A workspace is a folder pi works in; a session is a conversation against one.
Creating a session defaults to making a fresh workspace — name it however you like and the
folder is slugified ("Cool Project" becomes cool-project), with the session taking that
same name. Pick an existing workspace from the dropdown to continue in one you already have.
Each session has its own pi conversation, workspace, and status. The sidebar shows them all with a live status dot: running, idle, error, or interrupted — meaning the server restarted while that task was mid-run. Sessions are marked interrupted rather than left spinning forever; sending another message resumes the conversation.
Limitations
- A session does not survive a portal restart, only a browser disconnect. pi persists its
- Two sessions pointed at the same workspace in
hostmode will edit the same working tree.
container mode or separate workspaces if you want to run those in parallel.
Documentation
Full docs live in docs/ and are a VitePress site.
npm run docs # dev server
npm run docs:build # static build into docs/.vitepress/dist