Pular para o conteúdo
↑↓Navegar ↵Abrir escFechar

Crie uma extensão

Use @openchamber/sdk para adicionar seu próprio painel ao OpenChamber. Uma extensão é uma pequena página web que o OpenChamber mostra na barra lateral direita. Ela conversa com o app por meio de connectHost(): pode ler o projeto e a sessão atuais, mostrar avisos, colocar texto na caixa de chat, anexar uma tarefa a uma sessão e, com a aprovação do usuário, iniciar sessões e enviar prompts.

As extensões funcionam no OpenChamber web e desktop. O VS Code e o mobile ainda não as carregam.

O que é uma extensão

Uma pasta com três arquivos:

  • package.json com um bloco openchamber (o manifesto)
  • panel/index.html, a página que o OpenChamber mostra
  • panel/main.js, seu script, compilado em um único arquivo clássico (um IIFE, não um módulo ES, porque a página carrega em um iframe isolado)

O OpenChamber nunca compila seu código. Distribua o arquivo .js já compilado. O SDK inclui um comando de empacotamento; qualquer outro bundler que gere um IIFE também serve.

Terminal window
npm install @openchamber/sdk
bunx openchamber-guest-bundle panel/main.ts panel/main.js

O comando de empacotamento roda com Bun. Sem Bun, use esbuild ou qualquer outro bundler com --format=iife --platform=browser.

Aponte o index.html para o main.js com um <script src="main.js"></script> comum.

Uma extensão completa e funcional, com os três arquivos, está na página Exemplo. Copie-a e troque a API e a lista.

Instale enquanto trabalha

  1. Execute o OpenChamber na web ou no desktop.
  2. Abra Configurações → Extensões.
  3. Cole o caminho absoluto da sua pasta e clique em Adicionar.

O OpenChamber lê o manifesto e mostra um diálogo com o que a extensão pede (veja Capacidades). Aprove, e seu ícone aparece na barra lateral. Clique nele e sua página carrega.

Você também pode adicionar um .zip local ou um link https para um repositório git ou um arquivo zip. Só uma instalação por git consegue se atualizar: suba version no package.json, faça push e o OpenChamber verifica logo depois de abrir e a cada poucas horas, avisa o usuário uma vez e oferece a atualização em Configurações → Extensões. Um link para openchamber://extensions/install?url=<encoded repository URL> em qualquer página abre o aplicativo de desktop com esse repositório no diálogo de adicionar, então você pode oferecer um botão de instalação. Um #tag ou #branch na URL fixa qual ele segue. Esses são copiados para a pasta de dados do OpenChamber e rodam a partir dessa cópia, então distribua os arquivos compilados, não node_modules nem fontes em TypeScript. Remover a extensão apaga a cópia. Uma instalação a partir de pasta roda direto da sua pasta, então você pode editar, recompilar e recarregar.

Manifesto

{
"name": "@acme/hello",
"version": "1.0.0",
"openchamber": {
"apiVersion": 1,
"engines": { "openchamber": ">=1.24.0" },
"id": "acme-hello",
"name": "Hello",
"icon": "window",
"contributes": {
"panel": { "entry": "panel/index.html" },
"attach": "dialog",
"page": true,
"capabilities": ["prompt", "sessions"],
"integration": {
"name": "Acme",
"description": "Tasks from Acme",
"token": {
"apiOrigin": "https://api.acme.example",
"account": { "path": "/me", "name": "login" },
"scheme": "bearer"
},
"settings": [{ "id": "list-id", "label": "List ID" }]
}
}
}
}

version é obrigatório e precisa ser semver. Configurações → Extensões mostra o valor no card.

apiVersion é 1. O OpenChamber recusa qualquer outro valor.

engines.openchamber é opcional. Informe a versão mais antiga do OpenChamber com a qual sua extensão funciona, como 1.24.0 ou >=1.24.0. Versões anteriores se recusam a instalá-la em vez de falhar depois.

id, name e icon identificam a extensão. id é em kebab-case e precisa ser único entre as extensões instaladas. icon é um nome do Remixicon (RiWindowLine vira window) ou um arquivo SVG dentro da sua pasta, como icon.svg. contributes.panel é o painel da extensão na barra lateral: o entry dele é o arquivo HTML dentro da sua pasta. Uma extensão sem painel na barra lateral deixa panel de fora. Pacotes mais antigos mantêm id, name e icon dentro de panel, e isso continua funcionando.

contributes.attach é opcional. Ele adiciona sua extensão ao menu + ao lado da caixa de chat, para o usuário escolher uma tarefa e anexá-la a uma sessão.

  • "dialog" abre sua página em uma janela
  • true ou "panel" abre o painel da barra lateral no lugar
  • { "mode": "dialog", "entry": "panel/attach.html" } abre na janela uma página separada sua, para o seletor não precisar compartilhar código com o painel da barra lateral
  • omita, e a extensão fica fora desse menu

ctx.surface é panel, dialog, page, status ou block. Clicar em um item anexado coloca seus dados em ctx.item; nos demais casos é null.

contributes.page: true oferece o painel em tela cheia no menu Páginas de extensões acima das sessões. { "entry": "panel/page.html", "title": "Board" } usa um HTML separado e um título opcional. panel.entry continua obrigatório, com o mesmo isolamento e permissões. Só o usuário abre a página. Recarregar, pausar, remover ou trocar de servidor fecha a página.

Para um quadro, use host.storage e as listas de projetos, worktrees e sessões com estados ao vivo. startSession pode usar outro projeto sem fechar o quadro. Veja Host API.

contributes.integration é opcional. Ele adiciona um card em Configurações → Integrações onde o usuário conecta uma conta. Veja Contas e rede.

contributes.service é opcional. Ele declara um serviço local (um processo que o OpenChamber inicia junto com a extensão) para coisas que uma página web não alcança, como um socket do Docker. Esse processo roda com o acesso completo do usuário e sem sandbox, por isso o diálogo de aprovação avisa sobre ele; declare um serviço desses só quando a página não conseguir fazer o trabalho. Veja o arquivo GUEST_SERVICES.md do pacote. Um serviço com provides: ["browser"] também pode substituir o navegador do agente: ele responde às ações browser.* no servidor, então os agentes navegam sem nenhum aplicativo de desktop aberto, e o usuário o escolhe em Configurações → Ferramentas do OpenChamber. Um serviço assim não precisa de painel. Um serviço também pode ser avisado do que acontece nas sessões do usuário: declare contributes.sessionEvents ao lado dele, e o OpenChamber envia ao serviço cada turno que termina ou falha e cada permissão ou pergunta que o agente pede, de todos os projetos, sem nenhuma janela aberta. Veja Eventos de sessão. E um serviço com provides: ["notifications"] entrega as notificações do usuário, além do desktop e do celular, para o Slack, o Matrix ou um servidor seu; o usuário conecta a conta em Configurações → Integrações e o token fica no OpenChamber. Veja Canais de notificação. Um serviço com provides: ["tracker"] é um rastreador de itens de trabalho ao lado do Linear, com o cartão de conta dele em Contas de extensões; veja Rastreadores. E um serviço com provides: ["quota"] informa o uso ao lado dos provedores integrados, lendo as credenciais que o OpenCode guarda para os provedores que ele lista no manifesto; veja Provedores de uso.

Capacidades

Desenhar um painel e ler a sessão atual não exige permissão. Tudo que age em nome do usuário exige. Liste essas capacidades em contributes.capabilities:

CapacidadeO que permite
promptenviar mensagens para a sessão do usuário (prompt({ send: true }), startSession com text)
sessionslistar projetos, worktrees e estados de sessões; criar e abrir sessões em projetos registrados
filesler e escrever arquivos dentro do projeto aberto (readFile, writeFile, listDir, stat com um caminho relativo)
modelgeração de texto avulsa com o Small Model do usuário (generate), fora de qualquer sessão

Outras onze são adicionadas para você automaticamente: network quando você declara uma integration, service quando declara um service, filesystem quando declara padrões filesystem, origins quando declara origins, conversation quando uma ação de sessão pede as mensagens (veja Ações, comandos e o selo), transcript quando sessionEvents ou sessionControl pede content (veja Eventos de sessão), sessionControl quando seu serviço atua sobre sessões (veja Chamar o OpenChamber), sessionAnswers quando ele responde em nome do usuário aos pedidos de permissão e formulários do agente (sessionControl: { answers: true }), instructions quando ele complementa o que os agentes leem, e skills quando você declara contributes.skills, uma pasta do pacote com skills que chegam a todos os agentes enquanto a extensão está ativa, e opencodePlugin quando você declara contributes.opencodePlugin (veja abaixo).

O usuário vê a lista completa uma vez, ao instalar a extensão, e aprova ou remove a extensão. Se uma versão nova pedir mais, o diálogo aparece de novo. Uma chamada que precisa de uma capacidade que o usuário não aprovou falha com NOT_GRANTED.

Um plugin do OpenCode no seu pacote

Uma extensão pode trazer um plugin do OpenCode, para que as pessoas instalem uma coisa só em vez de duas. Declare "opencodePlugin": "plugin", uma pasta dentro do pacote com index.js ou server.js na raiz. O OpenCode não lê main, então para uma entrada em outro lugar defina name e exports no package.json da pasta. A entrada exporta por padrão { id, setup }, a forma que o OpenCode carrega; ela não importa nada, porque o OpenCode a carrega com um import() simples a partir da sua pasta, onde nada está instalado. O input de uma ferramenta é JSON Schema puro, e o execute dela responde { content }.

{
"contributes": {
"panel": { "id": "teapot-tools", "name": "Teapot Tools", "icon": "cup" },
"opencodePlugin": "plugin"
}
}

O diálogo de aprovação ganha mais uma linha, Adicionar um plugin ao OpenCode, que diz claramente que o código do plugin roda dentro do OpenCode com todo o acesso do usuário. Depois de permitido, o OpenChamber lista a sua pasta na configuração do OpenCode que ele gerencia, o mesmo arquivo que carrega contributes.skills; o OpenCode que ele roda observa esse arquivo e carrega o plugin em alguns segundos. Desativar ou remover a extensão tira o plugin do mesmo jeito. Nada é escrito no opencode.json do usuário: esse arquivo é dele, e um plugin ali sobreviveria à sua extensão e rodaria em todo OpenCode da máquina.

Onde esse arquivo não chega ao OpenCode, a tela da extensão diz isso e oferece Adicionar à configuração global, que escreve a sua pasta na configuração global do OpenCode do usuário como uma entrada que passa a ser dele. Isso acontece quando o OpenChamber não iniciou o OpenCode com o qual conversa, ou quando o ambiente do servidor define o próprio OPENCODE_CONFIG (o plugin então carrega na próxima inicialização do OpenCode). Um OpenCode em outra máquina não consegue carregar uma pasta desta; a tela diz isso e não oferece nada. Um plugin que não carrega é informado pelo OpenCode em Configurações → Plugins quando tem uma entrada lá; a sua extensão continua funcionando de qualquer forma. A instalação recusa um pacote cuja pasta do plugin não existe ou cujo package.json não indica um arquivo de entrada.

O VS Code e os apps móveis não carregam extensões, então nenhum plugin chega a eles. Uma sessão dentro de um espaço isolado roda o próprio OpenCode, que nunca lê a configuração do host. O modo empresarial trata o plugin como um serviço local: ele só instala a partir de um repositório listado pelo administrador. O SDK tem um exemplo funcional, examples/opencode-plugin.

Gerar texto

Com a capacidade model, host.generate pede ao Small Model do usuário uma resposta avulsa: um resumo, um título, um rascunho. Nada entra em uma sessão, nenhum histórico é mantido, e a extensão nunca escolhe um provedor. O OpenChamber usa o mesmo modelo que usa para seu próprio trabalho em segundo plano, escolhido em Configurações → Sessões → Small Model, ou o modelo pequeno do provedor com que o usuário está trabalhando, ou o modelo padrão da configuração do OpenCode dele.

const { text } = await host.generate({
prompt: task.description,
system: "Write a one-line summary. Return only the summary.",
maxOutputTokens: 200,
});

Quando nenhum modelo está disponível, a chamada falha com NO_MODEL; um modelo que deu erro é MODEL_FAILED. Os prompts são limitados a 64.000 caracteres, e a chamada espera até 90 segundos. O usuário vê essa permissão como “Usar seu Small Model” e isso custa tokens a ele, então mantenha os prompts curtos e chame isso em um clique, não a cada tecla digitada.

Contas e rede

Sua página roda em um ambiente isolado e não consegue acessar a rede por conta própria: scripts, estilos, imagens, fontes e mídia carregam só do seu pacote, e fetch alcança só os arquivos do seu pacote. Inclua fontes e imagens dentro do pacote em vez de carregá-las de um CDN.

Para falar com um serviço externo, declare uma integration e o OpenChamber faz as chamadas por você através de host.request, com o token do usuário anexado. O token nunca chega à sua página.

Para qualquer outra coisa de que sua página precise diretamente, como fontes ou imagens de um CDN ou uma API pública, liste até 8 origens https:

"origins": ["https://fonts.example.com"]

O usuário as vê no diálogo de aprovação como endereços para os quais sua extensão pode enviar dados e dos quais pode recebê-los, e as aprova de novo se uma atualização adicionar uma. Origens aprovadas ficam abertas para fetch, imagens, fontes, estilos e mídia, mas nunca para scripts: o código precisa vir no seu pacote. A origem da página é null, então para ler uma resposta de fetch o servidor precisa permitir CORS para essa origem.

No modo empresarial, uma extensão com origins, uma integration ou um service só é instalada a partir de um repositório Git que o administrador listou. Se a sua extensão funciona sem eles, ela continua instalável para essas equipes. Para desenvolver uma numa máquina assim, peça ao administrador allowLocalExtensions, que deixa você instalá-la a partir da pasta dela.

  • token: o usuário cola um token de API no card de Configurações → Integrações. apiOrigin é a única origem que request pode chamar. account é opcional: um caminho GET e um nome de campo, para o card mostrar quem está conectado. scheme diz como o token é enviado; veja a tabela abaixo. Confira na documentação da API do provedor qual header ela espera.
  • oauth: o usuário cola um client id, e o OpenChamber executa o fluxo de autorização. Precisa de authorizeUrl, tokenUrl e apiOrigin.
  • host: { "provider": "linear" }: reaproveita a conta do Linear já conectada no OpenChamber. Não precisa de client id.

O que cada scheme envia:

schemeHeader que o OpenChamber enviaQuando usar
omitidoAuthorization: <token>a API documenta um token sem prefixo no header
"bearer"Authorization: Bearer <token>a API documenta um token bearer ou um personal access token
"basic"Authorization: Basic base64(username:token)a API documenta HTTP Basic auth com um usuário (geralmente um email) e um token de API; o card pede os dois, e usernameLabel dá nome ao primeiro campo

settings adiciona campos de texto simples ao card. Os valores chegam em ctx.settings. Um campo com "secret": true fica mascarado no card e nunca chega a uma página; seu serviço o lê com readSettings().

Arquivos

Sua página não consegue tocar o disco por conta própria. O OpenChamber lê e escreve por ela, dentro dos limites que o usuário aprovou.

  • Um caminho relativo (README.md, src/index.ts, .) significa o projeto aberto. Precisa da capacidade files. Sem projeto aberto é NO_DIRECTORY.
  • Um caminho que começa com / ou ~/ significa qualquer outro lugar. Ele precisa bater com um dos padrões que você declara em contributes.filesystem, e o usuário vê exatamente esses padrões no diálogo de aprovação:
"filesystem": ["~/.config/opencode/opencode.json", "~/notes/**"]

Os padrões usam * para um segmento do caminho e ** para qualquer profundidade. Tudo que ficar fora deles é BAD_PATH, mesmo depois da aprovação. .. nunca é permitido.

const config = await host.readFile("~/.config/opencode/opencode.json");
await host.writeFile("~/.config/opencode/opencode.json", nextJson);
const { entries } = await host.listDir(".");

As escritas são atômicas: o OpenChamber escreve um arquivo temporário e o renomeia, então quem lê nunca vê um arquivo pela metade. Os arquivos são lidos e escritos como texto UTF-8, até 2 MB.

Uma seção no Work Status

contributes.statusSection adiciona uma seção sua ao painel Work Status ao lado do chat. Os usuários ligam, desligam e movem a seção na lista de seções desse painel, como as seções nativas.

"statusSection": { "entry": "status/index.html", "title": "Recent commits", "height": 160 }
  • true reutiliza panel.entry.
  • entry é uma página HTML separada na sua pasta. Com ela você não precisa de panel.entry, então a extensão pode ser só essa seção, sem ícone na barra lateral.
  • title substitui o nome do painel no cabeçalho da seção, até 60 caracteres.
  • height é a altura inicial em pixels, de 24 a 320 (120 por padrão). Chame host.setHeight() quando o tamanho do conteúdo mudar.

Sua página vê ctx.surface como status. O OpenChamber só a carrega enquanto o painel Work Status está visível e sua seção está expandida, e a descarrega quando o usuário recolhe a seção, então não guarde ali nada que você não consiga reconstruir. Uma extensão só com a seção pode usar permissões, um serviço, uma integração e arquivos, mas não page, attach, actions nem commands. Só web e desktop. O exemplo git-graph-status é uma seção completa.

Um editor para o seu tipo de arquivo

contributes.fileEditors abre arquivos na sua página em vez do editor de texto. Quando o usuário abre um arquivo correspondente no Files, o OpenChamber mostra seu editor ali, com o mesmo alternador de código-fonte, botão de tela cheia e botão de salvar que os visualizadores nativos têm.

"fileEditors": [
{ "id": "canvas", "title": "Excalidraw", "match": ["*.excalidraw", "*.excalidraw.md"], "entry": "editor/index.html" }
]
  • match compara só nomes de arquivo, sem diferenciar maiúsculas de minúsculas. * corresponde a qualquer sequência de caracteres e ? a um só. Um padrão não pode conter / nem ser formado só por curingas.
  • entry é uma página HTML na sua pasta. Você não precisa de panel.entry, então a extensão pode ser só o editor, sem ícone na barra lateral.
  • content é "text" (o padrão) ou "binary", veja abaixo.
  • Até 8 editores por extensão, com até 16 padrões cada.

O OpenChamber cuida do arquivo. Sua página nunca o lê nem o escreve e não precisa de nenhuma permissão para isso. O OpenChamber entrega o texto a você, pede o texto editado na hora de salvar e cuida de Cmd/Ctrl+S, do salvamento automático, do aviso de alterações não salvas, das quebras de linha e das mudanças feitas no disco:

import { connectHost, createFileSaveTracker } from "@openchamber/sdk";
const host = connectHost();
let tracker = createFileSaveTracker(null);
host.onFileOpen((file) => {
if (file.encoding !== "text") return; // um editor de texto sempre recebe texto
load(file.content); // também file.path, file.name, file.readOnly
tracker = createFileSaveTracker(versionOf(currentText()));
});
// Chame isto depois de cada edição. `edited` segura o salvamento automático até o usuário parar.
const changed = () => host.reportFileChange(tracker.observe(versionOf(currentText())));
host.onFileSnapshot(() => ({ content: currentText(), version: versionOf(currentText()) }));
// Edições feitas enquanto o OpenChamber gravava mantêm o arquivo não salvo.
host.onFileSaved((version) => host.reportFileChange({ dirty: tracker.markSaved(version), edited: false }));

version é qualquer string curta que muda quando o documento muda, como um hash. Se um arquivo não é algo que seu editor consegue abrir, chame host.reportFileUnsupported() e o OpenChamber mostra o código-fonte com um aviso. Arquivos acima de 20 MB ficam na visualização do código-fonte.

Arquivos binários

Defina "content": "binary" para abrir arquivos que não são texto, como planilhas, documentos ou imagens. Seu editor recebe os bytes do arquivo e devolve bytes; todo o resto funciona do mesmo jeito.

host.onFileOpen((file) => {
if (file.encoding !== "binary") return;
open(file.bytes); // um Uint8Array, exatamente como está no disco
});
host.onFileSnapshot(() => ({ bytes: currentBytes(), version: versionOf(currentBytes()) }));

Um editor binário pode assumir qualquer arquivo que corresponda aos seus padrões, inclusive os que o próprio OpenChamber mostra em prévia, como imagens e PDFs. Ele não tem visualização do código-fonte, e quando o usuário o leva para a tela cheia ou o tira dela com alterações não salvas, o OpenChamber as salva primeiro. Ler e gravar o formato é trabalho do seu editor: o OpenChamber grava exatamente os bytes que você devolve.

Sua página vê ctx.surface como file. O OpenChamber a recarrega, entregando o arquivo de novo, quando o arquivo muda no disco, o usuário descarta as alterações ou o editor entra ou sai da tela cheia. Vence a primeira extensão ativada cujo padrão corresponde, antes das prévias do próprio OpenChamber. Uma extensão só com o editor pode usar permissões, um serviço, uma integração e arquivos, mas não page, attach, actions nem commands. Só web e desktop. O exemplo checklist-editor é um editor completo.

Ações, comandos e o selo

Além da barra lateral e do menu +, uma extensão pode aparecer em mais três lugares. Todos entregam à extensão um item do mesmo jeito que um clique no chip.

Ações em mensagens e sessões. Declare entradas de menu e o OpenChamber as mostra ao lado das embutidas:

"actions": [
{ "id": "create-task", "label": "Create task", "icon": "add-line", "where": "message", "roles": ["assistant"] },
{ "id": "summarize", "label": "Summarize session", "where": "session", "payload": ["messages"] }
]

where também aceita selection (texto selecionado em uma mensagem), file (uma linha no Files) e diff (linhas selecionadas de um arquivo ou de um diff). Os itens deles têm um kind com o mesmo nome: selection traz o texto, file o caminho, diff o caminho e o intervalo de linhas. Em uma seleção, display define como a ação aparece: label (o padrão), icon com o rótulo como dica, ou both.

Uma ação de mensagem abre sua extensão com um ctx.item de kind: "message": o id e o título da sessão, a pasta do projeto, o id da mensagem, o papel dela e o texto. Uma ação de sessão abre com kind: "session" e o id, o título e a pasta da sessão. Adicione "payload": ["messages"] e o item também carrega a conversa inteira, da mais antiga para a mais nova, o mesmo texto que a exportação em Markdown produz. Isso precisa da capacidade conversation, que o usuário vê como uma linha separada no diálogo de aprovação. item.action diz qual entrada foi clicada.

Comandos de barra que anexam. Declare um comando e trate-o na página:

"commands": [{ "name": "task", "description": "Attach a task by id" }]
host.onResolve(async ({ command, args }) => {
const task = await findTask(args.trim());
return task ? { providerId: "acme-tasks", id: task.id, title: task.title, url: task.url } : null;
});

Quando o usuário digita /task ABC-12 na caixa de chat, o OpenChamber pede à sua extensão que o resolva e anexa o que você devolve como um chip, sem abrir nada. Devolva null para “nada encontrado”. Se o seu painel estiver fechado, o OpenChamber o carrega em segundo plano para essa chamada. Um nome que o OpenChamber ou o OpenCode já usa é ignorado.

Selo no ícone da barra lateral. host.setBadge(3) coloca um ponto no seu ícone e mostra o número na dica dele, por exemplo tarefas abertas; host.setBadge(null) o limpa. Abrir o painel também o limpa.

Suas ferramentas no chat

Quando o seu plugin do OpenCode ou servidor MCP adiciona uma ferramenta, o chat mostra as chamadas dela com um ícone genérico e a saída bruta. Em vez disso, declare como elas devem aparecer, sem código:

"tools": [
{
"match": "mcp.jira.*",
"name": "Jira",
"icon": "task-line",
"title": "{input.key}",
"subtitle": "{output.status}",
"output": "table",
"columns": ["key", "summary", "status"]
}
]

match é o nome da ferramenta como o OpenCode o reporta; um * no final casa com um prefixo. icon é um nome do Remixicon ou um arquivo .svg no seu pacote, como panel.icon. title e subtitle são templates sobre input, output e metadata da chamada. output escolhe como o corpo é renderizado: text, json (uma árvore), markdown, code com um language, table com columns (as linhas vêm do array de saída ou de output.items) ou auto para o padrão. Um match exato vence um curinga, e em caso de empate vence a primeira extensão instalada. Nenhuma permissão é necessária: isso só muda como dados que já estão no chat são desenhados.

Uma extensão que só estiliza ferramentas não precisa de página nenhuma: deixe panel.entry de fora e ela não terá ícone na barra lateral, só um card em Configurações → Extensões. Sem página ela pode declarar tools e nada mais. A outra exceção é uma seção do Work Status ou um editor de arquivos com entry próprio, veja acima.

Primeiras linhas no painel

import { connectHost } from "@openchamber/sdk";
const host = connectHost();
host.onReady((ctx) => {
document.body.dataset.theme = ctx.theme.mode;
document.body.dataset.surface = ctx.surface;
});

connectHost só funciona dentro do OpenChamber. Aberta como um arquivo avulso, toda chamada é rejeitada com HOST_UNAVAILABLE.

Monte o painel primeiro com os componentes de @openchamber/sdk/ui: botões, campos, dropdowns, abas, listas e mais, com as cores e fontes do app, para o painel parecer parte do OpenChamber. Escreva seu próprio HTML e CSS só para o que o kit não tem. Veja Kit de UI.

Relacionado

  • Extensões para instalar por SSH e escolher a identidade Git do servidor
  • API do host para cada método do connectHost, seus limites e códigos de erro
  • Kit de UI para os componentes
  • Exemplo para uma extensão completa de três arquivos para copiar