Chatons Extensions

Extensions add features to Chatons: sidebar views, LLM tools, storage, event handling, background servers, and messaging bridges.

Start here: The Extensions Tutorial walks you through building a complete extension from scratch.

Reference: The Extensions API documents every available method and its parameters.

Related:


1. What an Extension Is

A Chatons extension is a folder or npm package containing a manifest (manifest.json or chaton.extension.json), packaged UI assets, and optional runtime/server code. Extensions run inside isolated webview iframes with access to a set of host APIs.

Manifest file naming: Chatons looks for either manifest.json (preferred) or chaton.extension.json (legacy). Both formats are supported. If both exist, manifest.json takes priority.

An extension can contribute:

  • Sidebar menu items, topbar items, and main views (full-page UIs)
  • Quick actions (buttons shown on the empty conversation screen)
  • LLM tools (functions the AI model can call)
  • Event listeners (react to conversations, messages, projects)
  • Background servers (local processes started automatically)
  • Persistent storage (key-value and file-based)
  • Message queues (reliable background job processing)
  • Channel bridges (external messaging platform integrations)

Extensions vs Pi skills: Extensions have access to the Chatons host (storage, events, UI). Pi skills are prompt-based instructions with no host integration. Use an extension when you need UI, storage, or event-driven behavior.


2. Where Extensions Live

User extensions

~/.chaton/extensions/<extension-id>/

Example: ~/.chaton/extensions/@yourname/chatons-my-ext/

Built-in extensions

Chatons ships several built-in extensions:

  • @chaton/automation -- Automation rules engine
  • @chaton/memory -- Persistent memory store
  • @chaton/ide-launcher -- Project topbar launcher for supported local IDEs
  • @chaton/tps-monitor -- Topbar widget for per-conversation token throughput

These are bundled with the app and do not appear in the user extension folder. Their UIs use the same packaged-asset model available to third-party extensions.

Registry

Installed extension state is tracked in:

~/.chaton/extensions/registry.json

Extension data storage

File-based storage for each extension lives under:

~/.chaton/extensions/data/<extensionId>/

Extension logs

Runtime logs are written to:

~/.chaton/extensions/logs/

3. Discovery and Loading

At startup, Chatons loads extension manifests from:

  1. Built-in extensions
  2. Registered user extensions
  3. Valid on-disk extension folders discovered during registry scan

Local development: Drop a valid extension folder into ~/.chaton/extensions/ and restart Chatons. The app will discover it automatically.

Important limitation: After adding or changing extension files, restart Chatons. Hot reload is not fully reliable.


4. Manifest

Every extension needs a chaton.extension.json file. This is the manifest -- it tells Chatons what your extension does and what permissions it needs.

Minimal manifest

{
  "id": "@yourname/chatons-my-ext",
  "name": "My Extension",
  "version": "1.0.0",
  "capabilities": ["ui.mainView"],
  "ui": {
    "mainViews": [
      {
        "viewId": "myext.main",
        "title": "My Extension",
        "webviewUrl": "chaton-extension://@yourname/chatons-my-ext/dist/index.html",
        "initialRoute": "/"
      }
    ]
  }
}

Required fields

FieldTypeDescription
idstringUnique extension ID. Format: @username/chatons-name
namestringDisplay name shown in the UI
versionstringSemantic version (e.g. "1.0.0")
capabilitiesstring[]Permissions your extension needs

All manifest fields

{
  id: string                    // Required. Unique extension ID.
  name: string                  // Required. Display name.
  version: string               // Required. Semantic version.
  kind?: "channel"              // Set to "channel" for messaging bridges.
  capabilities: Capability[]    // Required. Permission list.
  ui?: {
    menuItems?: [{
      id: string                // Unique menu item ID
      label: string             // Display text
      icon?: string             // Lucide icon name (e.g. "BookOpen")
      location?: "sidebar"      // Where to show the item
      order?: number            // Sort order (lower = higher in list)
      openMainView?: string     // viewId to open when clicked
    }]
    topbarItems?: [{
      id: string                // Unique topbar item ID
      label: string             // Tooltip / accessible label
      icon?: string             // Optional icon name or custom identifier
      order?: number            // Sort order among topbar items
      when?: string             // Optional host-side visibility expression
      context?: "always" | "project" | "global" // Conversation context filter
      openMainView?: string     // Open an extension main view
      deeplink?: {              // Dispatch an extension deeplink action
        viewId: string
        target: string
        params?: Record<string, unknown>
        createConversation?: boolean
        prefillPrompt?: string
      }
      widget?: {                // Render a compact live widget from an extension view
        viewId: string
        width?: number
        minWidth?: number
      }
    }]
    mainViews?: [{
      viewId: string            // Unique view ID
      title: string             // Display title
      webviewUrl: string        // URL to load (chaton-extension://...), typically a built HTML entry such as dist/index.html
      initialRoute?: string     // Initial route path
    }]
    quickActions?: [{
      id: string                // Unique action ID
      title: string             // Button label
      description?: string      // Optional helper text
      scope?: "always" | "global-thread" | "project-thread" | "global-or-no-thread"
      prompt?: string           // If set, creates a conversation with this prompt
      extensionViewId?: string  // If set, opens this extension view
      deeplink?: {
        viewId: string          // Extension view to open
        target: string          // Deeplink target identifier
        params?: object         // Extra parameters
        createConversation?: boolean  // Create a new conversation first
        prefillPrompt?: string  // Pre-fill the composer with this text
      }
    }]
  }
  llm?: {
    tools?: [{
      name: string              // Tool name (alphanumeric, underscores, hyphens)
      label?: string            // Display label
      description: string       // Description shown to the model
      promptSnippet?: string    // One-line hint in the system prompt
      promptGuidelines?: string[] // Extra guidance lines
      parameters?: object       // JSON Schema for tool parameters
    }]
  }
  apis?: {
    exposes?: [{
      name: string              // API name (must match LLM tool name if paired)
      version: string           // Semver version
    }]
    consumes?: [{
      name: string              // API you depend on
      version: string           // Semver range
    }]
  }
  server?: {
    start?: {
      command: string           // Executable to run (e.g. "node")
      args?: string[]           // Arguments (e.g. ["server.js"])
      cwd?: string              // Working directory (relative to extension root)
      env?: object              // Environment variables
      readyUrl?: string         // URL to poll for readiness
      healthUrl?: string        // Health check URL
      expectExit?: boolean      // true for one-shot scripts
      startTimeoutMs?: number   // Max startup time
      readyTimeoutMs?: number   // Max readiness wait
    }
  }
  hooks?: {
    onInstall?: string
    onEnable?: string
    onDisable?: string
    onUninstall?: string
    onStart?: string
    onStop?: string
    onHealthCheck?: string
  }
  compat?: {
    minHostVersion?: string
    maxHostVersion?: string
  }
}

Packaged UI assets

The preferred extension UI format is now a prebuilt web app.

That means:

  • Build your UI ahead of time, usually into dist/
  • Point webviewUrl at the built HTML entry
  • Ship the built assets in the npm package
  • Do not depend on Chatons running your bundler on the user's machine

The runtime injects the Chatons bridge into the loaded HTML and serves relative JS/CSS/assets through chaton-extension://<extension-id>/..., so React and other bundler-based UIs can resolve chunks and assets normally.


5. Capabilities

Capabilities are permissions. The runtime checks them before allowing access to host APIs.

CapabilityWhat It Allows
ui.menuAdd sidebar menu items
ui.mainViewCreate full-page views
storage.kvKey-value storage (SQLite-backed)
storage.kvKey-value storage (SQLite-backed)
storage.filesFile-based storage (sandboxed directory)
events.subscribeListen to host events
events.publishEmit custom events
queue.publishEnqueue background jobs
queue.consumeProcess queued jobs
llm.toolsExpose tools to the AI model
host.notificationsShow notifications in the UI
host.conversations.readRead conversation data and messages
host.conversations.writeCreate/modify conversations, channel operations
host.projects.readRead project data

Rule: Only declare capabilities you actually use. If you call an API without declaring its capability, the call is rejected with an unauthorized error.


6. The Runtime API

Extensions access Chatons host functionality through the window.chaton object, which is available in all extension webviews.

This is the only API surface. There is no window.chatonExtension.api.* namespace. All calls go through window.chaton.* with your extension ID as the first parameter.

Quick reference

var EXT_ID = '@yourname/chatons-my-ext';

// Key-value storage
window.chaton.extensionStorageKvGet(EXT_ID, 'key')
window.chaton.extensionStorageKvSet(EXT_ID, 'key', value)
window.chaton.extensionStorageKvDelete(EXT_ID, 'key')
window.chaton.extensionStorageKvList(EXT_ID)

// File storage
window.chaton.extensionStorageFilesRead(EXT_ID, 'path/to/file.txt')
window.chaton.extensionStorageFilesWrite(EXT_ID, 'path/to/file.txt', 'content')

// Events
window.chaton.extensionEventSubscribe(EXT_ID, 'conversation.created', options)
window.chaton.extensionEventPublish(EXT_ID, 'my.custom.event', payload)

// Queue
window.chaton.extensionQueueEnqueue(EXT_ID, 'topic', payload, opts)
window.chaton.extensionQueueConsume(EXT_ID, 'topic', 'consumer-id', opts)
window.chaton.extensionQueueAck(EXT_ID, messageId)
window.chaton.extensionQueueNack(EXT_ID, messageId, retryAt, errorMessage)

// Host calls (notifications, conversations, projects, channels)
window.chaton.extensionHostCall(EXT_ID, 'notifications.notify', { title, body })
window.chaton.extensionHostCall(EXT_ID, 'conversations.list')
window.chaton.extensionHostCall(EXT_ID, 'conversations.getMessages', { conversationId })
window.chaton.extensionHostCall(EXT_ID, 'projects.list')

// Cross-extension API calls
window.chaton.extensionCall(EXT_ID, targetExtId, apiName, versionRange, payload)

// Model listing
window.chaton.listPiModels()

All calls return Promises. See the Extensions API for complete parameter details.


7. Sidebar Menu Items, Topbar Items, and Main Views

Add a sidebar entry that opens your extension's view:

{
  "ui": {
    "menuItems": [
      {
        "id": "myext.menu",
        "label": "My Extension",
        "icon": "BookOpen",
        "order": 50,
        "openMainView": "myext.main"
      }
    ]
  }
}

icon values are Lucide icon names (e.g. "BookOpen", "Settings", "Plus").

order controls position in the sidebar. Lower values appear higher.

Topbar items

Add a topbar contribution without adding a sidebar entry:

{
  "ui": {
    "topbarItems": [
      {
        "id": "myext.ide",
        "label": "Open IDE",
        "icon": "Code2",
        "order": 40,
        "context": "project",
        "openMainView": "myext.main"
      }
    ]
  }
}

context controls when the item is shown:

ValueWhen shown
"always"Always visible
"project"Only for project conversations
"global"Only for global conversations

Topbar items are intended for lightweight controls. They currently support three generic patterns:

  • openMainView: open one of the extension's declared main views
  • deeplink: dispatch an extension deeplink event to the renderer
  • widget: render a compact live widget from one of the extension's declared views

Use widget when you need richer topbar UI than a simple icon button, such as a selector, status pill, or mini-control.

Example widget contribution:

{
  "ui": {
    "topbarItems": [
      {
        "id": "myext.widget",
        "label": "My widget",
        "context": "project",
        "order": 50,
        "widget": {
          "viewId": "myext.widgetView",
          "width": 76,
          "minWidth": 76
        }
      }
    ],
    "mainViews": [
      {
        "viewId": "myext.widgetView",
        "title": "My Widget",
        "webviewUrl": "chaton-extension://@yourname/myext/widget.html"
      }
    ]
  }
}

Widgets are rendered in a compact iframe host and can receive renderer deeplinks using the same chaton.extension.deeplink message contract used by main views.

Every topbar widget also receives a standard chaton.extension.topbarContext postMessage payload containing:

  • extension and item identity
  • current conversation id, project id, worktree path, and access mode
  • current project name and repo path when applicable
  • a normalized thread kind: project, global, or none

Small example: project status pill widget

Below is a minimal widget that shows whether the current thread is attached to a project.

Manifest:

{
  "id": "@yourname/project-pill",
  "name": "Project Pill",
  "version": "1.0.0",
  "capabilities": ["ui.mainView"],
  "ui": {
    "topbarItems": [
      {
        "id": "project-pill.topbar",
        "label": "Project status",
        "context": "always",
        "order": 60,
        "widget": {
          "viewId": "project-pill.widget",
          "width": 96,
          "minWidth": 96
        }
      }
    ],
    "mainViews": [
      {
        "viewId": "project-pill.widget",
        "title": "Project Pill Widget",
        "webviewUrl": "chaton-extension://@yourname/project-pill/widget.html"
      }
    ]
  }
}

widget.html:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <style>
      body {
        margin: 0;
        font: 12px/1.2 system-ui, sans-serif;
        background: transparent;
      }
      .pill {
        height: 34px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 96px;
        padding: 0 10px;
        border-radius: 999px;
        background: rgba(96, 165, 250, 0.14);
        color: #1d4ed8;
      }
    </style>
  </head>
  <body>
    <div id="pill" class="pill">No thread</div>
    <script>
      const pill = document.getElementById('pill')

      window.addEventListener('message', (event) => {
        const message = event.data
        if (message?.type !== 'chaton.extension.topbarContext') return

        const kind = message.payload?.thread?.kind || 'none'
        const projectName = message.payload?.project?.name || null

        if (kind === 'project' && projectName) {
          pill.textContent = `Project: ${projectName}`
        } else if (kind === 'global') {
          pill.textContent = 'Global thread'
        } else {
          pill.textContent = 'No thread'
        }
      })
    </script>
  </body>
</html>

This example is intentionally simple, but it shows the full pattern for a live topbar widget: declare widget.viewId, provide an HTML file, and react to chaton.extension.topbarContext updates.

Main views

Your view runs in a full-width, full-height iframe. It is not constrained to the centered conversation column -- your extension gets the full application content area.

{
  "ui": {
    "mainViews": [
      {
        "viewId": "myext.main",
        "title": "My Extension",
        "webviewUrl": "chaton-extension://@yourname/chatons-my-ext/index.html",
        "initialRoute": "/"
      }
    ]
  }
}

The webviewUrl format is: chaton-extension://<extension-id>/<path-to-html>


8. Quick Actions

Quick actions appear as buttons on the empty conversation screen.

{
  "ui": {
    "quickActions": [
      {
        "id": "myext.create",
        "title": "Create Something",
        "description": "Optional description text",
        "scope": "global-thread",
        "deeplink": {
          "viewId": "myext.main",
          "target": "open-create",
          "params": { "preset": "default" },
          "createConversation": true,
          "prefillPrompt": "Help me create something new"
        }
      }
    ]
  }
}

Limits: Maximum 2 quick actions per extension.

Scope values

ScopeWhen Shown
"always"Always visible
"global-thread"Only when no project is selected
"project-thread"Only when a project is selected
"global-or-no-thread"When no project is selected, or no conversation is active
  • If createConversation is true, a new global conversation is created first
  • If prefillPrompt is set, the text is inserted into the composer
  • The target and params are sent to your extension as a deeplink message

When Chatons opens your view through a deeplink, it sends a message event:

window.addEventListener('message', function (event) {
  var data = event && event.data;
  if (!data || data.type !== 'chaton.extension.deeplink') return;
  var payload = data.payload || {};
  if (payload.viewId !== 'myext.main') return;
  if (payload.target === 'open-create') {
    // Handle the deeplink
  }
});

Ranking

Quick actions are ranked by usage. Chatons tracks click counts with a decayed score (14-day half-life), so recently-used actions appear first.


9. LLM Tools

Extensions can expose tools that the AI model can call during conversations.

Required: capability llm.tools, tool definition in llm.tools[], and a matching exposed API in apis.exposes[].

Manifest

{
  "capabilities": ["llm.tools"],
  "llm": {
    "tools": [
      {
        "name": "myext_get_weather",
        "description": "Get the current weather for a city.",
        "promptSnippet": "Use myext_get_weather to check weather conditions.",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string", "description": "City name" }
          },
          "required": ["city"]
        }
      }
    ]
  },
  "apis": {
    "exposes": [
      { "name": "myext_get_weather", "version": "1.0.0" }
    ]
  }
}

Tool name rules

Tool names sent to the AI model must match ^[a-zA-Z0-9_-]+$. If your manifest tool name contains . or /, Chatons normalizes the name automatically and logs a warning.

Handling tool calls

Tool calls are routed through the cross-extension API system. The model calls your tool, and Chatons dispatches it as an API call to your extension:

window.addEventListener('message', function (event) {
  var data = event && event.data;
  if (!data || data.type !== 'chaton.extension.apiCall') return;
  if (data.apiName !== 'myext_get_weather') return;

  // Process the tool call
  var result = { temperature: 22, condition: 'sunny', city: data.payload.city };

  // Reply to the host
  window.parent.postMessage({
    type: 'chaton.extension.apiCallResponse',
    callId: data.callId,
    result: result,
  }, '*');
});

Requirement sheets

A tool can return a requirement sheet to prompt the user for missing prerequisites. See Requirement Sheets for the full API.


10. Extension Servers

An extension can run a local background process (e.g. a webhook receiver, an API server):

{
  "server": {
    "start": {
      "command": "node",
      "args": ["server.js"],
      "cwd": ".",
      "readyUrl": "http://127.0.0.1:4317/health",
      "readyTimeoutMs": 12000
    }
  }
}

If readyUrl is set, Chatons polls it until it returns HTTP 200 or the timeout expires.

If that readyUrl is already returning HTTP 200 before a new child process is spawned, Chatons treats the server as reusable and skips the duplicate launch attempt. This avoids false-negative startup failures when a local channel bridge is already listening on its configured port after a reload or crash recovery.

When server.start.command is "node", Chatons resolves it through the bundled Electron runtime in packaged builds instead of requiring node to exist on the system PATH. This lets local extension servers start reliably even when the desktop app is launched outside a developer shell.

Desktop-triggered extension package-manager actions (npm install, npm update, npm publish) use that same embedded runtime plus a bundled npm CLI in packaged builds. This avoids GUI-only failures caused by missing shell PATH entries.

Environment variables available to the server process:

  • CHATON_EXTENSION_ID -- Your extension's ID
  • CHATON_EXTENSION_ROOT -- Path to your extension folder
  • CHATON_EXTENSION_DATA_DIR -- Path to your data directory

A server can also be registered dynamically from the UI:

window.chaton.registerExtensionServerFromUi({
  extensionId: '@yourname/chatons-my-ext',
  command: 'node',
  args: ['server.js'],
  readyUrl: 'http://127.0.0.1:4317/health',
});

11. Channel Extensions

Channel extensions bridge external messaging platforms (Telegram, Slack, etc.) with Chatons.

They are identified by kind: "channel" in the manifest and have special behavior:

  • Grouped under a dedicated Channels sidebar entry instead of separate sidebar items
  • Run automatically in hidden background iframes (polling stays alive)
  • Messages go to global threads only (not project conversations)

For the complete guide, see Extensions Channels.


12. Translation

Chatons does not translate extension labels. Fields like ui.menuItems[].label, ui.mainViews[].title, and quick action titles are displayed as-is.

If your extension needs localization, implement it yourself in your JavaScript.


13. Development Workflow

  1. Create a folder:

    mkdir -p ~/.chaton/extensions/@yourname/chatons-my-ext
  2. Add chaton.extension.json, index.html, and any JS/CSS files

  3. Restart Chatons

  4. Open DevTools (F12) to check for errors

  5. After any file changes, restart Chatons

See the Extensions Tutorial for a complete step-by-step example.


14. Current Limitations

  • Restart required after manifest or runtime file changes
  • No host-managed translation of extension labels
  • LLM tool results are JSON-oriented -- no arbitrary code bridge
  • Maximum 2 quick actions per extension
  • Channel classification is a profile on top of the extension system, not a separate subsystem

On this page