Chataway Developers
Concepts

Panels & the bridge

Your app's own UI in the Chataway dock — a sandboxed page with a CSP built from your declared domains, host-drawn view tabs, badges and live theme tokens.

A panel is your web UI in a tab of the Chataway dock, next to the chat. It's a static bundle (ui/ in your app) that Chataway loads in a sandboxed iframe. It can be anything a web page can be — a library browser, a dashboard, an editor — built with whatever framework you like.

#Declare a panel

plugin.json
json
"contributes": {
  "panels": [
    {
      "id": "main",
      "title": "Acme",
      "icon": "branding",
      "component": "webview",
      "config": { "entry": "ui/index.html" },
      "views": [
        { "id": "library", "label": "Library" },
        { "id": "uploads", "label": "Uploads" }
      ]
    }
  ]
}
Field
idYour panel id.
titleTab title (≤ 40 characters).
icon"branding" uses your mark.
componentAlways "webview" for store apps.
config.entryThe HTML file inside your bundle. Default ui/index.html.
viewsOptional. Chataway draws these as the tab's header, next to a Settings view it renders itself.

The bundle's ui/ folder holds the built files. The scaffold keeps sources in web/ and builds them into ui/ with Vite; any tool that outputs static files works.

#The sandbox

Your page runs with sandbox="allow-scripts" and no allow-same-origin. That means:

  • An opaque origin. No cookies, no localStorage shared with anything, no access to Chataway's DOM, no top-level navigation.
  • A strict Content Security Policy. Scripts, styles, fonts and images load from your bundle. Network requests (connect-src), images (img-src) and media (media-src) may also go to your remote.domains and your mcpUrl host — and nowhere else.
  • No popups or forms to other sites. Use accounts.connect for sign-in, and the bridge for files.

Because localStorage isn't reliable in an opaque origin, keep state on your server (keyed by the account token), or in memory.

#The bridge

The panel talks to Chataway with postMessage. @chataway/apps/bridge wraps it in a typed client:

ui/main.ts
ts
import { connect } from '@chataway/apps/bridge';
import '@chataway/apps/theme.css';

const app = await connect();

app.on('view', view => showView(view));        // the user switched the header tab
app.on('card', card => refreshQueue());        // one of your cards changed

const pending = await app.listCards({ status: 'pending' });
await app.setBadge('uploads', pending.length); // "Uploads 2"

Outside Chataway (opened directly in a browser with vite), connect() falls back to a mock host with a small toolbar, so you can build the UI without the desktop app. The full API is in the Bridge reference.

#Views and badges

When you declare views, Chataway draws them as the tab header and adds its own Settings view (your settings, rendered by Chataway). Selecting a view sends your page a view event; getContext() tells you the initial one. Your page shows the matching screen — one page, several views, no routing on your side needed beyond a switch.

setBadge(view, count) puts a count on a view's tab (Uploads 2). Set it to 0 to clear it.

#Theme

Chataway has several palettes and a light/dark mode per device. Your panel should look at home in all of them:

  • Import @chataway/apps/theme.css — the host's design tokens as CSS variables, plus native-looking base styles. Tokens are r g b triplets, so you use them as rgb(var(--color-bg)) or with alpha, rgb(var(--color-primary) / 0.5). When your page connects, the bridge sets the active palette (data-theme on <html>) and mode (.light) and the host's exact colours, and keeps them updated live when the user changes the theme.
  • Or read app.context.theme (hex values) and listen for theme events to apply them yourself.
css
body { background: rgb(var(--color-bg)); color: rgb(var(--color-text)); }
.card { background: rgb(var(--color-bg-surface)); border: 1px solid rgb(var(--color-border)); border-radius: 10px; }
.hint { color: rgb(var(--color-text-muted)); }
button.primary { background: rgb(var(--color-primary)); color: rgb(var(--color-primary-fg)); }
a { color: rgb(var(--color-accent)); }
TokenUse
--color-bg · --color-bg-surface · --color-bg-hoverPage, raised surfaces, hover
--color-text · --color-text-muted · --color-text-dimText, secondary text, hints
--color-border · --color-border-lightHairlines
--color-primary · --color-primary-fgFilled buttons and the text on them
--color-accentLinks, focus, in-progress state
--color-success · --color-warning · --color-dangerStatus

Your accent colour is for your header stripe and card edges — inside the panel, prefer the host's tokens so text stays readable in every palette.

#Webview cards

A card can also embed a page from your bundle (kind webview) — for results that need custom rendering, like an interactive chart. The page gets the same bridge, with context.cardId and context.params set. Keep them small; most results read better as a notice or gallery card.

#Phones

The Chataway phone app shows your panel too, full-screen. Design for 360 px wide and up, use the theme tokens, and make touch targets at least 40 px.

#Good practice

  • Load fast. Ship a small bundle; show something within 300 ms, then fetch.
  • Use the host's pickers and cards instead of your own file inputs and confirmation dialogs — users trust them, and they work on the phone.
  • Handle cancelled. Pickers and connect flows reject when the user backs out; that's not an error to show.
  • No tracking scripts. Third-party analytics can't load (they're not your domains) — and reviewers reject apps that try to get around that.