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.
| Field | What it is |
|---|---|
theme.mode | light or dark |
theme.tokens | the app’s colours (surfaces, text, interaction states, primary, success/warning/error/info), font, mono, and radius. Pass this to applyHostReady before you mount UI. |
locale | the app language tag |
directory | current 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. |
surface | panel on the rail, dialog in the attach window, page full-screen, status in the Work Status panel, file as a file editor in Files, block in a UI block in the chat |
item | what 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 |
settings | values 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
| Call | What it does |
|---|---|
toast({ kind, message }) | show a toast in the app. kind is info, success, or error. |
openUrl(url) | open a URL; in the desktop app a local address opens in the browser panel beside the chat |
openSurface(surfaceId) | show that screen, such as "git" or your own panel "plugin:<panel id>". Resolves once it is on screen; a screen that is already open stays open. |
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) | mark your rail icon with a dot and put the count (0 to 999) in its tooltip, or null to clear it |
openCommit(sha) | open the Diff view on that commit of the current project. sha is 7 to 64 hex characters; OpenChamber reads the commit itself. |
setHeight(px) | set the height of your Work Status section. OpenChamber keeps it between 24 and 320 pixels; taller content scrolls inside your page. Other places ignore it. |
onFileOpen(listener) | file editors: get the file to edit, { path, name, readOnly } plus encoding: "text" with content, or encoding: "binary" with bytes. Also makes Cmd/Ctrl+S in your page save. |
onFileSnapshot(handler) | file editors: return { content, version } (or { bytes, version } for a binary editor), the whole edited file. OpenChamber calls it to save and before switching to the source view or fullscreen. |
onFileSaved(listener) | file editors: the snapshot with that version is on disk. Answer with reportFileChange. |
reportFileChange({ dirty, edited }) | file editors: tell OpenChamber whether there are unsaved changes and whether the document just changed |
requestFileSave() | file editors: save now, as Cmd/Ctrl+S does |
reportFileUnsupported() | file editors: this file can’t be opened here, show its source instead |
attach, startSession, and sessionLink
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:
| Field | Max characters |
|---|---|
id | 128 |
title | 200 |
url | 2000 |
text | 16000 |
data (serialized) | 16000 |
author | 80 |
| each branch name | 200 |
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.
Background shells
With the shells capability, watch the shell commands running in the background, scoped to a session and its subagents, a registered project, or every session. OpenChamber answers from its local store; no network read happens.
| Call | What it does |
|---|---|
onRunningShells(scope, listener) | subscribe to the shell commands in scope: { kind: 'session', sessionId } covers the session and its subagents, { kind: 'project', projectId } covers a registered project, and { kind: 'global' } covers every session. You get the running commands oldest first, each with its background flag, and the ones that ended. Returns an unsubscribe function. |
readShellOutput(shellId, { cursor?, tailBytes? }) | one page of a running or ended command’s output. Without cursor it starts tailBytes (up to 65536) before the end. Returns { output, cursor, skipped }. |
stopShell(shellId) | send the agent the stop note first, then remove the command. |
The snapshot is { kind: "shells", scope, shells, ended }, with scope echoing the subscription’s scope. Each running shell is { id, sessionID, command, startedAt, background }, at most 200, oldest first. background is true for a job the turn does not wait for.
ended lists the commands OpenChamber saw end, oldest end first, at most 200 with the newest kept. Each one adds status, endedAt, and exit when the command exited with a code. The status is exited, timeout, killed by a signal, stopped by a user, or unknown when the end happened during a reconnect. OpenChamber keeps this list in memory only. It is empty after the app reloads, so save what your panel shows in extension storage.
OpenCode keeps an ended command’s output for its latest 25 ended commands per directory. A stopped command loses its output right away. Reading a shell OpenChamber never saw, or output OpenCode no longer has, is NOT_FOUND. So is stopping a shell that is not running. An unapproved shells capability is NOT_GRANTED.
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.
Session events
Your page only knows about a session while it is open. Your service can know more: declare contributes.sessionEvents next to contributes.service, and OpenChamber posts the events you list to the service, from every project, whether or not a window is open.
"service": { "entry": "service/main.js", "runtime": "host" },"sessionEvents": { "events": ["turn-finished", "question-asked"], "content": true }The events are turn-started, session-created, session-archived, session-deleted, turn-finished (the agent is done and waits for nothing: no running subagent, no open permission or question, no goal step), turn-failed, permission-asked and question-asked. A turn the user stopped sends nothing. Approving the extension is the subscription; the dialog lists the events under the service line. Without content the service gets identifiers only: the session, its project directory, the items the user linked to it. With "content": true the manifest also asks for a permission the user sees as its own line, Read what the agent says in the background, and each event carries the agent’s final answer, the question’s fields or the permission’s detail.
Each event arrives as one POST /session-events with the service bearer. Read it with readSessionEventRequest and answer { "ok": true }:
import { SESSION_EVENTS_PATH, readSessionEventRequest } from "@openchamber/sdk";
// inside your service's request handler, after the bearer checkif (url.pathname === SESSION_EVENTS_PATH) { const request = readSessionEventRequest(body); if (!request) return json(res, 400, { ok: false, error: "not a session event" }); const { event, content } = request; if (event.type === "turn-finished" && event.project.directory === myProject) { await postComment(event.linked, content?.type === "turn-finished" ? content.reply : ""); } return json(res, 200, { ok: true });}Every project’s events are sent; compare event.project.directory with your own settings and answer { "ok": true } for the rest. Delivery is at most once and in order, one request at a time; a failed answer drops your events for 30 seconds, and nothing is replayed after OpenChamber or your service was down. The service needs no panel for this. The session-events-log example journals every event to a file.
Calling OpenChamber from your service
A service can act with no window open. createServiceHost is its way back:
import { createServiceHost } from '@openchamber/sdk';
const host = createServiceHost({ env: process.env });
const { sessionId } = await host.sessions.start({ prompt: 'Summarize the failed deploy', directory });await host.sessions.send(sessionId, 'Now write the incident note');await host.setInstructions(directory, 'Deploys run through the release branch.');await host.emit('deploy-finished', { id: 42 });sessions.start, sessions.send, sessions.retry and sessions.status need contributes.sessionControl, which the user approves as its own line. sessions.lastReply needs sessionControl: { content: true }. setInstructions needs contributes.instructions and adds to what agents read, in one folder or, with null, everywhere.
emit reaches your open frames through host.onServiceEvent, so a panel stops polling. Nothing replays, so a frame that opens later reads the current state first. readSettings gives the service the integration’s settings, secret fields included, and needsConfiguration shows a message on the extension’s card.
Running with nobody asking
A service starts on the first request, and a role’s service stops when idle. service.keepRunning: true starts it with OpenChamber and brings it back when it stops. service.schedule wakes it on a clock:
"service": { "entry": "service/main.js", "runtime": "host", "keepRunning": true, "schedule": [{ "id": "nightly", "cron": "0 3 * * *", "timezone": "Europe/Kyiv" }]}A run is one POST /schedule. Read it with readServiceScheduleRequest and answer 200. A run missed while OpenChamber was down is gone. A service that needs outside events holds its own connection or polls.
Rows in the @ menu
contributes.mentions: { label } adds a section to the composer’s @ menu. Your service answers POST /mentions/search with { items: [{ id, title, subtitle?, url? }] }, within 2 seconds, and POST /mentions/resolve with { text } for the row the user picks. The text goes to the agent with the message, and the user sees a chip. A slow service adds no rows and never holds the files back; a row that cannot be read leaves no chip and says so.
Blocks in the chat
contributes.blocks adds components agents can write in a UI block. Your own page draws each one, in the same sandbox and with the same grants as your panel.
"blocks": [{ "name": "JiraIssue", "description": "A Jira ticket", "args": ["key"], "entry": "blocks/issue.html" }]The agent writes JiraIssue("OPE-1"), and your page reads the call:
host.onBlock(({ args }) => draw(args.key));Call host.setHeight(px) from a ResizeObserver on your root. The frame fills its box with no padding, so add your own. Up to 4 components per extension, since every signature is text each session reads.
A block can answer the agent, as a button in a block does. Call host.answerBlock(text) from a click: the agent gets the text and starts its next turn. One answer per block, and block.answered holds it after a reload, so draw the block as answered then.
host.onBlock(({ args, answered }) => draw(args.key, answered));button.onclick = () => host.answerBlock('Close this ticket');Set "answers": true on a component that does this. Agents are told to ask with it instead of the built-in choice when its description fits, and a page that waits for an answer leaves out its Send button. Under such a block OpenChamber offers the same note field as its own forms: the agent reads the note with your answer, and a note on its own is an answer too.
The arguments are written by a model, so text from a web page or a ticket can steer them. Treat them as untrusted, and do nothing with side effects until the user clicks.
Notification channels
A service can deliver the user’s notifications, next to the desktop and the phone: declare provides: ["notifications"] and describe the channel in contributes.notifications.
"service": { "entry": "service/main.js", "runtime": "host", "provides": ["notifications"] },"notifications": { "label": "Slack", "instances": { "kind": "fixed", "origin": "https://slack.com" }, "token": { "header": "authorization", "scheme": "bearer", "scopes": ["chat:write"] }}OpenChamber draws an account card for the channel in Settings → Integrations from that description, and keeps the account: the user connects it there, the token stays in OpenChamber, and your service never sees it. Once an account is connected, every notification OpenChamber would send to the phone goes to your service too, after the user’s own notification toggles, and only while no window is visible unless the user chose to be notified always. Pausing the extension stops it.
OpenChamber forwards three calls to POST /notifications: auth.probe (is this server one you deliver to), auth.verify (who does this token belong to) and notify (one notification). Read each with readNotificationsProviderRequest, and reach the channel’s server only through the outbound helper, which posts your request to OpenChamber with the call’s handle; OpenChamber adds the token and keeps the request on the account’s server.
import { NOTIFICATIONS_PROVIDER_PATH, createNotificationsOutbound, readNotificationsProviderRequest } from "@openchamber/sdk";
const outbound = createNotificationsOutbound({ env: process.env });
// inside your service's request handler, after the bearer checkif (url.pathname === NOTIFICATIONS_PROVIDER_PATH) { const request = readNotificationsProviderRequest(body); if (!request) return json(res, 400, { ok: false, error: { message: "not a notifications call" } }); if (request.method === "notify") { const { notification } = request.input as { notification: GuestNotification }; const answer = await outbound.request(request.session.handle, { method: "POST", path: "/api/chat.postMessage", headers: { "content-type": "application/json" }, body: JSON.stringify({ channel: "#agents", text: `${notification.title}: ${notification.body}` }), }); return json(res, 200, answer.status === 200 ? { ok: true, data: { delivered: true } } : { ok: false, error: { message: `Slack answered ${answer.status}`, status: answer.status } }); } // auth.probe → { ok: true, data: { supported: true } }; auth.verify → { ok: true, data: { account: { id, login } } }}Which room or channel a project goes to is your extension’s own choice: the notification carries directory and sessionName. In enterprise mode it carries the generic title only, as the phone does. The notification-webhook example is a complete channel with a receiver to try it against.
Trackers
A service can be a work item tracker next to Linear: declare provides: ["tracker"] and describe the tracker in contributes.tracker. OpenChamber then lists it beside Linear everywhere trackers appear, from the manifest alone, before the service ever runs.
"service": { "entry": "service/main.js", "runtime": "host", "provides": ["tracker"] },"tracker": { "wire": 1, "label": "YouTrack", "instances": { "kind": "user", "label": "Server address", "placeholder": "https://youtrack.example.com" }, "auth": { "token": { "header": "authorization", "scheme": "bearer", "createPath": "/users/me?tab=account-security" } }, "capabilities": { "stateUpdate": true, "labels": true, "priority": true, "projectMapping": true, "sessionComments": true }, "stateFilters": ["open", "all", "todo", "in-progress", "done"], "identifier": "key-number", "urls": { "item": ["/issue/{id}", "/{segments+}/issue/{id}"] }}The user connects accounts on the tracker’s card under Extension accounts: a token, or a browser sign-in when you declare auth.oauth (OpenChamber runs the sign-in with the client id the user enters and keeps the tokens). Your service never sees a credential. urls.item are templates for an item’s address, with {id} for the identifier (also as ?name={id} when it travels in the query); OpenChamber compiles them so pasted links become linked items. identifier says what the key looks like: key-number (PROJ-42), number, or short-code.
OpenChamber forwards nine calls to POST /tracker: auth.probe and auth.verify, the reads items.list, items.get, items.liveSummaries, states.list and containers.list, and the writes items.updateState and comments.create. Read each with readTrackerProviderRequest, answer in the neutral item shapes, and reach the tracker’s server only through the outbound helper, which posts your request to OpenChamber with the call’s handle; OpenChamber adds the credential and keeps the request on the account’s server. A read’s handle sends GET only.
import { TRACKER_PROVIDER_PATH, createTrackerOutbound, readTrackerProviderRequest } from "@openchamber/sdk";
const outbound = createTrackerOutbound({ env: process.env });
// inside your service's request handler, after the bearer checkif (url.pathname === TRACKER_PROVIDER_PATH) { const request = readTrackerProviderRequest(body); if (!request) return json(res, 400, { ok: false, error: { message: "not a tracker call" } }); if (request.method === "items.get") { const answer = await outbound.request(request.session.handle, { method: "GET", path: `/api/issues/${encodeURIComponent(String(request.input.id))}` }); if (answer.status === 404) return json(res, 200, { ok: true, data: { item: null } }); if (answer.status !== 200) return json(res, 200, { ok: false, error: { message: `YouTrack answered ${answer.status}`, status: answer.status } }); return json(res, 200, { ok: true, data: { item: toTrackerItem(JSON.parse(answer.body)) } }); }}When the user turns session comments on, OpenChamber calls comments.create with “session started”, “completed” or “failed” and a link to the session, once each; the comment carries nothing else. The tracker-provider example is a complete tracker with a fake server to try it against.
Usage providers
A service can report usage next to the built-in providers: declare provides: ["quota"] and list in contributes.quota.providers the OpenCode providers whose credentials it needs. OpenChamber names the provider ext-<panel.id>, draws its card with the extension’s name and icon, and asks it the way it asks a built-in.
"service": { "entry": "service/main.js", "runtime": "host", "provides": ["quota"] },"quota": { "providers": ["openrouter"] }The approval dialog lists those providers, and a version that changes the list asks again. On every refresh OpenChamber posts one call, usage.read, to POST /quota, with input.credentials holding the credential OpenCode stores for each listed provider it has one for (an API key, or a browser login’s tokens) and nothing else of the store. A listed provider OpenCode holds no credential for is shown as not configured and the service is not called; an empty list is allowed, for a provider whose key lives in the extension’s own settings.
Read the body with readQuotaProviderRequest and answer { ok: true, data } with the result a built-in computes: ok, configured, error, planLabel, and usage.windows keyed by window name, each with usedPercent, windowSeconds, resetAt (epoch milliseconds) and an optional valueLabel or value, plus usage.models for per-model windows. OpenChamber derives the remaining share, the reset countdown and the formatted strings, so the card matches a built-in’s.
import { QUOTA_PROVIDER_PATH, readQuotaProviderRequest } from "@openchamber/sdk";
// inside your service's request handler, after the bearer checkif (url.pathname === QUOTA_PROVIDER_PATH) { const request = readQuotaProviderRequest(body); if (!request) return json(res, 400, { ok: false, error: { message: "not a usage call" } }); const credential = request.input.credentials.openrouter; const bearer = credential.type === "api" ? credential.key : credential.access; const answer = await fetch("https://openrouter.ai/api/v1/key", { headers: { authorization: `Bearer ${bearer}` } }); if (answer.status === 401) return json(res, 200, { ok: true, data: { ok: false, configured: true, error: "OpenRouter refused the key.", usage: null } }); return json(res, 200, { ok: true, data: toUsage(await answer.json()) });}The service answers within 20 s and stops after ten minutes without a call; the Usage page refreshes every three minutes while open. The quota-provider example is a complete provider with a fake usage API to try it against.
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 960 seconds. No usable Small Model is NO_MODEL; a model error is MODEL_FAILED. See Generate text.
The user may close your panel or switch to another project before the answer arrives. The generation still finishes, and the app keeps the answer for your next panel on the same surface and project, in the same window. Register onLateGenerateResult to receive it there:
host.onLateGenerateResult((late) => { if (late.ok) showSummary(late.request.prompt, late.result.text); else showError(late.error);});late.request is what the closed panel sent, so you can tell which request it answers. Each answer arrives once. The app keeps answers for an hour, at most 8 per extension, and drops them when the extension is disabled or removed. A reload of the whole app or another window starts empty. Apps that do not keep late answers leave ready.features.lateGenerateResults unset and never call the listener.
Error codes
Failed calls throw HostRequestError. code is one of these; message says more.
| Code | When |
|---|---|
HOST_UNAVAILABLE | no window, not inside OpenChamber, or dispose() ran |
HOST_TIMEOUT | no answer for 20 seconds |
HOST_REJECTED | OpenChamber refused, or answered with a code this SDK does not know |
DISCONNECTED | no token or Linear connection for your integration |
BAD_PATH | path was malformed or tried to leave the allowed origin |
NO_INTEGRATION | the manifest has no integration |
NO_SESSION | prompt or sessionLink with no open session |
SESSION_BUSY | prompt({ send: true }) while the session was busy |
DISABLED | the user paused the extension in Settings → Extensions |
NO_SERVICE | no service declared, not approved, or not running |
NOT_GRANTED | the user has not approved the capability this call needs |
SERVICE_FAILED | the service crashed or never became ready |
NO_DIRECTORY | a relative file path was used with no open project, openCommit with no open project, or openSurface before any chat or project is open |
NOT_FOUND | readFile on a file that does not exist, or openCommit on a commit that is not in the project |
FILE_TOO_LARGE | file content over the limit, on read or write |
DENIED | the operating system refused the file operation |
NO_MODEL | generate with no Small Model available |
MODEL_FAILED | the Small Model returned an error |
UNSUPPORTED | this OpenChamber cannot do the call, for example openCommit where there is no Diff view, or openSurface for a screen that cannot open there |
Related
- Build an extension for the folder, manifest, install, and capabilities
- UI kit for buttons, fields, lists, and the other building blocks