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/SDK.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

7.0 KiB

Switchboard SDK Reference

switchboard-sdk.js — v0.30.2

The Switchboard SDK provides a unified API for surfaces and extensions. It wraps platform internals (API, Events, Theme, ChatPane, etc.) into a single sw namespace so authors never need to import platform files directly.

Initialization

// Automatic — the SDK initializes on page load.
// Access via the global `sw` alias:
sw.api.get('/api/v1/channels').then(console.log);

The SDK emits a sw:ready CustomEvent on document when initialized. Extensions loaded after boot can listen for it:

document.addEventListener('sw:ready', (e) => {
    const sw = e.detail.sw;
});

sw.user

Current authenticated user (read-only getter).

const u = sw.user;
// { id, username, display_name, email, role, avatar }

Returns null if not authenticated.

sw.isAdmin

true if the current user has the admin role.


sw.api

REST client with automatic auth header injection and 401 retry.

Method Signature Returns
get (path, opts?) Promise<object>
post (path, body, opts?) Promise<object>
put (path, body, opts?) Promise<object>
del (path, opts?) Promise<object>
stream (path, body, signal?) Promise<Response>

opts accepts { signal: AbortSignal } for cancellation.

Examples

// List channels
const channels = await sw.api.get('/api/v1/channels');

// Create a channel
const ch = await sw.api.post('/api/v1/channels', {
    title: 'Support',
    type: 'direct'
});

// Streaming completion
const resp = await sw.api.stream('/api/v1/completions', {
    channel_id: ch.id,
    message: 'Hello'
});
const reader = resp.body.getReader();

sw.on / sw.once / sw.off / sw.emit

EventBus integration. Supports wildcard patterns.

// Subscribe
const unsub = sw.on('chat.message.*', (payload) => {
    console.log('New message:', payload);
});

// One-shot
sw.once('channel.created', (ch) => { ... });

// Unsubscribe
sw.off('chat.message.*', handler);

// Emit
sw.emit('custom.event', { data: 123 });

Common Events

Event Payload Description
chat.message.sent { message } User sent a message
chat.message.received { message } AI response received
channel.created { channel } New channel created
channel.switched { channelId } Active channel changed
theme.changed { theme } Theme mode changed

sw.theme

Theme observation and control.

Property/Method Type Description
current string (getter) Resolved theme: 'dark' or 'light'
mode string (getter) User preference: 'dark', 'light', or 'system'
set(mode) void Set theme mode
on('change', fn) () => void Subscribe to theme changes; returns unsubscribe fn
// React to theme changes
const unsub = sw.theme.on('change', (resolved) => {
    document.body.classList.toggle('dark', resolved === 'dark');
});

sw.toast / sw.confirm / sw.modal

UI primitives for notifications and dialogs.

// Toast notification
sw.toast('Saved successfully', 'success');   // types: success, error, info, warn
sw.toast('Something went wrong', 'error');

// Confirmation dialog (returns Promise<boolean>)
const ok = await sw.confirm('Delete this item?');

// Modal
sw.modal.open(htmlContentOrElementId);
sw.modal.close(id);

sw.chat(container, opts)

Mount a ChatPane instance into a DOM element.

const pane = sw.chat(document.getElementById('my-chat'), {
    channelId: 'abc-123',
    standalone: true,   // default: true
});

// pane.renderMessages(), pane.destroy(), etc.

Options:

Key Type Default Description
channelId string null Channel to load
standalone boolean true Standalone mode (own input area)

sw.notes(container, opts)

Mount a NotePanel instance into a DOM element.

const panel = sw.notes(document.getElementById('my-notes'), {
    projectId: 'proj-123',
});

// panel.loadNotesList(), panel.openNoteEditor(id), panel.destroy()

sw.panels()

Access the PanelRegistry for sidebar panel management.

const panels = sw.panels();
panels.open('notes');
panels.toggle('editor');
panels.isOpen('notes');    // boolean
panels.active();           // current panel name or null
panels.cycle();            // cycle to next panel
panels.register('custom', { ... });

sw.workflow

Workflow stage surface management (v0.30.2).

sw.workflow(container, opts)

Mount a workflow stage surface.

sw.workflow(document.getElementById('stage-mount'), {
    channelId: 'ch-abc',
    stageMode: 'form_only',        // chat_only | form_only | form_chat | review
    surfacePkgId: 'my-surface',    // optional: custom package surface
    formTemplate: { fields: [...] },
});

sw.workflow.registerSurface(name, factory)

Register a custom stage surface from a package.

sw.workflow.registerSurface('intake-form', (container, ctx) => {
    // ctx: { channelId, basePath, stageMode, formTemplate, ... }
    container.innerHTML = '<h2>Custom Intake</h2>';
    return {
        mount() { /* called on attach */ },
        unmount() { /* called on detach */ },
    };
});

sw.workflow.getContext(channelId)

Fetch workflow instance status.

const status = await sw.workflow.getContext('ch-abc');
// { workflow_id, current_stage, stage_data, ... }

sw.workflow.advance(channelId, data?)

Advance to the next stage, optionally merging data.

await sw.workflow.advance('ch-abc', { approved: true });

sw.workflow.reject(channelId, reason)

Reject the current stage (go back).

await sw.workflow.reject('ch-abc', 'Missing required documents');

sw.pipe

Filter pipeline for intercepting chat messages at three stages: pre-send, post-receive (stream), and post-render.

Registration

// Pre-send filter (runs before message is sent to API)
sw.pipe.pre(50, (ctx) => {
    // ctx: { message, channel, metadata }
    ctx.message += '\n\n[via my extension]';
    return ctx;       // return ctx to continue, null to halt
});

// Stream filter (runs on each SSE chunk)
sw.pipe.stream(50, (ctx) => {
    // ctx: { chunk, channel, accumulated }
    return ctx;
});

// Render filter (runs after markdown rendering)
sw.pipe.render(50, (ctx) => {
    // ctx: { html, message, channel }
    ctx.html = ctx.html.replace(/TODO/g, '<mark>TODO</mark>');
    return ctx;
});

Priority: lower numbers run first. Convention: 0-49 system, 50-99 extensions, 100+ user customizations.

Scope: restrict a filter to specific channel types:

sw.pipe.render(50, myFilter, {
    scope: { channelType: ['direct', 'group'] },
    source: 'my-extension',
});

Introspection

sw.pipe.list();
// { pre: [...], stream: [...], render: [...] }
// Each entry: { priority, source, scope, calls, avgMs, errors }