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:
- Extensions Tutorial -- Build your first extension
- Extensions API -- API reference with code examples
- Composer Buttons -- Create custom Composer buttons
- Extensions Channels -- Messaging platform bridges
- Extensions UI Library -- UI components and styling
- Requirement Sheets -- Inline sheets for tool prerequisites
- Extension Publishing -- Publish to npm
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:
- Built-in extensions
- Registered user extensions
- 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
| Field | Type | Description |
|---|---|---|
id | string | Unique extension ID. Format: @username/chatons-name |
name | string | Display name shown in the UI |
version | string | Semantic version (e.g. "1.0.0") |
capabilities | string[] | 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
webviewUrlat 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.
| Capability | What It Allows |
|---|---|
ui.menu | Add sidebar menu items |
ui.mainView | Create full-page views |
storage.kv | Key-value storage (SQLite-backed) |
storage.kv | Key-value storage (SQLite-backed) |
storage.files | File-based storage (sandboxed directory) |
events.subscribe | Listen to host events |
events.publish | Emit custom events |
queue.publish | Enqueue background jobs |
queue.consume | Process queued jobs |
llm.tools | Expose tools to the AI model |
host.notifications | Show notifications in the UI |
host.conversations.read | Read conversation data and messages |
host.conversations.write | Create/modify conversations, channel operations |
host.projects.read | Read 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
Menu items
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:
| Value | When 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 viewsdeeplink: dispatch an extension deeplink event to the rendererwidget: 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, ornone
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
| Scope | When 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 |
Deeplink behavior
- If
createConversationistrue, a new global conversation is created first - If
prefillPromptis set, the text is inserted into the composer - The
targetandparamsare sent to your extension as a deeplink message
Receiving deeplinks
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 IDCHATON_EXTENSION_ROOT-- Path to your extension folderCHATON_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
-
Create a folder:
mkdir -p ~/.chaton/extensions/@yourname/chatons-my-ext -
Add
chaton.extension.json,index.html, and any JS/CSS files -
Restart Chatons
-
Open DevTools (F12) to check for errors
-
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