Omniscio documentation
Browse all documentation
  1. Getting Started18
  2. Sessions & Agents134
  3. Inbox & Notifications67
  4. Projects & Tasks98
  5. Automation & Scheduling78
  6. Knowledge & Memory28
  7. AI Features71
  8. Integrations106
  9. Plugins & Marketplace34
  10. Cloud & Teams53
  11. Settings & Customization70
  12. Account & Billing28
  13. Troubleshooting79
  14. CLI & API Reference27
  15. Legal & Policies5
  16. Uncategorised17

Build your own interface

Omniscio can run **your own interface** instead of its own. You tell it where your HTML file is; it gives that file read access to the same data its built-in screens use — your hubs, projects, sessions, messages, the needs-you list and your tags — a live feed of session changes, and the theme colours the app is drawing with right now. It gets **nothing else**: no files, no network, no way to change anything. Turn it on in **Settings → Appearance → Your own interface**. A bar at the top always stays, so you can get back to the normal interface even if your file is broken.

What it is

Omniscio normally has one interface. This feature lets you replace it with one of your own, built out of the same data — so an agent can arrange your work the way you actually think about it, instead of the way the app happens to be laid out. (Version 1 replaces the whole window; running your interface in a panel beside the normal one is not built yet.)

Four things go together:

  • A read API at /v1/ui — the data a UI needs, each object with one stable shape and a version.
  • A live feed — so a screen you build updates itself instead of re-reading on a timer.
  • A slot — one place a file you wrote can run, sandboxed so it can only ask for the data above.
  • The theme tokens — the app's own colours, sizes and fonts, so your interface can look like it belongs rather than like a page bolted on.

It is deliberately read-only. An interface built this way can show you things; if you want it to do things, it can call the same commands any agent uses (see Related).

Where to find it

To turn it on: Settings → Appearance → Your own interface. Two switches — run your own interface, and whether it takes the whole window — and a button that writes a working example for you to start from.

Your file lives here:

<your Omniscio data folder>/custom-ui/index.html

The card in Settings shows you the exact path on your machine, and the button writes the example there for you. Nothing else is required: no build step, no packaging, no upload. It is one HTML file, and everything — markup, styles and script — goes inside it.

If you do not want to touch HTML: ask an AI agent, in a normal Omniscio session, to write you one. Point it at this page. Everything it needs is here.

How it behaves

Your file is sandboxed, and the sandbox is real. It runs with no access to the app's own screens, no access to your files, and no access to the network — the app SERVES your document together with a rule that forbids outbound connections, so a page you (or an agent) wrote cannot quietly outbound connections before your document loads, so a page you (or an agent) wrote cannot quietly send your data anywhere. The only thing it can reach is the bridge described below.

Getting back out is always possible. If your interface takes the whole window, the app draws a bar across the top that your file cannot remove or cover, and it works even if your file's script has crashed. You can also get back from a terminal:

POST /custom-ui/exit

That turns full-window mode off. It is the recovery direction, so it needs no confirmation.

Two things worth knowing before you build:

  • Read-only. Your interface cannot archive a session, send a message or spend money. Anything like that is a job for a normal command, not for the interface.
  • A page on the web cannot call the read API. The API is local to your computer and deliberately sends no browser cross-origin permission, so only programs on your machine (your terminal, an agent, a small local app) can read it. Your sandboxed interface does not use the API directly — it goes through the bridge.

For agents

You are building an interface for someone. Everything below is the whole contract.

The sandboxed route — for the in-app slot

Your document talks to the app with postMessage. Two calls in, two kinds of message out. The marker key amcCustomUi must be set to true and v must be 1, or your message is ignored.

Say hello as soon as your script runs. The app replies with the theme tokens.

parent.postMessage({ amcCustomUi: true, v: 1, kind: 'hello' }, '*')

Ask for data. Give each call an id of your own; the answer comes back with it.

parent.postMessage({
  amcCustomUi: true, v: 1, kind: 'call', id: '1', method: 'sessions', params: {}
}, '*')

Listen for every message the app sends you:

window.addEventListener('message', (event) => {
  const m = event.data
  if (!m || m.amcCustomUi !== true || m.v !== 1) return   // not for you
  if (m.kind === 'result') { /* m.id, m.ok, m.data or m.error */ }
  if (m.kind === 'tokens') { /* m.tokens: name -> value, m.mode: 'dark' | 'light' */ }
  if (m.kind === 'event')  { /* m.event.type === 'session-status' */ }
})

The methods you may call — all nine return exactly what /v1/ui returns, below.

method params you get back
meta — { apiVersion, appVersion, objects }
hubs — { hubs: [...] }
projects — { projects: [...] }
sessions { projectId?, limit? } { sessions: [...] }
session { id } { session }
messages { id, window? } { messages: [...], capped }
needs-you { limit? } { sessions: [...] }
tags — { tags: [...] }
counts — { sessions: {...}, projects, hubs, tags }

Anything else is refused. There is no method that changes anything.

The theme tokens

On hello, and again whenever the theme changes, the app sends:

{ amcCustomUi: true, v: 1, kind: 'tokens', mode: 'dark', themeId: '...', tokens: { 'color-surface-100': '#...', 'accent': '#...', ... } }

Apply them as CSS custom properties on your own root and use them in your stylesheet. The names published are the surface ladder color-surface-50 through color-surface-950 (plus an -rgb twin for each), accent and accent-rgb, font-family-body, font-family-mono, app-font-size, and the status colours as rgb triples, so you can use them at any opacity: status-agent-rgb, status-needs-you-rgb, status-ready-rgb, status-error-rgb, status-question-rgb — and for agent, needs-you and error, a -strong-rgb twin and a -fg-rgb colour to put ON top. That is the whole list on purpose: it is the part that is promised to stay.

A name the app does not define is simply absent from the map — it is dropped rather than sent empty — so check for it before you use it.

The local API — for anything outside the app

Same data, same shapes, over HTTP on 127.0.0.1:19519, with a bearer token (the same one your other commands use). Every response is { ok, apiVersion, data }.

GET /v1/ui/meta                          what this API is and what it publishes
GET /v1/ui/hubs
GET /v1/ui/projects
GET /v1/ui/sessions?projectId=&limit=
GET /v1/ui/sessions/:id
GET /v1/ui/sessions/:id/messages?limit=
GET /v1/ui/needs-you                     sessions waiting on a person
GET /v1/ui/tags
GET /v1/ui/counts                        what the sidebar and hub rows are built from
GET /v1/ui/stream                        live changes (see below)

Objects, exactly. These are the published shapes. A field not listed here is not published.

// hub
{ "key": "…", "name": "…", "kind": "…", "description": "…", "enabled": true }

// project
{ "id": "…", "name": "…", "color": null, "sessionCount": 3 }

// session  (one shape, used by sessions / session / needs-you)
{ "id": "…", "name": "…", "projectId": "…", "projectName": "…", "projectColor": null,
  "status": "…", "pendingAction": null,
  "bucket": "live",              // live | needs-you | interrupted | paused | archived
  "startedAt": "…", "lastActiveAt": "…", "messageCount": 4 }

// message
{ "id": "…", "role": "user", "content": "…", "timestamp": "…" }

// tag
{ "id": "…", "name": "…", "description": null, "color": null, "icon": null, "showInSidebar": false }

// counts
{ "sessions": { "total": 0, "live": 0, "needsYou": 0, "interrupted": 0, "paused": 0, "archived": 0 },
  "projects": 0, "hubs": 0, "tags": 0 }

bucket is the one to group by — it is the app's own answer to "where does this session belong", so a list you build sorts the same way the normal sidebar does.

The live feed

GET /v1/ui/stream sends Server-Sent Events. Reconnect on close — each connection is a bounded cycle, not a permanent stream, so closing is normal and your client should simply connect again.

  • event: ready — sent the moment you are connected. Wait for it: without it a quiet feed and a broken one look the same.
  • event: session-status — { "sessionId", "status", "previousStatus", "statusChangedAt", "at" }.
  • event: cycle — the connection is ending cleanly; reconnect.

A bearer token is required, and this server deliberately does not accept a token in the URL, so use a client that can send a header (fetch with a streaming body reader — not EventSource).

The worked example

The button in Settings writes a complete, working interface to your file: hubs, sessions grouped by bucket, live updates, and the theme applied. Read it, change it, or ask an agent to rewrite it. It uses only what this page documents — deliberately, so that if it works, so will yours.

Related

  • Settings → Appearance — where the switches and the example button live.
  • The local control server — the commands an agent can run, including the ones that change things, which this interface deliberately cannot.

Last verified 2026-10-12