ompweb is a local, browser-based workspace for the
oh-my-pi (omp) coding agent. It lets a
user browse the same local sessions they use in the terminal, continue live
work, configure supported OMP settings, and inspect project files without
creating a second agent runtime or a second source of truth.
The project originated from agegr/pi-web (MIT), but it is maintained as an OMP-focused downstream. We preserve the license and attribution, and selectively learn from upstream improvements; we do not assume that Pi-specific implementation changes can be merged unchanged.
- OMP remains authoritative. Sessions, credentials, providers, and agent
behavior belong to omp (run as ompweb's own build of it,
runner/). ompweb must not invent a parallel data format or credential store. - Local-first by default. The server binds to
127.0.0.1; remote access is an explicit user choice and must be protected by a trusted network boundary and HTTPS. - Node-first installation. A normal user installs Node.js 24+ and OMP,
then runs
npx ompweb@latestor installsompwebglobally. The npm package brings its own Bun for the web server, so users do not install Bun themselves. - Native compatibility over imitation. Prefer OMP's CLI and documented on-disk formats to copied SDK internals. If a capability cannot be done safely through those boundaries, leave it out rather than emulating it speculatively.
- A calm, capable workspace. The UI should make active work, session history, configuration, and project context understandable without hiding the agent's state or expanding the app into a general remote-control plane.
- npm package and CLI command:
ompweb. - Default server address:
http://127.0.0.1:30177. - Existing
OMP_WEB_*environment variables remain the configuration prefix for compatibility:OMP_WEB_HOSTNAME,OMP_WEB_NO_OPEN, andOMP_WEB_OMP_BIN.OMP_WEB_PASSWORDis accepted only as a migration step (see the security contract). PI_CODING_AGENT_DIRand OMP's own directory conventions are respected because they identify the user's existing OMP state.- The web UI displays its own package version separately from the detected installed OMP version; those versions may legitimately differ.
Browser
│ HTTP / Server-Sent Events
▼
ompweb (Bun server, started by the Node launcher)
├─ reads native OMP session files and selected configuration
├─ serves allow-listed project files
└─ starts one `ompweb-omp --headless` child per active session
│ ompweb channel, NDJSON over stdio
▼
ompweb's own omp build (runner/) and the existing ~/.omp/agent state
OMP SDK packages are Bun-only TypeScript and import Bun APIs. Importing
@oh-my-pi/* or @earendil-works/* into the ompweb server, even though it runs on
Bun, would tie it to omp's internals and make the application unreliable.
Therefore, the ompweb server and daemon must not add those runtime dependencies.
Live work goes through ompweb's own omp build, runner/, compiled against the
fork's integration build: an ompweb-dev/oh-my-pi release that is an upstream omp
release plus the PRs the fork carries (among them runSessionHost, which the
headless mode needs), pinned to one release in runner/omp-build.json and
re-pinned to the newest one by each ompweb release. The runner replaces omp's
launch command: its session factory connects every session to ompweb host
over ompweb's channel. The host receives the same AgentSession events and
calls the same session methods omp's TUI does, so nothing depends on omp's RPC
mode. A session started from the runner's TUI connects to the daemon's Unix
socket and shows up live in the web; dialogs open on both sides and the first
answer wins. Each ompweb release attaches its runners (ompweb-omp-<platform>),
and every install downloads the one of its own version
(lib/omp/runner-fetch.ts), so omp updates with ompweb. The design and the wire are in
docs/specs/2026-11-12-omp-runner-design.md; the fork build in
docs/specs/2026-11-12-omp-build-in-ompweb-design.md.
Every machine-local action of a session goes through one interface,
Host (lib/hosts/host.ts): spawning ompweb-omp --headless, checking that a
session file exists, invalidating cached session lists, and the
file-reading operations behind the session and project routes. The web server
has no direct path to any machine, its own included: it reaches every host
through the link described below. The implementation on the web server is
RemoteHost; the daemon (ompweb host) runs the other side, LocalHost, on
the machine whose omp it owns.
Browser ─▶ ompweb ◀── link (loopback) ── ompweb host ── spawn ompweb-omp --headless (stdio)
◀── link (WebSocket) ── ompweb host on another machine
└─ spawn ompweb-omp --headless
"This machine" is the host with the fixed id local, linked to the daemon
that the service installers set up next to the web service. Because the daemon
owns the omp processes, restarting or redeploying the web server does not end
a run: the restarted server adopts the live children from the daemon's hello,
with their open blocking requests replayed. A direct spawn path would tie
every run to the web server's lifetime, which is why none exists.
The omp boundary above holds on every machine: a remote machine runs its own
runner build, its own ~/.omp/agent and its own credentials, unless sharing is
turned on for that host (Settings → Hosts): then its sessions use the server's
provider logins through omp auth-broker serve, and the host never gets a
refresh token. ompweb only routes. RpcProcess takes its byte pipe
(RpcTransport) from the host, so the
session wrapper, event streams, notifications and queue handle frames without
knowing where omp runs. Session routes address a session by id; the owning
host resolves the file path, so a remote path never reaches the serving
machine's filesystem.
The link is one WebSocket that the ompweb host daemon opens to the
server: text frames carry the messages in both directions and binary frames
carry large op results (daemon → server). The daemon
forwards omp's NDJSON unchanged and answers a fixed allow-list of host
operations; there is no generic exec or file-read operation. A host is
trusted like the serving machine: control of the server or of a host token is
a shell on that host. Tokens are stored hashed, shown once, and revocable, and
the daemon requires https:// except for loopback or an explicit
--allow-http. The full protocol is in
docs/specs/2026-10-28-remote-hosts-design.md.
~/.omp/agentis the source of truth for sessions, configuration, models, skills, plugins, and blobs.agent.dbcontains authentication data. ompweb never reads or writes it; authentication actions go through theompweb-ompprocess.- A live OMP process owns writes to its session file. ompweb routes supported live actions through the runner's channel and never races a live file rewrite.
Session browsing is implemented in pure Node against OMP JSONL files. The
reader tolerates the fixed title slot and older session shapes, resolves blob
references when needed, and builds the active branch context from the entry
tree. The session and project routes run it in the daemon of the session's host
(session-ops.ts), so the same code serves this machine and every linked one.
Direct session mutation is deliberately narrow and explicit: rename/title, archive, deletion, and required branch-parent maintenance. These writes are atomic where possible; archive or deletion stops the associated live process first. ompweb does not provide a general editor for session JSONL or opaque OMP state.
Models and allow-listed OMP settings use surgical YAML updates that preserve
unrelated content. Plugin operations run the installed omp plugin CLI. MCP
configuration is project-local, validated before writing, and saved atomically.
Completed conversation history is identified by persisted session entry IDs.
Live message_end frames have no durable ID and can arrive before OMP writes
the entry, so they trigger catch-up rather than append an unidentified copy.
Streaming text, active tool output, and an optimistic user prompt remain
separate from confirmed history.
GET /api/sessions/:id/context?sync=1 returns a SessionSyncResponse:
cursorcontainsfirstEntryIdandlastEntryId(both null for empty history). Send its JSON value in the next request'scursorquery parameter.mode: "append"returns only messages after that position;baseEntryIdidentifies the expected client prefix. An orphaned cursor or changed compaction prefix returnsmode: "replace".context.messagesandcontext.entryIdsremain aligned.limitis an integer from 1 to 200 (default 200); followhasMorewith the returned cursor. Only the selected page's image blobs are resolved.- Existing
leafId,includePreCompaction,deferThinking, anddeferMediaview parameters remain available. A historical view excludes live output. livecontains the web-owned process's current partial message, active tool snapshots, and lifecycle flags, or null for a file-only session. Reading history never starts an OMP process. A live process can supply its first partial before its session file exists, but a missing file cannot erase a nonempty confirmed cursor.live.responseObservedretains positive visible-answer evidence for the current run after completion, even before its entry is readable on disk. New runs and process/session changes reset it; tool calls alone do not count.
GET /api/sessions/:id/context?boundary=1 returns only { entryIds } for
the current active context. Prompt and interrupt dispatch use this fresh
ID-only boundary instead of downloading the transcript. It shares the history
index without hydrating message bodies or blobs and cannot be combined with
sync, pagination, or historical-view parameters.
SSE events carry web: { streamId, sequence }. The stream epoch changes when
the native process or session identity changes. These values order live
snapshots; they are not a persisted replay journal or Last-Event-ID support.
Subscription precedes the connected cursor announcement. The client keeps
processing live events while history loads and applies snapshot fields only
when they cannot overwrite newer message, tool, or lifecycle state.
Open/reopen, foreground/online, message completion, and persisted-file notifications share one coalesced catch-up loop. Pages are accumulated privately and published only when the selected history is complete. Failed reads retain the last complete cursor and displayed history. Overlapping full loads preserve newer complete history rather than treating an intermediate page as authoritative. Full initial loads and terminal metadata refreshes still update the branch tree. A completion or persistence notification during an in-flight read schedules one follow-up read from the newly returned cursor. Raw SSE completions never append a second copy of an entry that the history response already includes.
Unchanged history pages reuse a file-versioned offset index and seek only the requested message bodies. The in-memory index cache is bounded to 32 views and 32 MiB of charged metadata; it does not retain transcript bodies or increase the existing 256 MiB raw-file cache budget or 1 GiB load ceiling. Cold indexes and changed files require a full metadata scan. Growth alone is not proof of an append: the same inode can be rewritten and then extended, so any file version change invalidates offsets rather than trusting an unchecked prefix. Failed reads never advance the confirmed cursor.
File-only catch-up also refreshes model and thinking metadata without replacing newer or pending channel choices. Replacement observer connections restore browser registrations and the subagent roster only after successfully connecting.
Deploy the frontend and API support together; no native OMP upgrade is required.
- Bind loopback-only by default. A non-loopback hostname is an explicit opt-in.
OMP_WEB_PASSWORD_HASHprotects every route with a password-only sign-in screen. Only a scrypt hash reaches the server (ompweb hash-password), because everyompsession inherits the server's environment and an agent could print the password into its transcript, session file and model-provider traffic.--password, or a plaintextOMP_WEB_PASSWORDnext to a hash, refuses to start. A loneOMP_WEB_PASSWORDis hashed by the launcher, removed from the server's environment, and logged as a warning with the hash to use instead; the server itself still refuses a plaintext. Successful sign-in creates an HTTP-only, signed cookie with a 30-day expiry; the signing key is a per-installation random value mixed with the hash, so a copied password hash cannot forge a session and changing the password still invalidates existing sessions. Exposed deployments require HTTPS through a trusted reverse proxy or VPN. WithOMP_WEB_TRUSTED_HEADER_SHA256set, a request whoseX-Omp-Web-Authheader hashes to that digest counts as signed in; omp-web never holds the secret itself. Deciding which clients get the header is the reverse proxy's job, because omp-web has no client address it can trust.- omp-web's own secrets (
OMP_WEB_PASSWORD,OMP_WEB_PASSWORD_HASH,OMP_WEB_TRUSTED_HEADER_SHA256and its legacy nameOMP_WEB_TRUSTED_HEADER_SECRET) are stripped from every child process environment throughcreateChildProcessEnvironment(lib/project-command-env.ts).OMP_WEB_*values that are configuration rather than secrets (OMP_WEB_PACKAGE_DIR,OMP_WEB_OMP_BIN, ...) are kept. - API requests are origin-checked. Do not add browser-to-host execution paths that bypass this boundary.
- Host tools (
host_tool_call) come from a fixed set: the browser's own tools (open a URL or file, notify) and the server's session tools inlib/agent-tools.ts; none runs a host command. A browser request must not become arbitrary host command execution through an extension callback. - File APIs are not a general filesystem browser. They are restricted to selected workspaces, valid Git worktrees, session-referenced directories, and explicitly selected roots. Paths are canonicalized to reject traversal and symlink escapes.
- Secrets, raw API keys, and auth database contents never appear in API responses, logs, or the browser.
- The session sidebar is the durable navigation model: projects, sessions, branches, worktrees, and files must agree about the selected workspace.
- Streaming state is explicit. The UI reconciles Server-Sent Events with the session's state so a background tab cannot remain falsely “running”.
- Desktop and mobile share the same core workflow. Mobile controls keep usable touch targets and a visible loading state rather than a blank shell.
- Accessibility and motion preferences are first-class. Components use the shared design tokens and UI primitives rather than one-off colors or controls.
- Expensive rendering is deferred until needed; responsiveness and initial bundle size are part of the product contract.
agegr/pi-web is the historical source and a useful source of UI ideas,
bug fixes, and tests. Before adopting an upstream change, verify that it does
not depend on Pi runtime behavior or Bun-only APIs. Port the user-visible
behavior, not blindly the implementation.
Releases are independent:
- Run typecheck, lint, relevant tests, and a production build.
- Confirm
npm pack --dry-runcontains the builtdist/output and exposes theompwebbinary. - Publish
ompweb@<version>only from an npm account authorized for that package. - Tag and release the repository that owns this downstream project.
- Reimplementing OMP, its provider registry, or its credential database.
- Embedding Bun-only OMP SDK packages in the omp-web server.
- Turning a local agent workspace into an internet-facing multi-user service.
- Unrestricted filesystem browsing or arbitrary browser-triggered host tools.
- Automatic bulk synchronization from
agegr/pi-web.
When a proposed feature conflicts with one of these boundaries, preserve the boundary unless the design is intentionally revised first.