# 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

```js
// 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:

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

---

## sw.user

Current authenticated user (read-only getter).

```js
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

```js
// 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.

```js
// 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 |

```js
// 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.

```js
// 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.

```js
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.

```js
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.

```js
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.

```js
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.

```js
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.

```js
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.

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

### sw.workflow.reject(channelId, reason)

Reject the current stage (go back).

```js
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

```js
// 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:

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

### Introspection

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