Skip to content

Repository files navigation

omp-gui

A browser and desktop GUI for oh-my-pi, built as a separate project on top of omp's documented RPC protocol.

Addresses oh-my-pi#5742.

Under active development, and it needs testing. Everything below works against the author's own setup — one machine, one provider, one workspace at a time. Other providers, other platforms, and sustained multi-session use are unverified. Treat rough edges as expected and please report them.

Why this is a separate repository

The upstream issue has been open since 2026-07-16 and is blocked on a maintainer scope decision, not on implementation. CONTRIBUTING.md requires prior Discord discussion for "new subsystems, large UI changes, new dependencies, and changes that span several packages" — a GUI is all four — and warns that discussion does not guarantee a merge.

Building outside the tree turned out to cost very little, because the pieces a GUI needs are already published:

Need Already exists
Agent protocol omp --mode rpc — ~40 commands, documented
Protocol client RpcClient, shipped inside the npm package
Event shapes @oh-my-pi/pi-wire, published to npm
Transcript + 30 tool renderers packages/collab-web, MIT (vendored — see below)

What did not exist, and is what this repository actually adds: a daemon that owns sessions rather than attaching to a TUI-hosted one, a WebSocket transport, concurrent multi-session support, and a session list.

Architecture

browser (React SPA)
      │  WebSocket  ── @omp-gui/protocol
      ▼
omp-gui daemon (Bun)
      │  one child process per session
      ▼
omp --mode rpc          omp --mode rpc          …
   (session A)             (session B)
  • packages/protocol — the daemon↔browser wire contract. Depends only on @oh-my-pi/pi-wire, so the browser never pulls in the coding agent.
  • packages/daemonBun.serve HTTP + WebSocket. Wraps one RpcClient per session, assigns sequence numbers to agent events, keeps a ring buffer for reconnect replay, and serves the built SPA.
  • packages/web — React client. First-party shell (session list, header, composer) around vendored upstream renderers.

Two findings that shaped the design

RPC's AgentSessionEvent is a structural superset of pi-wire's AgentEvent. Shared variants carry at least the fields pi-wire declares, so the daemon forwards agent events verbatim and upstream's renderers consume them with no translation layer.

RPC is single-session per process. switch_session swaps which session a process drives; it does not multiplex. Concurrent sessions therefore mean concurrent processes — which also contains a crash to one session.

Tool approval is not its own protocol frame. The tool wrapper asks by calling uiContext.select(prompt, ["Approve", "Deny"]), so approval reaches a host as an ordinary extension_ui_request — indistinguishable from a dialog an extension raised for itself. It also carries no timeout, so an unanswered one stalls the turn forever. See Extension UI.

Transcript consistency

Live events drive the transcript during a turn so text streams in. They are not treated as the source of truth: at agent_end the client re-attaches for a full resync from the daemon. Optimistic rendering stays responsive without drifting permanently out of step with the session on disk.

Quick start

Everything runs in Docker. The one hard requirement is a glibc base image — omp's pi-natives addon ships glibc-only builds and fails to load on Alpine (linked against glibc (DT_NEEDED libm.so.6)).

# Build the web bundle and start the daemon.
docker compose run --rm tools run build
WORKSPACE=/path/to/your/project docker compose up

The daemon prints a URL with the access token in the fragment:

open: http://127.0.0.1:7890/#token=…

The client moves that token into sessionStorage and strips it from the address bar, so it does not linger in history or a screenshot of the URL.

Local (non-Docker)

bun install
bun run build
bun run serve

Requires Bun 1.4+ and a configured omp provider — the daemon spawns the real omp CLI, which exits if no model is available.

Development

bun run check        # tsgo typecheck + biome
bun run smoke        # boots the daemon and exercises the protocol end to end
bun run build        # web bundle into packages/web/dist

bun run smoke covers auth rejection, the hello/list/attach/close flow, a real session spawn, a full extension UI round trip, and slash command listing and output. scripts/fixtures/ holds a probe extension that raises one dialog per session, so the approval path is exercised without a model call. Session creation needs a provider key:

ANTHROPIC_API_KEY=sk-ant-dummy bun run smoke

Docker + bun install

Install node_modules onto a volume, not a bind mount. Bun's default hardlink backend cannot link across the bind-mount boundary and fails with Fail extracting tarball. docker-compose.yml already does this; the ompgui-nm volume in the dev setup is the same idea.

Bun 1.4 uses isolated installs: the store lives in node_modules/.bun and each workspace gets symlinks. An empty-looking root node_modules is expected.

Vendored upstream code

packages/web/src/vendor/ is copied from oh-my-pi's packages/collab-web — the transcript, the markdown renderer, and ~30 per-tool React renderers. That package is private: true and unpublished, so vendoring is the only way to consume it. It is MIT-licensed; see NOTICE.

Refresh it with:

bun run vendor:collab-web -- --source /path/to/oh-my-pi

Two consequences of keeping those copies byte-identical:

  • Biome excludes src/vendor/**. Formatting it would produce a diff against upstream on every re-vendor.
  • The project does not set noUncheckedIndexedAccess, because upstream does not. See packages/web/src/vendor/README-STRICTNESS.md.

Type-checking uses tsgo (upstream's checker) rather than tsc. They differ on one line in the vendored markdown renderer; matching upstream's checker is what keeps vendored code clean.

Extension UI

Anything the agent needs a human for — tool approval, an extension's own confirm/select/input/editor dialog — arrives over one RPC channel as an extension_ui_request. The daemon forwards the four blocking methods to the browser and answers them from the client's reply. notify and open_url become notices; setStatus, setWidget, setTitle, and set_editor_text are terminal chrome with no browser equivalent and are dropped.

Three things about this path are easy to get wrong:

  • Dropping a blocking request wedges the session. Approval dialogs carry no timeout, so nothing settles them but an answer. Listeners are therefore attached before RpcClient.start(): an extension can raise a dialog from session_start, and a request emitted during startup would otherwise be dropped and the session would come up already stuck.
  • A blocked agent emits no events, so pending requests ride on the attached snapshot rather than the event replay. Reconnecting mid-dialog shows the dialog again instead of a session that looks hung for no reason.
  • A deadline is sent as time remaining, not as an instant. omp expires its own dialogs without telling the host, so the client has to expire them too, and neither side has to trust the other's clock.

Approval mode defaults to write (prompt before exec-tier tools) rather than omp's own yolo, which is the right default only for an embedding that cannot ask. OMP_GUI_APPROVAL_MODE overrides it.

The upstream patch

RpcClient drops two classes of server frame that a host actually needs, so patches/ reopens both. It is applied at install time via Bun's patchedDependencies.

  • Extension UI. The listener set is private and populated only inside login(), so as shipped there is no way to observe or answer a request. Adds onExtensionUiRequest and respondToExtensionUi.
  • Command output. command_output frames — how every ACP builtin (/model, /cost, /compact, …) reports — are not agent events, so they match no listener and are discarded. A slash command appears to do nothing at all. Adds onCommandOutput.

Both expose machinery that already exists, and both are deliberately the smallest possible diff, because this is also the patch to send upstream. If it lands, delete patches/ and the patchedDependencies entry in package.json.

Security

The daemon executes agent tools — arbitrary file reads, edits, and shell commands. Accordingly:

  • It binds 127.0.0.1 by default and refuses to bind another interface unless OMP_GUI_ALLOW_REMOTE=1 is set.
  • Every HTTP and WebSocket request needs the bearer token, compared in constant time.
  • Cross-origin WebSocket handshakes are rejected. The token alone is not enough: any page can open a WebSocket to localhost, and Origin is what separates the GUI from a tab that guessed the port.
  • New sessions are confined to the configured workspace; a cwd that escapes it is refused.
  • The file tree and viewer are read-only, capped at 512 KB per file, and confined the same way. Every path is resolved to its real location and re-checked against the workspace root, because path.resolve alone would follow a symlink anywhere on the disk. bun run smoke covers relative, absolute, and symlink-based escapes.
  • The diff viewer runs git with argument arrays rather than a command string, confines every client-supplied path before it is passed, and passes it after a -- separator so it can never be read as a revision or a flag.

Exposing this beyond loopback needs a real authenticating proxy. The upstream issue's "SSH-forward it and use it from a tablet" framing treats remote access as incidental; it is not.

Status

Early. The pieces below are built and exercised by bun run smoke, but "built" here means the path works end to end on one setup — not that it has been tested broadly. Reports from other providers, platforms, and workloads are the most useful contribution right now.

Working: session list, create/resume/close, streaming transcript with tool cards, prompt and steer, interrupt, model switching, reconnect with gap replay, approval prompts, image attachments, slash commands, a session settings panel, a workspace file tree with a read-only viewer, and a diff viewer over uncommitted changes.

Not built yet: the subagent panel and the Tauri desktop shell.

Known gaps in what is built:

  • The approval prompt has been driven end to end by the smoke run's probe extension, but not yet against a real model call reaching a real approval gate.
  • An extension that raises a dialog from session_start and then awaits it cannot be answered at all. omp only begins reading stdin once startup has finished, so the dialog blocks the very loop that would collect its answer — a response written by any RPC host sits unread in the pipe. Session creation now fails with that explanation instead of a bare timeout, but the dialog itself is unanswerable until upstream reads stdin earlier. Raising one without awaiting is fine, and is what the smoke probe does.
  • Nothing has been tested with more than a handful of concurrent sessions.
  • Slash command output is transient. It is not an agent event, so it is neither written to the session file nor replayed after a reconnect.
  • The diff viewer shows the working tree against HEAD, so staged and unstaged changes appear together. It cannot stage, unstage, or commit — it is for reading, like the file viewer.
  • The file tree has no watcher. It reflects the workspace as of the last listing, and a directory is re-read only when collapsed and expanded again, or on Refresh.
  • The settings panel omits auto-retry. RpcClient can set it, but get_state never reports it, so a switch for it could only ever display a guess.

Configuration

Variable Default Meaning
OMP_GUI_HOST 127.0.0.1 Bind address
OMP_GUI_PORT 7890 Bind port
OMP_GUI_TOKEN generated Pins the access token across restarts
OMP_GUI_WORKSPACE cwd Root for new sessions
OMP_GUI_CLI resolved Path to omp's dist/cli.js
OMP_GUI_REPLAY_BUFFER 2000 Events retained per session for replay
OMP_GUI_APPROVAL_MODE write always-ask, write, or yolo
OMP_GUI_OMP_ARGS unset Extra flags for every omp spawn
OMP_GUI_ALLOW_REMOTE unset Required to bind a non-loopback address

License

MIT. See LICENSE and NOTICE.

About

Browser GUI for oh-my-pi: a Bun daemon that owns omp RPC sessions plus a React client, built on the documented RPC protocol

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages