This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/EXTENSION-SURFACES.md
Jeffrey Smith 5883cb50e2 Changeset 0.30.2 cs2 (#202)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-18 18:05:58 +00:00

12 KiB

Extension Surfaces — Authoring Guide

Version: v0.30.2 Audience: Developers building custom surfaces for Chat Switchboard Prerequisite: Admin access to install surfaces See also: PACKAGES.md for the .pkg archive format, SDK.md for the sw.* API


What Is an Extension Surface?

An extension surface is a custom full-page UI that runs inside the Chat Switchboard shell. It gets its own URL (/s/your-surface), a link in the sidebar, and full access to the platform's authenticated API, theme system, and UI components.

Examples of what you can build:

  • A monitoring dashboard that polls external APIs
  • A custom form builder for workflow intake
  • A kanban board backed by channel data
  • A cost tracking view using usage data
  • A project timeline visualization

Extension surfaces are installed as .surface archives via the admin panel. No code changes to the platform are required.


Quick Start

1. Create the Archive

A .surface file is a zip archive with this structure:

my-dashboard/
├── manifest.json       ← required: surface metadata
├── js/
│   └── main.js         ← required: entry point script
└── css/
    └── main.css        ← optional: styles

The top-level directory name must match the id in manifest.json.

2. Write the Manifest

{
  "id": "my-dashboard",
  "title": "My Dashboard",
  "route": "/s/my-dashboard",
  "auth": "authenticated",
  "layout": "single",
  "version": "1.0.0",
  "description": "A custom dashboard surface."
}
Field Required Description
id yes Unique identifier. Lowercase, alphanumeric + hyphens. Must match the archive's top-level directory name.
title yes Human-readable name shown in the sidebar nav.
route no URL path. Defaults to /s/{id} if omitted. Must start with /s/.
auth no Auth requirement. Only "authenticated" is supported currently. Default: "authenticated".
layout no Layout preset. Only "single" is supported currently. Default: "single".
version no Semver string for your own tracking. Not enforced.
description no Shown in the admin surfaces panel.

3. Write the Entry Point

js/main.js runs after all platform scripts have loaded. Your code mounts into the #extension-mount container:

(function() {
    'use strict';

    var mount = document.getElementById('extension-mount');
    if (!mount) return;

    var manifest = window.__MANIFEST__ || {};
    var user = window.__USER__ || {};

    mount.innerHTML = '<h1>Hello, ' + esc(user.display_name || user.username) + '!</h1>';

    function esc(s) {
        var el = document.createElement('span');
        el.textContent = s;
        return el.innerHTML;
    }
})();

4. Package and Install

zip -r my-dashboard.surface my-dashboard/

Then in the admin panel: Admin → Surfaces → Upload and select the .surface file. The surface is immediately available — no restart required. A link appears in the sidebar.


Platform Globals

Your main.js runs in the full platform context. These globals are available when your script executes:

Window Injections

Global Type Description
window.__MANIFEST__ Object Your surface's manifest with route, layout, etc. Keys are lowercase (JSON serialization from Go). Access as manifest.id, manifest.route, manifest.title.
window.__USER__ Object Authenticated user: { username, display_name, role, id, email }.
window.__BASE__ String URL base path (e.g. "/dev" or ""). Prepend to all internal links.
window.__VERSION__ String Platform version string.
window.__SURFACE__ String Your surface ID.

API Module

API provides authenticated fetch wrappers. Auth headers and token refresh are handled automatically.

// GET request — returns parsed JSON
var data = await API._get('/api/v1/channels');

// POST request — body is JSON-serialized automatically
var result = await API._post('/api/v1/channels', {
    name: 'my-channel',
    type: 'channel'
});

// PUT request
await API._put('/api/v1/channels/' + id, { name: 'renamed' });

All methods return a Promise that resolves to parsed JSON. On 401, tokens are refreshed automatically and the request is retried.

Important: The methods are _get, _post, _put — prefixed with underscore. There is no API.get().

UI Module

UI provides platform UI primitives:

// Toast notification — types: 'success', 'error', 'info', 'warning'
UI.toast('Operation completed', 'success');
UI.toast('Something went wrong', 'error');

// Navigate to settings
UI.openSettings('general');

Theme Module

Theme manages dark/light mode:

// Current resolved theme: 'dark' or 'light'
var mode = Theme.resolved();

// Check via DOM attribute
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';

Your CSS should use CSS custom properties (see below) rather than querying Theme directly. Properties update automatically on theme change.

Events Module

Events is the platform event bus (WebSocket-backed):

// Subscribe to events
Events.on('chat.message.created', function(data) {
    console.log('New message:', data);
});

// Emit local-only events
Events.emit('my-surface.updated', { key: 'value' });

ChatPane Component

If your surface needs an embedded chat, you can mount a ChatPane:

var container = document.createElement('div');
container.style.height = '400px';
mount.appendChild(container);

ChatPane.create(container, { role: 'assist' });

CSS Custom Properties

Your CSS should use the platform's CSS custom properties for consistent theming. These update automatically when the user switches themes.

Colors

Property Dark Default Light Default Usage
--bg #0e0e10 #f7f7fa Page background
--bg-surface #18181b #ffffff Card/panel background
--bg-raised #222227 #eff0f3 Elevated element background
--bg-hover #2a2a30 #e5e6eb Hover state background
--border #2e2e35 #d5d6dc Default borders
--border-light #3a3a42 #c2c3cb Subtle borders
--text #e8e8ed #1a1a2e Primary text
--text-2 #9898a8 Secondary text
--text-3 #6b6b7b Tertiary/muted text
--accent #6c9fff Accent color (links, highlights)
--accent-hover #84b0ff Accent hover state
--accent-dim rgba(108,159,255,0.12) Accent tinted background
--danger #ef4444 Error/destructive actions
--success #22c55e Success indicators
--warning #eab308 Warning indicators

Layout

Property Default Usage
--radius 8px Default border radius
--radius-lg 12px Large border radius
--font 'DM Sans', ... Primary font stack
--mono 'JetBrains Mono', ... Monospace font stack
--transition 180ms ease Default transition timing

Example

.my-card {
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 16px;
    color: var(--text);
}

.my-card-subtitle {
    color: var(--text-2);
    font-size: 13px;
}

.my-card:hover {
    background: var(--bg-hover);
}

Do not use --bg-secondary, --text-secondary, or --text-tertiary — these do not exist. Use --bg-surface, --text-2, and --text-3 respectively.


DOM Structure

When your surface loads, the page DOM looks like this:

<body data-surface="my-dashboard">
  <div class="surface">
    <div class="surface-inner">
      <div id="extension-surface" class="extension-surface">
        <div class="user-menu-wrap"> ... </div>   ← platform nav
        <div id="extension-mount" class="extension-mount">
          ← YOUR CONTENT GOES HERE
        </div>
      </div>
    </div>
  </div>
  <script>window.__MANIFEST__ = {...};</script>
  <script>window.__USER__ = {...};</script>
  <script src="/js/api.js"></script>
  <script src="/js/ui-core.js"></script>
  ... platform scripts ...
  <script src="/surfaces/my-dashboard/js/main.js"></script>  ← YOUR SCRIPT
</body>

The #extension-mount div uses flex: 1 and overflow: auto, so it fills all available vertical space. You can set its content to anything.


Admin API Reference

Surfaces are managed via the admin API. All endpoints require admin authentication.

Install a Surface

POST /api/v1/admin/surfaces/install
Content-Type: multipart/form-data

file: <.surface or .zip file>

The archive must contain a manifest.json with id and title. Static assets (js/, css/, assets/) are extracted to the server's storage directory. The surface is registered in the database and enabled immediately.

Response:

{
  "id": "my-dashboard",
  "title": "My Dashboard",
  "source": "extension",
  "enabled": true
}

List All Surfaces

GET /api/v1/admin/surfaces

Returns all surfaces (core + extension) with their enabled state.

Enable / Disable

PUT /api/v1/admin/surfaces/:id/enable
PUT /api/v1/admin/surfaces/:id/disable

Disabled surfaces redirect to the chat surface. The sidebar link is hidden. Core surfaces (chat, admin) cannot be disabled.

Uninstall

DELETE /api/v1/admin/surfaces/:id

Removes the surface registration and cleans up extracted static assets. Core surfaces cannot be deleted.

List Enabled (Non-Admin)

GET /api/v1/surfaces

Returns enabled surfaces with minimal info (id, title, route). Used by the sidebar to render nav links. Available to all authenticated users.


Platform CSS Classes

Your extension can use these CSS classes from the platform's primitives.css without importing anything:

Buttons

<button class="btn-primary">Primary Action</button>
<button class="btn-secondary">Secondary</button>
<button class="btn-danger">Destructive</button>
<button class="btn-ghost">Ghost</button>
<button class="btn-small">Small</button>

Badges

<span class="badge">Default</span>
<span class="badge badge-success">Success</span>
<span class="badge badge-danger">Error</span>
<span class="badge badge-warning">Warning</span>

Toast (via JS)

UI.toast('Message here', 'success');   // green
UI.toast('Message here', 'error');     // red
UI.toast('Message here', 'info');      // blue
UI.toast('Message here', 'warning');   // yellow

Tips

Always wrap in an IIFE. Your script shares the global scope with platform scripts. Use (function() { ... })(); to avoid collisions.

Use esc() for user-generated content. The platform does not provide a global escaping function. Define your own:

function esc(s) {
    var el = document.createElement('span');
    el.textContent = s;
    return el.innerHTML;
}

Prepend __BASE__ to internal links. The platform may be deployed under a path prefix (e.g. /dev):

var base = window.__BASE__ || '';
link.href = base + '/settings/general';

Check for globals before using them. If your surface might be previewed outside the platform:

if (typeof API !== 'undefined' && API._get) {
    // safe to make API calls
}
if (typeof UI !== 'undefined' && UI.toast) {
    UI.toast('Works!', 'success');
}

Size limit: Archive upload is capped at 50 MB. Only js/, css/, and assets/ directories are extracted from the archive.

Cache busting: Static assets are served with ?v={platform_version} query parameter. When the platform is upgraded, caches are invalidated automatically.


Full Example

See the included hello-dashboard.surface archive for a complete working example that demonstrates:

  • Manifest structure
  • Mounting into #extension-mount
  • Reading __MANIFEST__ and __USER__
  • Using UI.toast() for notifications
  • Using API._get() for authenticated requests
  • Using CSS custom properties for theming
  • Proper esc() function for XSS safety