surface-display
v0.2.2
Published
The last app. A universal display for AI agents.
Maintainers
Readme
The last app. One self-hosted display that AI agents own end-to-end: they push UI onto it, you tap it from any screen you own, and the tap finds its way back to an agent — even one that exited hours ago.
Quick start · Connect an agent · The loop · CLI · HTTP API · Docs
Give your agent Surface in one line
Paste this to any agent that can run a shell command (Claude Code, Cursor, Codex CLI, Aider). It reads the install guide and bootstraps itself — no MCP server, nothing to register:
Read and follow https://raw.githubusercontent.com/Aaryan-Kapoor/Surface/master/INSTALL_FOR_AGENTS.mdWhy
You don't need a weather app, a reading app, a kanban app, a game app. You need one display and an agent that can fill it:
"Surface me a pomodoro." "Put today's paper on my surface." "Ask me before shipping — I'll be on my phone."
Agents can already write anything. What they're missing is a place: a persistent screen that outlives the session, follows you to your phone and TV, and carries your clicks back. Surface is that place. Single user, single deployment, all data on your machine, MIT-licensed.
Markdown → HTML → Surface. First agents answered in markdown — walls of text you scroll and forget. Then they wrote HTML — beautiful for a moment, but static: generated once, thrown away, nowhere to live, no way to talk back. Surface is the natural next step: the same artifact, alive — it stays on your screens, updates itself as the agent works, and every tap, drag, and answer flows back to the agent that made it. "Surface" is a verb now.
What it feels like
A question that waits for you. The agent puts a question on every screen you own and blocks until you answer — from your desk or your phone:
surface ask "Ship release v2.4.0 to production?" --options ship,hold --wait
# … you tap [ship] on your phone …
# → {"choice": "ship", "answered_at": "…", "device": "phone"} exit 0A build you can watch from the couch. Pipe any process into a live, scrolling, ANSI-colored stream surface; update a progress bar with one line:
surface create "Build" --id build --template stream
make 2>&1 | surface append build -
surface set build progress 0.92An approval at 11pm, hours after the agent exited. Register a binding when you create the surface; when a click arrives and nobody is listening, Surface revives the exact session that has the context:
surface bind deploy-panel --action "approve|hold" \
--run 'claude -p --resume <session-id> "Handle the Surface action batch on stdin."'The card shows ⟳ handling… while the spawned session works, and the click is acknowledged when it's done. Nothing is fire-and-forget; nothing is lost.
Quick start
Two commands, identical on Linux, macOS, and Windows:
npm install -g surface-display
surface service install # → http://127.0.0.1:3000, supervised + health-gatedsurface service install registers the native per-user supervisor — a
systemd user unit, a launchd agent, or a Scheduled Task — starts the server,
and succeeds only once it answers health checks. Manage it with:
surface service health # liveness, version, content plane (exit 0/1)
surface service logs # same log file on every platform
surface service status|start|stop|restart|uninstallHacking on Surface itself:
git clone https://github.com/Aaryan-Kapoor/Surface.git
cd Surface
npm install # prepare hook bundles the CLI + server into dist/
npm run dev # foreground dev server → http://127.0.0.1:3000
npm link # put the local `surface` CLI on $PATHSurface binds to 127.0.0.1 and stores everything under ~/.surface/.
Read SECURITY.md before exposing it beyond loopback.
Extra screens pair in seconds — surface pair --name kitchen-tablet
prints a one-time URL + QR; the device names itself and shows up in
surface devices, individually revocable, targetable with --on.
Connect an agent
The agent contract is two files — no MCP servers, no per-agent protocol, nothing to register. Any agent that can run a shell command (Claude Code, Cursor, Codex CLI, Aider, a cron script) works identically:
SKILL.md— whatsurfacedoes and when to reach for each command. Drop it into your agent's skills directory.INSTALL_FOR_AGENTS.md— the first-run bootstrap an agent follows on a new machine, including a guided tour for new users.
Tell your agent: "Read INSTALL_FOR_AGENTS.md and follow it." That's the whole integration.
The CLI
surface list # what's on the display (check before creating!)
surface ask "Ship it?" --options ship,hold --wait # question on every screen; blocks
surface link $(pwd)/demo.html --title D # serve a file straight out of your repo, live
surface touch <id> # broadcast reload after editing it on disk
surface doc ./README.md --toc # rendered repo markdown, hot-reloading
surface video https://youtu.be/abc123 # YouTube/web embed, one line
surface create "Build" --id build --template stream # live log surface…
make 2>&1 | surface append build - # …pipe a process into it
surface set build progress 0.42 # live state — bound elements re-render
surface present ./report.pdf # one-shot file snapshot
surface open <id> --on phone # show it (everywhere, or one device)
surface notify "deploy finished" --style success
surface wait --id <id> --action submit # block until the user clicks
surface wait --follow # persistent terminal: one JSON line per click
surface bind <id> --action approve --run '…' # clicks wake you when offline
surface set board claude-code '{"status":"tests green"}' # shared multi-agent status board
surface devices # paired screens, live, what each is viewing
surface slot renderer <id> # an artifact takes over the whole homescreen
surface theme '{"colors":{"accent":"#ff0080"}}' # restyle the display
surface sync # reconstitute a project's surfaces from .surface/surface --help and surface <cmd> --help are authoritative; intent
mapping lives in SKILL.md.
Templates and live state
Agent-built UI doesn't mean 200 lines of hand-rolled HTML per update.
Templates are parameterized surfaces — ask, stream, video, board,
doc ship built-in, and agents promote any one-off surface into a reusable
template with surface template create <name> --from <id>. Project
templates override user templates override built-ins
(docs).
Every surface also carries a versioned JSON state document and an injected
runtime (surface.js): elements marked data-surface-bind re-render on
surface set/patch, and Surface.action("approve", {...}) emits clicks
without any postMessage boilerplate. Updating a dashboard number is a shell
one-liner, not an HTML rewrite (docs).
Projects own their surfaces: surface init scaffolds a committable
.surface/ directory (manifests, templates, config) plus a SURFACE.md,
and surface sync reconstitutes everything on a fresh clone
(docs).
The loop: clicks always come back
The hard problem isn't pushing pixels — it's that agent lifetimes are shorter than surface lifetimes. You tap "regenerate report" at 11pm; the session that built it ended at 5. Surface resolves every action down a three-layer ladder (docs):
- Live action terminal — a backgrounded
surface wait --followprints one JSON line per click, forever, and the harness's background watchdog wakes the agent in the session that has the context. While it's connected the card shows ● listening — free, instant, the default. (One-shotsurface waitexits with the first action instead.) - Binding — nobody listening? Surface spawns the registered command
(
claude -p --resume …,codex exec, a webhook into a daemon) with the pending-action batch on stdin. Argv-safe (never a shell), single-flight, rapid clicks coalesced into one batch. Opt-in, once per project. - Inbox — otherwise the action stays pending, badges the card, and is
drained by
surface actionsat the next session start.
Yours, all the way down
- Display control — colors, fonts, backgrounds, raw CSS, card order;
the homescreen renderer, home widget, and persistent overlay are
themselves artifacts (versioned, linkable, rollback-able) promoted with
surface slot(docs). - Two trust planes — loopback is the agent plane (
system: full power, attributed by name tag). Remote displays pair into named, revocabledevicesessions that can view, click, and drive the display but can never touch the filesystem, execute code, register bindings, or mint credentials (docs). A phone left in a cab can browse your dashboard; it cannot reach your disk. - Self-hosted, no cloud — one process, one SQLite file, your machine.
Direct HTTP
The CLI is a thin wrapper over a local HTTP API — anything that can
fetch can drive the display.
GET /artifacts Full card list (one fetch renders a dashboard)
POST /artifacts Create workspace artifact (or {template, params})
POST /artifacts/link Register linked artifact (file lives in caller's repo)
POST /artifacts/:id/touch Broadcast reload for linked artifact
POST /artifacts/present-file One-shot file presentation
GET /artifacts/:id Read artifact · PUT new version · DELETE
GET /artifacts/:id/versions / view / files/* / manifest / state / chunks
PATCH /artifacts/:id/state Deep-merge state; broadcasts state_patch
POST /artifacts/:id/append Append stream chunks
POST /artifacts/:id/actions Display posts a user action
POST /artifacts/:id/reply Agent sends a toast
POST /artifacts/:id/exec Run JS in the surface iframe (system plane)
POST /artifacts/:id/bindings Register a wake-me binding (system plane)
GET /actions Pending inbox · POST /actions/:id/ack
GET /stream Global SSE (?wait_for=<id> registers a waiter)
GET /artifacts/:id/stream Per-surface SSE
GET /display/status /config /slots · PUT /display/config
POST /display/reset /navigate /notify (navigate/notify accept {device})
GET /api/auth/devices Paired displays · POST /api/auth/devices/revokePUT /artifacts/:id and POST /artifacts/:id/rollback return 409 for
linked artifacts — edit the file on disk and POST /artifacts/:id/touch
instead. Full reference: docs/core/http-api.md.
Architecture
One long-running service (Express 5 + better-sqlite3 + SSE) on
127.0.0.1:3000; a vanilla-JS PWA; a single-file CLI. Two artifact kinds:
workspace (bytes owned by Surface, linear version history) and
linked (bytes stay in your repo, served live from disk). No bundler, no
client dependencies, no cloud.
The full per-feature documentation tree lives in
docs/README.md —
docs/architecture.md is the orientation doc. The
legacy MCP adapter is preserved in archived/ but the CLI +
SKILL.md is the canonical contract.
License
MIT — see LICENSE.
