User interfaces
Build a front-end over IPC, xui and the settings schema.
User interfaces
A UI is a plugin of type ui that renders Sayri on screen. The core knows nothing about windows: every UI attaches to the same event stream (CoreUI sink over IPC) and sends the same commands back. That is why the orb, the cajita, a Clippy widget and the CLI all behave identically.
Shipped UIs
Orb + cajita (sayri-ui-orb, entrypoint: gateway.py) — the default. A GTK4 app that pins a transparent, always-on-top overlay to the top of the screen with layer-shell. Two elements side by side:
- the orb: a circular indicator that reacts to audio level and animates through
idle -> listening -> thinking -> speaking; - the cajita: a rounded card with live transcriptions, replies, tool output, text input and the settings tab.
Web content inside WebKit, with a sayri:// bridge to the daemon. webkit.py holds the bridge; orb.py the orb rendering; cajita.py the panel.
Classic companions (sayri-ui-clippy, entrypoint: main.py) — Clippy, Bonzi, Merlin, Rover, Peedy, Genie, Links, Rocky and F1, with speech balloons, sound effects and voice/chat. GTK3 with WebKit; it re-executes itself without the gtk4-layer-shell preload because the GTK4 preload breaks GDK3 symbol resolution. It speaks the full IPC command set through an allowlist (CALL_ALLOWLIST in main.py): conversation, config, permissions, settings, agents, skills, plugins, UIs, gateways, routines, vault.
The xui micro-framework
sayri/xui.py renders the same screen description — a plain JSON component tree — in an interactive terminal, inside a WebKit/GTK window, or in a browser page. Only the event transport changes.
A screen:
{
"id": "welcome",
"title": "Welcome to Sayri",
"subtitle": "One-minute setup",
"step": "1/5",
"body": [ nodes ],
"footer":[ buttons ],
"busy": false,
"done": False,
}
Node types (t): text, sub, note (info|ok|warn|error), spacer, button (primary|secondary|danger), entry, select, check, progress.
A host owns the flow and implements render() -> screen | None and dispatch(event) -> screen | None (None ends the flow). Events:
{"type": "submit", "value": {field: value}} form
{"type": "action", "widget": "<button id>"} button
{"type": "change", "widget": id, "value": v} live widget
{"type": "poll"} refresh while busy
The welcome wizard and every plugin wizard are written once and rendered everywhere. If you build a UI, use xui for forms and flows instead of inventing your own widgets.
The settings schema
sayri/settings_schema.py describes every setting as data: sections, labels, hints, and which widget each one deserves (text | password | number | toggle | choice | textarea), with save semantics (now applies immediately, section waits for the section's Save). Anything present in config.DEFAULTS but missing from the schema is appended to a catch-all section, so adding a config key can never make it unconfigurable. Plugin settings come from each manifest's ui.settings and are converted, not restated.
A UI renders settings with settings_schema over IPC (settings_schema, settings_save commands) instead of hardcoding them.
Writing a UI plugin
Manifest:
{
"id": "sayri-ui-mine",
"name": "My UI",
"type": "ui",
"version": "1.0.0",
"description": "One line.",
"author": "You",
"entrypoint": "main.py",
"ui": {
"kind": "character",
"xui_renderer": "speech-bubble",
"sync_instructions": "Shown once to the user on first sync.",
"settings": [ ...declarative settings... ]
}
}
kind is free-form (orb, character, ...); xui_renderer names the renderer that will draw your xui screens; sync_instructions is displayed by other UIs when they offer to switch.
The entrypoint:
- Connects to the daemon IPC (
sayri.ipc.SayriClientonsayri-daemon.sock). - Renders state from the broadcast events (
state,partial,assistant_delta,tool_start, ...). - Sends commands for user actions (
talk,interrupt,config_set, ...). - Restricts what the UI can invoke to an explicit allowlist if it renders remote content.
- Writes a PID file and exits cleanly on SIGTERM.
Only one UI renders at a time; UIs are switched from the panel or ui_start / ui_stop IPC commands, and the active one is a per-user setting.
The orb animation
The orb is a shader-driven circle whose palette matches the rest of Pulsar OS: a deep space-blue base with magenta/violet energy, an outer chroma ring that rotates continuously, an inner react ring driven by the microphone level, and a bloom/glow layer. States:
idle: slow rotation, low intensity, gentle breathing scale.listening: ring amplitude tracks audio level in real time.thinking: faster rotation and a pulsing inner glow.speaking: amplitude tracks the TTS output.
Any UI can reproduce the look: layered radial gradients, conic-gradient chroma ring rotating via @keyframes, opacity and scale tied to state, durations between 0.8s (thinking pulse) and 6s (idle rotation). The same palette is used by this website.