API del host
Usa esta página cuando necesites los métodos exactos de connectHost, los límites y los códigos de error. Para la estructura de la carpeta, el manifiesto y la instalación, empieza en Crea una extensión.
import { connectHost, HostRequestError } from "@openchamber/sdk";
const host = connectHost();connectHost lanza HOST_UNAVAILABLE cuando no hay window. Fuera de OpenChamber devuelve un cliente cuyas llamadas se rechazan todas con HOST_UNAVAILABLE. Llama a dispose() cuando desmontes la página; las llamadas todavía en curso se rechazan con el mismo código.
Lo que OpenChamber te envía
onReady se dispara con la primera instantánea y de nuevo cada vez que OpenChamber la actualiza.
No es un callback de montaje único. Monta la UI y registra suscripciones una vez; después actualiza el tema y el contexto sin reemplazar campos ni borradores. Consulta el ejemplo protegido en UI kit. Listeners como onConnection entregan el valor actual inmediatamente y pueden repetirlo con nuevas instantáneas. Compara los campos relevantes antes de iniciar otra petición e ignora respuestas de peticiones reemplazadas.
| Campo | Qué es |
|---|---|
theme.mode | light o dark |
theme.tokens | los colores de la app (superficies, texto, estados de interacción, primario, success/warning/error/info), font, mono y radius. Pásalo a applyHostReady antes de montar la UI. |
locale | la etiqueta de idioma de la app |
directory | el directorio del proyecto actual, o null |
session | { id, title, busy } o null. Si no hay título, se usa el id de la sesión. busy es el estado en vivo. model y agent (el agente de OpenCode) aparecen cuando la sesión los tiene. |
surface | panel en la barra lateral, dialog en la ventana de adjuntar, page a pantalla completa, status en el panel Work Status, file como editor de archivos en Files, block en un bloque de UI del chat |
item | para qué se abrió esta superficie, o null: el elemento adjunto en el que el usuario hizo clic (los mismos campos que pasaste a attach, incluido data), un mensaje (kind: "message") o una sesión (kind: "session") desde una de tus acciones declaradas. Ver Acciones. |
connection | { connected, account } de tu integración |
settings | los valores de los campos que declaraste en integration.settings |
Los tokens de acceso nunca aparecen aquí ni en el resultado de un request.
onDirectory, onSession, onSessionLifecycle, onConnection, onSettings y onItem reproducen el último valor si te suscribes tarde, y luego siguen disparándose cada vez que cambia.
theme.tokens incluye primaryText, successText, warningText, errorText e infoText. Estos campos obligatorios contienen colores de texto calculados por el host para fondos neutros y controles con tintes suaves del UI kit, no para rellenos de color sólido. applyHostReady los aplica en cada instantánea. Consulta UI kit para las variables CSS.
Métodos
| Llamada | Qué hace |
|---|---|
toast({ kind, message }) | muestra un aviso en la app. kind es info, success o error. |
openUrl(url) | abre una URL en el navegador del usuario |
openSurface(surfaceId) | cambia la app a esa pantalla |
writeClipboard(text) | copia texto. De 1 a 32000 caracteres. |
compose({ text, mode? }) | pone texto en el cuadro de chat sin enviarlo. mode es append (por defecto) o replace. De 1 a 16000 caracteres tras recortar espacios. |
attach({ ... }) | pone un chip en el cuadro de chat, en el mismo sitio donde caen los elementos de GitHub y Linear. Solo un chip a la vez. |
startSession({ ... }) | crea una sesión con ese elemento adjunto. Devuelve { sessionId, sent }. Necesita la capacidad sessions. |
prompt({ text, send? }) | escribe en la sesión actual, o envía en ella. Devuelve { sent }. Enviar necesita la capacidad prompt. |
sessionLink({ ... }) | adjunta un elemento a la sesión actual sin crear una nueva |
close() | cierra la ventana de adjuntar. No hace nada en la barra lateral. |
oauthStart() | abre la página de autorización del proveedor, o la de Linear para una integración host: { provider: "linear" } |
oauthDisconnect() | olvida el token guardado, o la conexión con Linear |
request({ method, path, query?, body? }) | llama al apiOrigin de tu integración con el token del usuario adjunto |
serviceRequest({ method, path, query?, body? }) | llama al servicio local de tu extensión (consulta GUEST_SERVICES.md) |
serviceStatus() | stopped, starting, ready o failed |
readFile(path) | lee un archivo de texto. Devuelve { content }. |
writeFile(path, content) | escribe un archivo de texto de forma atómica, creando las carpetas superiores. Devuelve { written: true }. |
listDir(path) | lista una carpeta. Devuelve { entries: [{ name, kind }] }, kind es file, directory u other. |
stat(path) | { kind, size, mtime }, kind es file, directory, other o missing. |
generate({ prompt, system?, maxOutputTokens? }) | texto puntual del Small Model del usuario. Devuelve { text }. Necesita la capacidad model. |
onResolve(handler) | registra el manejador de tus comandos de barra. Recibe { command, args } y devuelve un elemento para adjuntar o null. |
setBadge(count) | marca tu icono de la barra lateral con un punto y muestra el número (0 a 999) en su descripción emergente, o null para quitarlo |
openCommit(sha) | abrir la vista Diff en ese commit del proyecto actual. sha tiene de 7 a 64 caracteres hexadecimales; OpenChamber lee el commit por su cuenta. |
setHeight(px) | fijar la altura de tu sección de Work Status. OpenChamber la mantiene entre 24 y 320 píxeles; el contenido más alto se desplaza dentro de tu página. En otros sitios se ignora. |
onFileOpen(listener) | editores de archivos: recibir el archivo que se va a editar, { path, name, readOnly } más encoding: "text" con content, o encoding: "binary" con bytes. También hace que Cmd/Ctrl+S guarde dentro de tu página. |
onFileSnapshot(handler) | editores de archivos: devolver { content, version } (o { bytes, version } en un editor binario), el archivo editado completo. OpenChamber lo llama para guardar y antes de cambiar a la vista de código fuente o a pantalla completa. |
onFileSaved(listener) | editores de archivos: la instantánea con esa version está en disco. Responde con reportFileChange. |
reportFileChange({ dirty, edited }) | editores de archivos: indicar a OpenChamber si hay cambios sin guardar y si el documento acaba de cambiar |
requestFileSave() | editores de archivos: guardar ahora, igual que Cmd/Ctrl+S |
reportFileUnsupported() | editores de archivos: este archivo no se puede abrir aquí, mostrar su código fuente en su lugar |
attach, startSession y sessionLink
Los tres aceptan los mismos campos de elemento:
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 es el id de tu panel; OpenChamber lo sobrescribe con tu id de todas formas. id es tu propio identificador para el elemento. kind es issue (por defecto) o pull. text es contexto opcional para el modelo, de 1 a 16000 caracteres tras recortar espacios. data es JSON opcional a tu gusto (estado, comentarios, lo que sea), hasta 16000 caracteres una vez serializado. OpenChamber lo guarda junto con el chip y te lo devuelve sin cambios en ctx.item cuando el usuario hace clic en el chip; nunca llega al modelo.
startSession acepta projectId sin cambiar el proyecto activo. Omite worktree para el directorio de destino, usa true para uno generado, { kind: "existing", directory } para uno conocido o { kind: "new", name?, baseBranch? } para uno nuevo. El nombre se aplica a la rama y al worktree. navigation es "preserve" por defecto; "open" abre el nuevo chat. El modelo, agente y variante del primer mensaje se capturan al iniciar.
El resultado incluye sessionId, directory, sent, linked y worktree opcional. sent es sent, no-model, skipped o failed; linked: false indica que no se guardó el elemento. Si queda un worktree tras fallar la preparación o la creación de sesión, se devuelve sessionId: null con failure: "bootstrap-failed" | "session-create-failed". Revisa el resultado antes de reintentar. El límite es de 180 segundos; un timeout no demuestra que se revirtiera la creación.
sessionLink adjunta el elemento a la sesión abierta en ese momento. Si no hay proyecto o no hay sesión, el error es NO_SESSION.
Límites que el cliente aplica antes de enviar nada:
| Campo | Máximo de caracteres |
|---|---|
id | 128 |
title | 200 |
url | 2000 |
text | 16000 |
data (serializado) | 16000 |
author | 80 |
| cada nombre de rama | 200 |
prompt y ciclo de vida de la sesión
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;});Sin send, prompt reemplaza el texto del cuadro de chat. Con send: true envía el mensaje con el modelo y el agente que el usuario tiene seleccionados. La extensión nunca los elige. Si no hay sesión abierta, el error es NO_SESSION. Enviar mientras la sesión está ocupada es SESSION_BUSY; escribir en el cuadro sí funciona mientras está ocupada. El resultado es { sent } con los mismos valores que startSession.
onSessionLifecycle te dice qué está haciendo la sesión. phase es started mientras el modelo trabaja, completed cuando queda inactivo y failure ante un estado inesperado. Un oyente que se suscribe tarde recibe la fase actual de inmediato.
Proyectos, sesiones en directo y almacenamiento
listProjects(), listWorktrees(projectId) y listSessions(projectId) usan el permiso sessions y los stores compartidos, sin recorrer Git por llamada ni exponer conversaciones. Los proyectos tienen ID, nombre y directorio; los worktrees añaden rama y disponibilidad. Las sesiones incluyen metadatos, fechas, padre, worktree y solo los elementos adjuntos de esta extensión. Se incluyen las sesiones archivadas conocidas.
await onProjects(listener), await onWorktrees(projectId, listener) y await onSessions(projectId, listener) devuelven una función para cancelar la suscripción. Captura los errores al registrarla. Envían una instantánea inicial y después cambios. Máximo 32 por iframe; dispose(), cerrar, pausar, eliminar o cambiar de servidor las liberan. state es loading, ready o error, con coverage por directorio para sesiones. Los errores conservan datos; solo ready confirma una lista vacía completa.
activity es unknown, idle, running, retrying, waiting-permission o waiting-question. outcome es completed, failed o null según eventos observados, en memoria para hasta 2.000 sesiones. Idle después de un error conserva el fallo hasta el siguiente inicio. Esto no marca tu tarea como terminada. openSession(sessionId) abre el chat explícitamente.
host.storage.get(key), set(key, value), delete(key) y keys() guardan JSON propio sin otro permiso. Una clave ausente devuelve undefined; null guardado sigue siendo null. Las claves tienen de 1 a 128 caracteres, cada valor hasta 64 KiB UTF-8 y el conjunto hasta 2 MiB y 2.000 claves. Usa el ID del proyecto en la clave para datos por proyecto. Las escrituras se serializan y son atómicas; los errores conservan los datos. Desinstalar elimina este almacenamiento del servidor conectado.
request
const user = await host.request({ method: "GET", path: "/api/v2/user" });path empieza por / y no lleva esquema ni host; OpenChamber lo une al apiOrigin de tu manifiesto y añade la cabecera Authorization. Las integraciones con token envían el token pegado tal cual, como Bearer <token> cuando el manifiesto define token.scheme: "bearer", o como Basic base64(username:token) cuando define "basic". Las integraciones OAuth y de Linear siempre envían Bearer. El resultado es { status, body }. El cuerpo es texto; analiza el JSON tú mismo.
Una integración de Linear también puede hacer GET /api/linear/issues/get, que OpenChamber responde desde su propia ruta de Linear.
Si no hay respuesta en 20 segundos, el error es 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
Cuando el manifiesto declara contributes.service, OpenChamber inicia ese proceso y tu página habla con él mediante el mismo tipo de llamada. La página nunca abre el socket por su cuenta.
const status = await host.serviceStatus();const result = await host.serviceRequest({ method: "GET", path: "/containers" });path sigue las mismas reglas que request. Declarar un servicio local añade service a las capacidades que el usuario aprueba al instalar; hasta entonces, cada serviceRequest es NO_SERVICE. El contrato completo está en el archivo GUEST_SERVICES.md del paquete.
Eventos de sesión
Tu página solo sabe de una sesión mientras está abierta. Tu servicio puede saber más: declara contributes.sessionEvents junto a contributes.service, y OpenChamber envía al servicio los eventos que enumeras, desde todos los proyectos, haya o no una ventana abierta.
"service": { "entry": "service/main.js", "runtime": "host" },"sessionEvents": { "events": ["turn-finished", "question-asked"], "content": true }Los eventos son turn-started, session-created, session-archived, session-deleted, turn-finished (el agente ha terminado y no espera nada: ningún subagente en marcha, ningún permiso ni pregunta pendiente, ningún paso de objetivo), turn-failed, permission-asked y question-asked. Un turno que el usuario detuvo no envía nada. Aprobar la extensión es la suscripción; el diálogo enumera los eventos bajo la línea del servicio. Sin content, el servicio recibe solo identificadores: la sesión, el directorio de su proyecto y los elementos que el usuario le vinculó. Con "content": true, el manifiesto pide además un permiso que el usuario ve como una línea propia, Leer lo que dice el agente en segundo plano, y cada evento lleva la respuesta final del agente, los campos de la pregunta o el detalle del permiso.
Cada evento llega como un POST /session-events con el bearer del servicio. Léelo con readSessionEventRequest y responde { "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 });}Se envían los eventos de todos los proyectos; compara event.project.directory con tu propia configuración y responde { "ok": true } para el resto. La entrega es como mucho una vez y en orden, una petición a la vez; una respuesta fallida descarta tus eventos durante 30 segundos, y nada se reenvía después de que OpenChamber o tu servicio estuvieran caídos. El servicio no necesita panel para esto. El ejemplo session-events-log registra cada evento en un archivo.
Llamar a OpenChamber desde tu servicio
Un servicio puede actuar sin ninguna ventana abierta. createServiceHost es su camino de vuelta a OpenChamber:
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, sessions.status y sessions.stop necesitan contributes.sessionControl, que el usuario aprueba como una línea propia. sessions.lastReply necesita sessionControl: { content: true }. Un mensaje de sessions.send cierra un formulario o una página que el agente espera en esa sesión, igual que un mensaje escrito en el chat.
Para llevar las solicitudes del agente a otro sitio, como una app de chat, añade answers: true. El usuario lo aprueba como una línea más, y entonces el servicio puede responder en nombre del usuario en cualquier sesión: sessions.answerPermission(sessionId, requestId, { decision: 'allow' }) (o 'deny' con un message que lee el agente) para un evento permission-asked, y sessions.answerForm(sessionId, formId, values) para un evento question-asked cuyo event.question.kind es form. El contenido del evento lista los campos del formulario con su key y sus opciones. null en lugar de valores descarta el formulario. Un permiso concedido cubre solo esa solicitud.
setInstructions necesita contributes.instructions y añade contenido a lo que leen los agentes, en una carpeta o, con null, en todas partes.
emit llega a tus frames abiertos mediante host.onServiceEvent, así un panel deja de hacer sondeos. Nada se reenvía, por lo que un frame que se abre más tarde lee primero el estado actual. readSettings da al servicio la configuración de la integración, incluidos los campos secret, y needsConfiguration muestra un mensaje en la tarjeta de la extensión.
Ejecución sin que nadie lo pida
Un servicio arranca con la primera petición, y el servicio de un rol se detiene cuando está inactivo. service.keepRunning: true lo arranca junto con OpenChamber y lo vuelve a levantar cuando se detiene. service.schedule lo despierta según un horario:
"service": { "entry": "service/main.js", "runtime": "host", "keepRunning": true, "schedule": [{ "id": "nightly", "cron": "0 3 * * *", "timezone": "Europe/Kyiv" }]}Una ejecución es un POST /schedule. Léela con readServiceScheduleRequest y responde 200. Una ejecución perdida mientras OpenChamber estaba caído no se recupera. Un servicio que necesita eventos externos mantiene su propia conexión o hace sondeos.
Filas en el menú @
contributes.mentions: { label } añade una sección al menú @ del cuadro de chat. Tu servicio responde a POST /mentions/search con { items: [{ id, title, subtitle?, url? }] } en un máximo de 2 segundos, y a POST /mentions/resolve con { text } para la fila que elige el usuario. El texto va al agente con el mensaje, y el usuario ve un chip. Un servicio lento no añade filas y nunca retiene los archivos; una fila que no se puede leer no deja chip y lo indica.
Bloques en el chat
contributes.blocks añade componentes que los agentes pueden escribir en un bloque de UI. Tu propia página dibuja cada uno, en el mismo sandbox y con los mismos permisos que tu panel.
"blocks": [{ "name": "JiraIssue", "description": "A Jira ticket", "args": ["key"], "entry": "blocks/issue.html" }]El agente escribe JiraIssue("OPE-1"), y tu página lee la llamada:
host.onBlock(({ args }) => draw(args.key));Llama a host.setHeight(px) desde un ResizeObserver sobre tu elemento raíz. El frame llena su caja sin padding, así que añade el tuyo. Hasta 4 componentes por extensión, porque cada firma es texto que lee cada sesión.
Un bloque puede responder al agente, como hace un botón dentro de un bloque. Llama a host.answerBlock(text) desde un clic: el agente recibe el texto y empieza su siguiente turno. Hay una respuesta por bloque, y block.answered la conserva tras recargar, así que entonces dibuja el bloque como respondido.
host.onBlock(({ args, answered }) => draw(args.key, answered));button.onclick = () => host.answerBlock('Close this ticket');Pon "answers": true en un componente que haga esto. Se indica a los agentes que pregunten con él en lugar de con la elección integrada cuando su descripción encaje, y una página que espera una respuesta omite su botón Enviar. Debajo de un bloque así, OpenChamber ofrece el mismo campo de nota que en sus propios formularios: el agente lee la nota junto con tu respuesta, y una nota sola también es una respuesta.
Los argumentos los escribe un modelo, así que el texto de una página web o de un ticket puede dirigirlos. Trátalos como no confiables y no hagas nada con efectos secundarios hasta que el usuario haga clic.
Canales de notificación
Un servicio puede entregar las notificaciones del usuario, junto al escritorio y al teléfono: declara provides: ["notifications"] y describe el canal en 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"] }}A partir de esa descripción, OpenChamber dibuja una tarjeta de cuenta para el canal en Ajustes → Integraciones y guarda la cuenta: el usuario la conecta ahí, el token se queda en OpenChamber y tu servicio nunca lo ve. Una vez conectada una cuenta, cada notificación que OpenChamber enviaría al teléfono va también a tu servicio, según los ajustes de notificación del usuario, y solo mientras no haya ninguna ventana visible, salvo que el usuario haya elegido recibir avisos siempre. Pausar la extensión lo detiene.
OpenChamber reenvía tres llamadas a POST /notifications: auth.probe (si este servidor es uno de los que atiendes), auth.verify (a quién pertenece este token) y notify (una notificación). Lee cada una con readNotificationsProviderRequest, y llega al servidor del canal solo a través del helper de salida, que envía tu petición a OpenChamber con el handle de la llamada; OpenChamber añade el token y mantiene la petición en el servidor de la cuenta.
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 } } }}A qué sala o canal va cada proyecto lo decide tu extensión: la notificación lleva directory y sessionName. En modo empresa lleva solo el título genérico, como el teléfono. El ejemplo notification-webhook es un canal completo con un receptor para probarlo.
Gestores de incidencias
Un servicio puede ser un gestor de incidencias junto a Linear: declara provides: ["tracker"] y describe el gestor en contributes.tracker. OpenChamber lo lista entonces junto a Linear en todos los sitios donde aparecen los gestores, solo a partir del manifiesto, antes de que el servicio llegue a ejecutarse.
"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}"] }}El usuario conecta cuentas en la tarjeta del gestor bajo Cuentas de extensiones: un token, o un inicio de sesión en el navegador cuando declaras auth.oauth (OpenChamber ejecuta el inicio de sesión con el client id que introduce el usuario y guarda los tokens). Tu servicio nunca ve una credencial. urls.item son plantillas para la dirección de un elemento, con {id} para el identificador (también como ?name={id} cuando viaja en la query); OpenChamber las compila para que los enlaces pegados se conviertan en elementos vinculados. identifier dice qué aspecto tiene la clave: key-number (PROJ-42), number o short-code.
OpenChamber reenvía nueve llamadas a POST /tracker: auth.probe y auth.verify, las lecturas items.list, items.get, items.liveSummaries, states.list y containers.list, y las escrituras items.updateState y comments.create. Lee cada una con readTrackerProviderRequest, responde con las formas neutras de elemento y llega al servidor del gestor solo a través del helper de salida, que envía tu petición a OpenChamber con el handle de la llamada; OpenChamber añade la credencial y mantiene la petición en el servidor de la cuenta. El handle de una lectura solo envía GET.
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)) } }); }}Cuando el usuario activa los comentarios de sesión, OpenChamber llama a comments.create con “session started”, “completed” o “failed” y un enlace a la sesión, una vez cada uno; el comentario no lleva nada más. El ejemplo tracker-provider es un gestor completo con un servidor falso para probarlo.
Proveedores de uso
Un servicio puede informar del uso junto a los proveedores integrados: declara provides: ["quota"] y enumera en contributes.quota.providers los proveedores de OpenCode cuyas credenciales necesita. OpenChamber llama al proveedor ext-<panel.id>, dibuja su tarjeta con el nombre y el icono de la extensión y le pregunta como pregunta a un proveedor integrado.
"service": { "entry": "service/main.js", "runtime": "host", "provides": ["quota"] },"quota": { "providers": ["openrouter"] }El diálogo de aprobación enumera esos proveedores, y una versión que cambie la lista vuelve a preguntar. En cada actualización OpenChamber envía una llamada, usage.read, a POST /quota, con input.credentials con la credencial que OpenCode guarda para cada proveedor enumerado del que tenga una (una clave de API o los tokens de un inicio de sesión en el navegador) y nada más del almacén. Un proveedor enumerado para el que OpenCode no guarda credencial se muestra como no configurado y el servicio no se llama; se permite una lista vacía, para un proveedor cuya clave vive en los ajustes propios de la extensión.
Lee el cuerpo con readQuotaProviderRequest y responde { ok: true, data } con el resultado que calcula un proveedor integrado: ok, configured, error, planLabel y usage.windows por nombre de ventana, cada una con usedPercent, windowSeconds, resetAt (época en milisegundos) y un valueLabel o value opcional, más usage.models para las ventanas por modelo. OpenChamber deriva la parte restante, la cuenta atrás del reinicio y las cadenas formateadas, así que la tarjeta coincide con la de un proveedor integrado.
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()) });}El servicio responde en 20 s y se detiene tras diez minutos sin llamadas; la página de uso se actualiza cada tres minutos mientras está abierta. El ejemplo quota-provider es un proveedor completo con una API de uso falsa para probarlo.
Archivos
Una ruta relativa está dentro del proyecto abierto y necesita la capacidad files. Una ruta que empieza por / o ~/ debe coincidir con un patrón declarado en contributes.filesystem y necesita la capacidad filesystem. Consulta Crea una extensión para las reglas.
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");Límites: ruta de hasta 1024 caracteres, contenido de hasta 2,000,000 caracteres, listado de hasta 2000 entradas. Superar el límite de contenido es FILE_TOO_LARGE.
generate
const { text } = await host.generate({ prompt: notes, system: "Summarize in one sentence." });prompt es de 1 a 64.000 caracteres tras recortar espacios, system hasta 8.000, maxOutputTokens de 1 a 4.000. Nada entra en una sesión y no se guarda historial; la extensión nunca elige el modelo. La llamada espera hasta 90 segundos. No tener ningún Small Model utilizable es NO_MODEL; un error del modelo es MODEL_FAILED. Consulta Generar texto.
Códigos de error
Las llamadas fallidas lanzan HostRequestError. code es uno de estos; message da más detalles.
| Código | Cuándo |
|---|---|
HOST_UNAVAILABLE | no hay window, no estás dentro de OpenChamber, o se ejecutó dispose() |
HOST_TIMEOUT | sin respuesta durante 20 segundos |
HOST_REJECTED | OpenChamber se negó, o respondió con un código que este SDK no conoce |
DISCONNECTED | no hay token ni conexión con Linear para tu integración |
BAD_PATH | path estaba mal formado o intentó salir del origen permitido |
NO_INTEGRATION | el manifiesto no tiene integration |
NO_SESSION | prompt o sessionLink sin sesión abierta |
SESSION_BUSY | prompt({ send: true }) mientras la sesión estaba ocupada |
DISABLED | el usuario pausó la extensión en Ajustes → Extensiones |
NO_SERVICE | no hay servicio local declarado, no está aprobado o no está en ejecución |
NOT_GRANTED | el usuario no ha aprobado la capacidad que necesita esta llamada |
SERVICE_FAILED | el servicio local falló o nunca llegó a estar listo |
NO_DIRECTORY | se usó una ruta de archivo relativa sin proyecto abierto, o openCommit sin proyecto abierto |
NOT_FOUND | readFile sobre un archivo que no existe, o openCommit sobre un commit que no está en el proyecto |
FILE_TOO_LARGE | contenido del archivo por encima del límite, al leer o escribir |
DENIED | el sistema operativo rechazó la operación sobre el archivo |
NO_MODEL | generate sin ningún Small Model disponible |
MODEL_FAILED | el Small Model devolvió un error |
UNSUPPORTED | este OpenChamber no puede hacer la llamada, por ejemplo openCommit donde no hay vista Diff |
Relacionado
- Crea una extensión para la carpeta, el manifiesto, la instalación y las capacidades
- Kit de UI para botones, campos, listas y el resto de componentes