Aller au contenu
Naviguer Ouvrir escFermer

Créer une extension

Utilisez @openchamber/sdk pour ajouter votre propre panneau à OpenChamber. Une extension est une petite page web qu’OpenChamber affiche dans la barre latérale droite. Elle communique avec l’application via connectHost() : elle peut lire le projet et la session en cours, afficher des toasts, insérer du texte dans la zone de chat, attacher une tâche à une session et, avec l’accord de l’utilisateur, démarrer des sessions et envoyer des prompts.

Les extensions fonctionnent dans OpenChamber web et desktop. VS Code et mobile ne les chargent pas encore.

Ce qu’est une extension

Un dossier avec trois fichiers :

  • package.json avec un bloc openchamber (le manifeste)
  • panel/index.html, la page qu’OpenChamber affiche
  • panel/main.js, votre script, compilé en un seul fichier classique (un IIFE, pas un module ES, car la page se charge dans une iframe sandboxée)

OpenChamber ne compile jamais votre code. Livrez le fichier .js déjà construit. Le SDK inclut une commande de bundling ; n’importe quel bundler qui produit un IIFE convient aussi.

Fenêtre de terminal
npm install @openchamber/sdk
bunx openchamber-guest-bundle panel/main.ts panel/main.js

La commande de bundling tourne sur Bun. Sans Bun, utilisez esbuild ou tout autre bundler avec --format=iife --platform=browser.

Une extension complète et fonctionnelle, avec ses trois fichiers, se trouve sur la page Exemple. Copiez-la et changez l’API et la liste.

Faites pointer index.html vers main.js avec un <script src="main.js"></script> classique.

L’installer pendant le développement

  1. Lancez OpenChamber sur web ou desktop.
  2. Ouvrez Paramètres → Extensions.
  3. Collez le chemin absolu de votre dossier et cliquez sur Ajouter.

OpenChamber lit le manifeste et affiche une boîte de dialogue qui liste ce que l’extension demande (voir Capacités). Approuvez, et votre icône apparaît dans la barre latérale. Cliquez dessus et votre page se charge.

Vous pouvez aussi ajouter un .zip local ou un lien https vers un dépôt git ou un fichier zip. Seule une installation git peut se mettre à jour : montez version dans package.json, poussez, et OpenChamber propose la mise à jour la prochaine fois que l’utilisateur ouvre Paramètres → Extensions. Un #tag ou #branch dans l’URL fixe ce qu’elle suit. Ceux-ci sont copiés dans le dossier de données d’OpenChamber et exécutés depuis cette copie, donc livrez les fichiers construits, pas node_modules ni les sources TypeScript. Supprimer l’extension supprime la copie. Une installation depuis un dossier s’exécute directement depuis votre dossier, vous pouvez donc modifier, reconstruire et recharger.

Manifeste

{
"name": "@acme/hello",
"version": "1.0.0",
"openchamber": {
"apiVersion": 1,
"engines": { "openchamber": ">=1.24.0" },
"contributes": {
"panel": {
"id": "acme-hello",
"name": "Hello",
"icon": "window",
"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 est obligatoire et doit être un semver. Paramètres → Extensions l’affiche sur la carte.

apiVersion vaut 1. OpenChamber refuse toute autre valeur.

engines.openchamber est optionnel. Indiquez la version d’OpenChamber la plus ancienne avec laquelle votre extension fonctionne, sous la forme 1.24.0 ou >=1.24.0. Les versions plus anciennes refusent de l’installer au lieu d’échouer plus tard.

contributes.panel décrit l’entrée dans la barre latérale. id est en kebab-case et doit être unique parmi les extensions installées. icon est un nom Remixicon (RiWindowLine devient window) ou un fichier SVG dans votre dossier, comme icon.svg. entry est le fichier HTML dans votre dossier ; omettez-le pour une extension qui ne déclare que tools (voir Vos outils dans le chat).

contributes.attach est optionnel. Il ajoute votre extension au menu + à côté de la zone de chat, pour que l’utilisateur puisse choisir une tâche et l’attacher à une session.

  • "dialog" ouvre votre page dans une fenêtre
  • true ou "panel" ouvre plutôt le panneau de la barre latérale
  • { "mode": "dialog", "entry": "panel/attach.html" } ouvre dans la fenêtre une page séparée à vous, pour que le sélecteur n’ait pas à partager de code avec le panneau de la barre latérale
  • sans cette clé, l’extension n’apparaît pas dans ce menu

ctx.surface vaut panel, dialog ou page. Un clic sur un élément attaché place ses données dans ctx.item, sinon cette valeur est null.

contributes.page: true propose votre panneau en plein écran dans le menu Pages des extensions au-dessus des sessions. { "entry": "panel/page.html", "title": "Board" } utilise un HTML distinct et un titre facultatif. panel.entry reste obligatoire, avec le même bac à sable et les mêmes permissions. Seul l’utilisateur ouvre la page. Recharger, mettre en pause, supprimer ou changer de serveur la ferme.

Pour un tableau, utilisez host.storage et les listes de projets, worktrees et sessions avec leurs états en direct. startSession peut viser un autre projet sans fermer le tableau. Voir Host API.

contributes.integration est optionnel. Il ajoute une carte dans Paramètres → Intégrations où l’utilisateur connecte un compte. Voir Comptes et réseau.

contributes.service est optionnel. Il déclare un service local (un processus qu’OpenChamber démarre à côté de l’extension) pour ce qu’une page web ne peut pas atteindre, comme un socket Docker. Ce processus s’exécute avec l’accès complet de l’utilisateur et sans sandbox, c’est pourquoi la boîte de dialogue d’approbation en avertit ; déclarez-en un seulement quand la page ne peut pas faire le travail. Voir le fichier GUEST_SERVICES.md du package. Un service avec provides: ["browser"] peut aussi remplacer le navigateur de l’agent : il répond aux actions browser.* sur le serveur, les agents naviguent donc sans application de bureau ouverte, et l’utilisateur le choisit dans Réglages → Outils OpenChamber. Un tel service n’a pas besoin de panneau.

Capacités

Dessiner un panneau et lire la session en cours ne demandent aucune permission. Tout ce qui agit au nom de l’utilisateur en demande une. Listez ces actions dans contributes.capabilities :

CapacitéCe qu’elle autorise
promptenvoyer des messages dans la session de l’utilisateur (prompt({ send: true }), startSession avec text)
sessionslister les projets, worktrees et états des sessions ; créer et ouvrir des sessions dans les projets enregistrés
fileslire et écrire des fichiers dans le projet ouvert (readFile, writeFile, listDir, stat avec un chemin relatif)
modelgénération de texte ponctuelle avec le Small Model de l’utilisateur (generate), en dehors de toute session

Quatre autres sont ajoutées pour vous : network quand vous déclarez une integration, service quand vous déclarez un service, filesystem quand vous déclarez des motifs filesystem, et conversation quand une action de session demande les messages (voir Actions, commandes et le badge).

L’utilisateur voit la liste complète une seule fois, à l’installation de l’extension, et l’approuve ou supprime l’extension. Si une nouvelle version demande davantage, la boîte de dialogue réapparaît. Un appel qui a besoin d’une capacité non approuvée par l’utilisateur échoue avec NOT_GRANTED.

Générer du texte

Avec la capacité model, host.generate demande au Small Model de l’utilisateur une réponse ponctuelle : un résumé, un titre, un brouillon. Rien n’entre dans une session, aucun historique n’est conservé, et l’extension ne choisit jamais de fournisseur. OpenChamber utilise le même modèle que pour son propre travail en arrière-plan, choisi dans Paramètres → Sessions → Small Model, ou sélectionné automatiquement parmi les fournisseurs connectés de l’utilisateur.

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

Quand aucun modèle n’est disponible, l’appel échoue avec NO_MODEL ; un modèle qui a échoué donne MODEL_FAILED. Les prompts sont limités à 64 000 caractères, et l’appel attend jusqu’à 90 secondes. L’utilisateur voit cette permission sous le nom « Utiliser ton Small Model » et cela lui coûte des tokens, donc gardez les prompts courts et déclenchez l’appel sur un clic, pas à chaque frappe.

Comptes et réseau

Votre page tourne dans un sandbox et ne peut pas appeler internet directement. Déclarez une integration et OpenChamber fait les appels pour vous via host.request, avec le token de l’utilisateur attaché. Le token n’atteint jamais votre page.

  • token : l’utilisateur colle un token d’API sur la carte Paramètres → Intégrations. apiOrigin est la seule origine que request peut appeler. account est optionnel : un chemin GET et un nom de champ, pour que la carte affiche qui est connecté. scheme indique comment le token est envoyé ; voir le tableau ci-dessous. Vérifiez dans la documentation de l’API du fournisseur quel en-tête elle attend.
  • oauth : l’utilisateur colle un client id, et OpenChamber exécute le flux d’autorisation. Nécessite authorizeUrl, tokenUrl et apiOrigin.
  • host: { "provider": "linear" } : réutilise le compte Linear déjà connecté dans OpenChamber. Pas besoin de client id.

Ce que chaque scheme envoie :

schemeEn-tête envoyé par OpenChamberQuand l’utiliser
omisAuthorization: <token>l’API documente un token sans préfixe dans l’en-tête
"bearer"Authorization: Bearer <token>l’API documente un token bearer ou un personal access token
"basic"Authorization: Basic base64(username:token)l’API documente HTTP Basic auth avec un nom d’utilisateur (souvent un e-mail) et un token d’API ; la carte demande les deux, et usernameLabel nomme le premier champ

settings ajoute des champs texte simples à la carte. Leurs valeurs arrivent dans ctx.settings.

Fichiers

Votre page ne peut pas toucher le disque elle-même. OpenChamber lit et écrit pour elle, dans les limites approuvées par l’utilisateur.

  • Un chemin relatif (README.md, src/index.ts, .) désigne le projet ouvert. Nécessite la capacité files. Sans projet ouvert, c’est NO_DIRECTORY.
  • Un chemin qui commence par / ou ~/ désigne n’importe quel autre endroit. Il doit correspondre à l’un des motifs que vous déclarez dans contributes.filesystem, et l’utilisateur voit exactement ces motifs dans la boîte de dialogue d’approbation :
"filesystem": ["~/.config/opencode/opencode.json", "~/notes/**"]

Les motifs utilisent * pour un segment de chemin et ** pour n’importe quelle profondeur. Tout ce qui est en dehors donne BAD_PATH, même après approbation. .. n’est jamais autorisé.

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

Les écritures sont atomiques : OpenChamber écrit un fichier temporaire puis le renomme, donc un lecteur ne voit jamais un fichier à moitié écrit. Les fichiers sont lus et écrits en texte UTF-8, jusqu’à 2 Mo.

Actions, commandes et le badge

Au-delà de la barre latérale et du menu +, une extension peut apparaître à trois autres endroits. Tous transmettent à l’extension un item, de la même façon qu’un clic sur une puce.

Actions sur les messages et les sessions. Déclarez des entrées de menu et OpenChamber les affiche à côté des entrées intégrées :

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

Une action de message ouvre votre extension avec un ctx.item de kind: "message" : l’id et le titre de la session, le dossier du projet, l’id du message, son rôle et son texte. Une action de session l’ouvre avec kind: "session" et l’id, le titre et le dossier de la session. Ajoutez "payload": ["messages"] et l’élément porte aussi toute la conversation, du plus ancien au plus récent, le même texte que produit l’export Markdown. Cela demande la capacité conversation, que l’utilisateur voit comme une ligne à part dans la boîte de dialogue d’approbation. item.action vous dit quelle entrée a été cliquée.

Commandes slash qui attachent. Déclarez une commande et traitez-la dans la page :

"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;
});

Quand l’utilisateur tape /task ABC-12 dans la zone de chat, OpenChamber demande à votre extension de le résoudre et attache ce que vous renvoyez sous forme de puce, sans rien ouvrir. Renvoyez null pour “rien trouvé”. Si votre panneau est fermé, OpenChamber le charge en arrière-plan pour cet appel. Un nom déjà utilisé par OpenChamber ou OpenCode est ignoré.

Badge sur l’icône de la barre latérale. host.setBadge(3) affiche un nombre sur votre icône, par exemple les tâches ouvertes ; host.setBadge(null) l’efface. Ouvrir le panneau l’efface aussi.

Vos outils dans le chat

Quand votre plugin OpenCode ou serveur MCP ajoute un outil, le chat affiche ses appels avec une icône générique et une sortie brute. Déclarez plutôt à quoi ils doivent ressembler, sans code :

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

match est le nom de l’outil tel qu’OpenCode le rapporte ; un * final correspond à un préfixe. icon est un nom Remixicon ou un fichier .svg dans votre paquet, comme panel.icon. title et subtitle sont des gabarits sur input, output et metadata de l’appel. output choisit comment le corps est rendu : text, json (un arbre), markdown, code avec un language, table avec columns (les lignes viennent du tableau de sortie ou de output.items) ou auto pour le rendu par défaut. Un match exact l’emporte sur un joker, et en cas d’égalité la première extension installée gagne. Aucune permission n’est nécessaire : cela ne change que la façon dont des données déjà dans le chat sont dessinées.

Une extension qui ne fait que styliser des outils n’a besoin d’aucune page : omettez panel.entry et elle n’aura pas d’icône dans la barre latérale, juste une carte dans Paramètres → Extensions. Sans page, elle peut déclarer tools et rien d’autre.

Premières lignes dans le panneau

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 ne fonctionne qu’à l’intérieur d’OpenChamber. Ouverte comme un simple fichier, chaque appel est rejeté avec HOST_UNAVAILABLE.

Construisez d’abord le panneau avec les composants de @openchamber/sdk/ui : boutons, champs, listes déroulantes, onglets, listes et plus, stylés avec les couleurs et polices de l’application, pour que le panneau se sente partie intégrante d’OpenChamber. N’écrivez votre propre HTML et CSS que pour ce que le kit ne couvre pas. Voir Kit UI.

Voir aussi

  • Extensions pour installer par SSH et choisir l’identité Git du serveur
  • API hôte pour chaque méthode de connectHost, ses limites et les codes d’erreur
  • Kit UI pour les composants
  • Exemple pour une extension complète en trois fichiers à copier