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/SURFACES.md
2026-03-08 18:54:53 +00:00

11 KiB

Surface Development Guide

Version: 0.25.1
Audience: Anyone with admin access to a Chat Switchboard instance


Overview

A surface is a full-screen application mode in Chat Switchboard. The platform ships with core surfaces (Chat, Notes, Settings, Admin, Workflow) that cannot be uninstalled. Extension surfaces are uploaded as .surface archives via the Admin panel and can be enabled, disabled, and uninstalled at runtime.

This guide covers everything needed to build, package, install, and manage surfaces using only a running instance — no access to the Go codebase required.


Quick Start

Create a minimal surface in under 5 minutes:

1. Create the files

manifest.json:

{
    "id": "hello",
    "version": "1.0.0",
    "title": "Hello World",
    "description": "Minimal surface example",
    "route": "/hello",
    "auth": "authenticated",
    "scripts": ["js/hello.js"],
    "styles": ["css/hello.css"],
    "source": "extension"
}

js/hello.js:

(function() {
    document.addEventListener('DOMContentLoaded', function() {
        if (window.__SURFACE__ !== 'hello') return;

        var root = document.getElementById('surfaceRoot');
        if (!root) return;

        root.innerHTML =
            '<div style="display:flex;flex-direction:column;height:100%;background:var(--bg);color:var(--text);font-family:inherit;">' +
                '<div style="display:flex;align-items:center;gap:8px;padding:0 12px;height:40px;background:var(--bg-secondary);border-bottom:1px solid var(--border);position:relative;z-index:20;">' +
                    '<a href="' + (window.__BASE__ || '') + '/" style="color:var(--text-3);text-decoration:none;font-size:12px;">← Back</a>' +
                    '<span style="font-size:13px;font-weight:600;">Hello World</span>' +
                '</div>' +
                '<div style="flex:1;display:flex;align-items:center;justify-content:center;">' +
                    '<div style="text-align:center;">' +
                        '<div style="font-size:48px;margin-bottom:16px;">👋</div>' +
                        '<h1 style="font-size:24px;font-weight:600;margin:0 0 8px;">Hello from a Surface</h1>' +
                        '<p style="color:var(--text-3);margin:0;" id="helloStatus">Waiting for app init…</p>' +
                    '</div>' +
                '</div>' +
            '</div>';

        document.addEventListener('sb:ready', function() {
            var el = document.getElementById('helloStatus');
            if (el) {
                el.textContent = 'Logged in as ' + (API.user?.username || 'unknown') +
                    ' | ' + (App.models?.length || 0) + ' models loaded';
            }
        }, { once: true });
    });
})();

css/hello.css:

/* Empty for minimal example. Real surfaces use CSS classes with var(--*) tokens. */

2. Package

zip -r hello.surface manifest.json js/ css/

3. Install

Admin → System → Surfaces → + Install Surface. Upload hello.surface.

4. Visit

Navigate to https://your-instance/hello.


How Surface Loading Works

Browser requests /hello
    → Router matches route from surface_registry
    → Auth middleware runs (based on manifest "auth" field)
    → base.html renders with:
        - Common CSS (variables, layout, components)
        - Common JS (api.js, events.js, ui-core.js, all component factories)
        - Your surface CSS and JS from manifest
        - app.js (boot script)
    → DOMContentLoaded fires:
        1. Your surface JS runs — build DOM, create components
        2. app.js init() runs — auth, settings, models, WebSocket
        3. 'sb:ready' CustomEvent fires when init is complete

Critical: Boot Timing

Your JS runs BEFORE app init completes.

Available at DOMContentLoaded Available at sb:ready
window.__SURFACE__, window.__BASE__ App.models, App.settings
All component factories API.user, API.isAdmin
DOM ready Events WebSocket connected

Rule: Anything needing models, user info, or settings goes in sb:ready:

document.addEventListener('sb:ready', function() {
    // Safe to use App.models, API.user, App.settings
}, { once: true });

manifest.json Reference

Field Required Description
id Yes Unique identifier. Lowercase, no spaces.
title Yes Display name in admin panel and nav.
version No Semver for display.
description No Short description for admin.
route No URL pattern. Gin params: /thing/:id. Defaults to /{id}.
alt_routes No Additional URL patterns.
auth No "authenticated" (default), "admin", "session", "public".
scripts No JS files relative to archive root.
styles No CSS files relative to archive root.
components No Component IDs used (documentation only).
source No Always "extension" for uploaded surfaces.

.surface Archive Format

my-surface.surface          (ZIP file)
├── manifest.json           ← Required
├── js/
│   └── my-surface.js       ← Boot script(s)
├── css/
│   └── my-surface.css      ← Styles
└── assets/                  ← Optional: images, fonts

Standard ZIP. .surface extension is conventional; .zip also accepted.


CSS Theme Tokens

Use these instead of hardcoded colors:

/* Backgrounds */
var(--bg)  var(--bg-secondary)  var(--bg-tertiary)  var(--bg-raised)  var(--bg-hover)

/* Text */
var(--text)  var(--text-2)  var(--text-3)

/* Accent */
var(--accent)  var(--accent-dim)  var(--accent-hover)

/* Borders */
var(--border)  var(--border-light)

/* Semantic */
var(--success)  var(--warning)  var(--danger)  var(--purple)
var(--success-dim)  var(--warning-dim)  var(--danger-dim)  var(--purple-dim)

/* Type & Layout */
var(--mono)  var(--msg-font)  var(--radius)  var(--radius-lg)  var(--transition)

Reusable Components

All loaded by base.html on every surface. No imports needed.

UserMenu

// Build DOM into a container
container.innerHTML =
    '<div class="user-menu-wrap" id="userMenuWrap">' +
        '<button id="userMenuBtn" class="user-btn">' +
            '<div id="userAvatar" class="user-avatar"><span id="avatarLetter">?</span></div>' +
            '<span id="userName" class="sb-label">User</span>' +
        '</button>' +
        '<div id="userFlyout" class="user-flyout">' +
            '<button id="menuSettings" class="flyout-item">Settings</button>' +
            '<button id="menuAdmin" class="flyout-item" style="display:none">Admin</button>' +
            '<button id="menuDebug" class="flyout-item" style="display:none">Debug</button>' +
            '<hr class="flyout-divider">' +
            '<button id="menuSignout" class="flyout-item flyout-danger">Sign Out</button>' +
        '</div>' +
    '</div>';

// Wire after auth (sb:ready)
var menu = UserMenu.create({ id: '' });
menu.setUser(API.user);
menu.bind({
    onSettings: function() { location.href = (window.__BASE__ || '') + '/settings'; },
    onAdmin:    function() { location.href = (window.__BASE__ || '') + '/admin'; },
    onSignout:  function() { if (typeof handleLogout === 'function') handleLogout(); },
});
menu.showAdmin(API.isAdmin);

ChatPane

// Build DOM
slot.innerHTML =
    '<div class="chat-pane" id="xChatPane">' +
        '<div class="chat-pane-messages" id="xChatMessages"></div>' +
        '<div class="chat-pane-input-bar">' +
            '<div class="chat-pane-input-wrap">' +
                '<div class="chat-pane-input" id="xChatInput"></div>' +
                '<button class="chat-pane-send" id="xSendBtn">Send</button>' +
            '</div>' +
        '</div>' +
    '</div>';

// Create instance
var pane = ChatPane.create({
    id: 'x',
    messagesEl: document.getElementById('xChatMessages'),
    inputEl:    document.getElementById('xChatInput'),
    sendBtnEl:  document.getElementById('xSendBtn'),
    standalone: true,
});
pane.showWelcome();

PaneContainer

PaneContainer.registerPreset('my-layout', {
    type: 'split', direction: 'horizontal',
    sizes: [250, null, 350],
    children: [
        { type: 'leaf', id: 'nav', size: 250, minSize: 150 },
        { type: 'leaf', id: 'main' },                         // flex pane (no size)
        { type: 'tabbed', id: 'assist', size: 350, minSize: 200,
            tabs: [{ id: 'chat', label: 'Chat' }, { id: 'tools', label: 'Tools' }]
        },
    ],
});

var layout = PaneContainer.mount(document.getElementById('body'), 'my-layout');
var mainEl = layout._panes.get('main').el;
var chatTab = layout._panes.get('assist').getTabPanel('chat');

Rules: One child per split must omit size (flex pane). Drag handles cap at 60%. Sizes persist in localStorage.


Making API Calls

// CRUD
var data = await API._get('/api/v1/channels');
var resp = await API._post('/api/v1/channels', { title: 'New', type: 'direct' });
await API._put('/api/v1/channels/' + id, { title: 'Updated' });
await API._del('/api/v1/channels/' + id);

// Streaming (SSE)
var resp = await API.streamCompletion(channelId, content, model, abortSignal);
var reader = resp.body.getReader();

Auth tokens and base path are handled automatically.


Admin: Surface Management

Location: Admin → System → Surfaces

Action Description
+ Install Surface Upload .surface or .zip archive
Toggle Enable/disable. Disabled surfaces redirect to Chat.
Uninstall Remove extension surface (files + DB entry).

Protected: Chat and Admin toggles are locked — cannot be disabled.

API

# List
curl -H "$AUTH" "$BASE/api/v1/admin/surfaces"

# Install
curl -H "$AUTH" -F "file=@hello.surface" "$BASE/api/v1/admin/surfaces/install"

# Disable / Enable
curl -X PUT -H "$AUTH" "$BASE/api/v1/admin/surfaces/editor/disable"
curl -X PUT -H "$AUTH" "$BASE/api/v1/admin/surfaces/editor/enable"

# Uninstall
curl -X DELETE -H "$AUTH" "$BASE/api/v1/admin/surfaces/hello"

# Public: list enabled (for nav)
curl -H "$AUTH" "$BASE/api/v1/surfaces"

Checklist

  • manifest.json has id and title
  • Boot script checks window.__SURFACE__ guard
  • DOM built in JS (no Go template dependency)
  • Deferred init via sb:ready
  • Topbar: position: relative; z-index: 20
  • All colors use var(--*) tokens
  • Back link: window.__BASE__ + '/'
  • Graceful with 0 models
  • Archive: only manifest.json, js/, css/, assets/

Troubleshooting

Symptom Cause Fix
Surface doesn't appear after install Routes registered at startup Restart server or toggle disable/enable
"Cannot overwrite core surface" ID conflicts with chat/notes/etc. Choose different ID
API is undefined Code runs before app init Move to sb:ready listener
No styles applied CSS path mismatch Check manifest styles matches archive structure
Flyout hidden behind content Missing stacking context Add z-index: 20 to topbar
Drag handle snaps v0.25.0 known issue Sub-pixel rounding; partially mitigated