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
"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 | |
|---|---|
id | Your panel id. |
title | Tab title (≤ 40 characters). |
icon | "branding" uses your mark. |
component | Always "webview" for store apps. |
config.entry | The HTML file inside your bundle. Default ui/index.html. |
views | Optional. 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
localStorageshared 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 yourremote.domainsand yourmcpUrlhost — and nowhere else. - No popups or forms to other sites. Use
accounts.connectfor 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:
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 arer g btriplets, so you use them asrgb(var(--color-bg))or with alpha,rgb(var(--color-primary) / 0.5). When your page connects, the bridge sets the active palette (data-themeon<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 forthemeevents to apply them yourself.
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)); }| Token | Use |
|---|---|
--color-bg · --color-bg-surface · --color-bg-hover | Page, raised surfaces, hover |
--color-text · --color-text-muted · --color-text-dim | Text, secondary text, hints |
--color-border · --color-border-light | Hairlines |
--color-primary · --color-primary-fg | Filled buttons and the text on them |
--color-accent | Links, focus, in-progress state |
--color-success · --color-warning · --color-danger | Status |
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.