Skip to content
Navigate Open escClose

Host API

Use this page when you need the exact connectHost methods, limits, and error codes. For the folder layout, the manifest, and installing, start at Build an extension.

import { connectHost, HostRequestError } from "@openchamber/sdk";
const host = connectHost();

connectHost throws HOST_UNAVAILABLE when there is no window. Outside OpenChamber it returns a client whose every call rejects with HOST_UNAVAILABLE. Call dispose() when you tear the page down; calls still in flight reject with the same code.

What OpenChamber pushes to you

onReady fires with the first snapshot and again whenever OpenChamber refreshes it.

It is not a one-time mount callback. Mount your UI and register subscriptions once; use later snapshots to update the theme and context without replacing inputs or drafts. See the guarded example in UI kit. Field listeners such as onConnection replay their current value and can receive it again with a refreshed snapshot. Compare the fields your data depends on before starting another request, and ignore responses from superseded requests.

FieldWhat it is
theme.modelight or dark
theme.tokensthe app’s colours (surfaces, text, interaction states, primary, success/warning/error/info), font, mono, and radius. Pass this to applyHostReady before you mount UI.
localethe app language tag
directorycurrent project directory, or null
session{ id, title, busy } or null. Title falls back to the session id. busy is the live status. model and agent (the OpenCode agent) appear when the session has them.
surfacepanel on the rail, dialog in the attach window, page full-screen
itemwhat this surface was opened for, or null: the attached item the user clicked (the same fields you passed to attach, including data), a message (kind: "message"), or a session (kind: "session") from one of your declared actions. See Actions.
connection{ connected, account } for your integration
settingsvalues of the fields you declared in integration.settings

Access tokens never appear here or in a request result.

onDirectory, onSession, onSessionLifecycle, onConnection, onSettings, and onItem replay the latest value when you subscribe late, then keep firing as it changes.

theme.tokens includes primaryText, successText, warningText, errorText, and infoText. These required fields contain host-computed text colors for neutral backgrounds and the UI kit’s tinted controls, not solid fills. applyHostReady applies them on each snapshot. See UI kit for the CSS variables.

Methods

CallWhat it does
toast({ kind, message })show a toast in the app. kind is info, success, or error.
openUrl(url)open a URL in the user’s browser
openSurface(surfaceId)switch the app to that screen
writeClipboard(text)copy text. 1 to 32000 characters.
compose({ text, mode? })put text in the chat box without sending. mode is append (default) or replace. 1 to 16000 characters after trim.
attach({ ... })put a chip on the chat box, the same place GitHub and Linear items land. Only one chip at a time.
startSession({ ... })create a session with that item attached. Returns { sessionId, sent }. Needs the sessions capability.
prompt({ text, send? })write into, or send on, the current session. Returns { sent }. Sending needs the prompt capability.
sessionLink({ ... })attach an item to the current session without creating one
close()dismiss the attach window. Does nothing on the rail.
oauthStart()open the provider’s authorize page, or the Linear one for a host: { provider: "linear" } integration
oauthDisconnect()forget the stored token, or the Linear connection
request({ method, path, query?, body? })call your integration’s apiOrigin with the user’s token attached
serviceRequest({ method, path, query?, body? })call your extension’s local service (see GUEST_SERVICES.md)
serviceStatus()stopped, starting, ready, or failed
readFile(path)read a text file. Returns { content }.
writeFile(path, content)write a text file atomically, creating parent folders. Returns { written: true }.
listDir(path)list a folder. Returns { entries: [{ name, kind }] }, kind is file, directory, or other.
stat(path){ kind, size, mtime }, kind is file, directory, other, or missing.
generate({ prompt, system?, maxOutputTokens? })one-off text from the user’s Small Model. Returns { text }. Needs the model capability.
onResolve(handler)register the handler for your slash commands. It gets { command, args } and returns an attach item or null.
setBadge(count)show a number (0 to 999) on your rail icon, or null to clear it

All three take the same item fields:

await host.attach({
providerId: "acme-hello",
id: "TICKET-1",
title: "Login is broken",
url: "https://example.com/TICKET-1",
});
await host.attach({
providerId: "acme-hello",
id: "!12",
title: "Fix login",
url: "https://example.com/merge_requests/12",
kind: "pull",
author: "ada",
branches: { head: "feature", base: "main" },
text: "Optional notes for the model",
});
await host.startSession({
providerId: "acme-hello",
id: "!12",
title: "Fix login",
url: "https://example.com/merge_requests/12",
kind: "pull",
worktree: true,
text: "Optional first message",
});

providerId is your panel id; OpenChamber overwrites it with your id anyway. id is your own identifier for the item. kind is issue (default) or pull. text is optional context for the model, 1 to 16000 characters after trim. data is optional JSON of your own (status, comments, anything), up to 16000 characters when serialized. OpenChamber stores it with the chip and hands it back unchanged in ctx.item when the user clicks the chip; it never reaches the model.

startSession accepts projectId without switching the current project. Omit worktree for the target directory, use true for a generated worktree, { kind: "existing", directory } for a known worktree, or { kind: "new", name?, baseBranch? } for a named new one. The name sets both branch and worktree. navigation defaults to "preserve"; use "open" to select the new chat. The first-message model, agent, and variant are captured when creation starts.

The result includes sessionId, directory, sent, linked, and optional worktree. sent is sent, no-model, skipped, or failed; linked: false means the item was not saved on the created session. A worktree left after bootstrap or session creation fails returns sessionId: null and failure: "bootstrap-failed" | "session-create-failed", plus its directory/worktree. Inspect that result before retrying. The call waits up to 180 seconds; a timeout does not mean creation was rolled back.

sessionLink attaches the item to the session that is open now. No project or no session is a NO_SESSION error.

Limits the client applies before anything is sent:

FieldMax characters
id128
title200
url2000
text16000
data (serialized)16000
author80
each branch name200

prompt and session lifecycle

await host.prompt({ text: "Fix the login" });
await host.prompt({ text: "Fix the login", send: true });
host.onSessionLifecycle((event) => {
document.body.dataset.phase = event.phase;
});

Without send, prompt replaces the chat box text. With send: true it sends the message using the model and agent the user has selected. The extension never picks them. No open session is NO_SESSION. Sending while the session is busy is SESSION_BUSY; writing into the box works while busy. The result is { sent } with the same values as startSession.

onSessionLifecycle tells you what the session is doing. phase is started while the model works, completed when it goes idle, and failure on an unexpected status. A late listener gets the current phase right away.

Projects, worktrees, and live sessions

These methods use the existing sessions permission and the app’s shared stores. They do not run a git scan for each request or expose conversation content.

const projects = await host.listProjects();
const projectId = projects.projects[0]?.id;
if (projectId) {
const worktrees = await host.listWorktrees(projectId);
const sessions = await host.listSessions(projectId);
const stop = await host.onSessions(projectId, (snapshot) => {
renderSessions(snapshot.sessions, snapshot.state);
});
// Call stop() when this view closes.
}

onProjects(listener) and onWorktrees(projectId, listener) work the same way. Await registration to catch permission errors; the result is an unsubscribe function. Each sends an initial snapshot followed by changes. dispose() releases all subscriptions. The host allows at most 32 per frame and clears them on unmount, pause, removal, or server switch.

Snapshots have state: "loading" | "ready" | "error". Loading/error may retain data; only ready establishes complete empty success. Session snapshots include per-directory coverage and known archived sessions. Projects expose ID, name, and directory; worktrees expose directory, name, branch, and availability. Sessions include their project/directory/worktree, timestamps, parent ID, and this extension’s attached item IDs/data.

Session activity is unknown, idle, running, retrying, waiting-permission, or waiting-question. outcome is an observed completed/failed turn or null. It is not reconstructed from history and is retained in memory for at most 2,000 observed sessions. Idle after an error keeps the failed outcome until another run starts. Neither idle nor completed marks your task Done. Use host.openSession(sessionId) for an explicit transition from a card to its chat.

Persistent extension data

await host.storage.set("board", { columns: ["Todo", "Review"] });
const board = await host.storage.get("board");
const keys = await host.storage.keys();
await host.storage.delete("board");

No extra permission is needed. Storage belongs to this extension on the connected server, survives reloads, and is removed on uninstall. get returns undefined for a missing key; stored JSON null stays null. Keys have 1 to 128 characters. Each serialized JSON value is at most 64 KiB UTF-8, and the namespace is at most 2 MiB or 2,000 keys. Use a project ID in the key for project-specific data. Writes serialize and are atomic; failures preserve existing data.

request

const user = await host.request({ method: "GET", path: "/api/v2/user" });

path starts with / and has no scheme or host; OpenChamber joins it to the apiOrigin from your manifest and adds the Authorization header. Token integrations send the pasted token as is, as Bearer <token> when the manifest sets token.scheme: "bearer", or as Basic base64(username:token) when it sets "basic". OAuth and Linear integrations always send Bearer. The result is { status, body }. The body is text; parse JSON yourself.

A Linear integration may also GET /api/linear/issues/get, which OpenChamber answers from its own Linear route.

No answer within 20 seconds is HOST_TIMEOUT.

try {
await host.request({ method: "GET", path: "/api/v2/user" });
} catch (error) {
if (error instanceof HostRequestError && error.code === "DISCONNECTED") {
await host.oauthStart();
}
}

serviceRequest

When the manifest declares contributes.service, OpenChamber starts that process and your page talks to it through the same kind of call. The page never opens the socket itself.

const status = await host.serviceStatus();
const result = await host.serviceRequest({ method: "GET", path: "/containers" });

path follows the same rules as request. Declaring a service adds service to the capabilities the user approves at install; until then every serviceRequest is NO_SERVICE. Full contract: the package file GUEST_SERVICES.md.

Files

A relative path is inside the open project and needs the files capability. A path starting with / or ~/ must match a declared contributes.filesystem pattern and needs the filesystem capability. See Build an extension for the rules.

const { content } = await host.readFile("package.json");
await host.writeFile("notes/today.md", "# Today\n");
const { entries } = await host.listDir("src");
const info = await host.stat("~/.config/opencode/opencode.json");

Limits: path up to 1024 characters, content up to 2,000,000 characters, listing up to 2000 entries. Over the content limit is FILE_TOO_LARGE.

generate

const { text } = await host.generate({ prompt: notes, system: "Summarize in one sentence." });

prompt is 1 to 64,000 characters after trim, system up to 8,000, maxOutputTokens 1 to 4,000. Nothing enters a session and no history is kept; the extension never chooses the model. The call waits up to 90 seconds. No usable Small Model is NO_MODEL; a model error is MODEL_FAILED. See Generate text.

Error codes

Failed calls throw HostRequestError. code is one of these; message says more.

CodeWhen
HOST_UNAVAILABLEno window, not inside OpenChamber, or dispose() ran
HOST_TIMEOUTno answer for 20 seconds
HOST_REJECTEDOpenChamber refused, or answered with a code this SDK does not know
DISCONNECTEDno token or Linear connection for your integration
BAD_PATHpath was malformed or tried to leave the allowed origin
NO_INTEGRATIONthe manifest has no integration
NO_SESSIONprompt or sessionLink with no open session
SESSION_BUSYprompt({ send: true }) while the session was busy
DISABLEDthe user paused the extension in Settings → Extensions
NO_SERVICEno service declared, not approved, or not running
NOT_GRANTEDthe user has not approved the capability this call needs
SERVICE_FAILEDthe service crashed or never became ready
NO_DIRECTORYa relative file path was used with no open project
NOT_FOUNDreadFile on a file that does not exist
FILE_TOO_LARGEfile content over the limit, on read or write
DENIEDthe operating system refused the file operation
NO_MODELgenerate with no Small Model available
MODEL_FAILEDthe Small Model returned an error
  • Build an extension for the folder, manifest, install, and capabilities
  • UI kit for buttons, fields, lists, and the other building blocks