Eine Erweiterung bauen
Mit @openchamber/sdk fügst du OpenChamber ein eigenes Panel hinzu. Eine Erweiterung ist eine kleine Webseite, die OpenChamber in der rechten Leiste anzeigt. Mit der App spricht sie über connectHost(): Sie kann das aktuelle Projekt und die Session lesen, Toasts anzeigen, Text ins Chatfeld setzen, eine Aufgabe an eine Session anhängen und, mit Zustimmung des Nutzers, Sessions starten und Prompts senden.
Erweiterungen laufen in OpenChamber im Web und auf dem Desktop. VS Code und die Mobile-App laden sie noch nicht.
Was eine Erweiterung ist
Ein Ordner mit drei Dateien:
package.jsonmit einemopenchamber-Block (dem Manifest)panel/index.html, die Seite, die OpenChamber anzeigtpanel/main.js, dein Skript, gebündelt in eine einzelne klassische Datei (ein IIFE, kein ES-Modul, weil die Seite in einem abgeschotteten iframe lädt)
OpenChamber kompiliert deinen Code nie. Liefere die gebaute .js-Datei aus. Das SDK bringt einen Bundler-Befehl mit; jeder andere Bundler, der ein IIFE ausgibt, funktioniert genauso.
npm install @openchamber/sdkbunx openchamber-guest-bundle panel/main.ts panel/main.jsDer Bundler-Befehl läuft auf Bun. Ohne Bun nimmst du esbuild oder einen anderen Bundler mit --format=iife --platform=browser.
Eine komplette, funktionierende Erweiterung, alle drei Dateien, steht auf der Seite Beispiel. Kopiere sie und tausche die API und die Liste aus.
Binde main.js in index.html mit einem normalen <script src="main.js"></script> ein.
Während der Arbeit installieren
- Starte OpenChamber im Web oder auf dem Desktop.
- Öffne Einstellungen → Erweiterungen.
- Füge den absoluten Pfad deines Ordners ein und klicke auf Hinzufügen.
OpenChamber liest das Manifest und zeigt einen Dialog mit allem, was die Erweiterung anfordert (siehe Berechtigungen). Bestätige ihn, und dein Icon erscheint in der Leiste. Klick darauf, und deine Seite lädt.
Du kannst auch eine lokale .zip-Datei oder einen https-Link zu einem Git-Repository oder einer Zip-Datei hinzufügen. Nur eine Git-Installation kann sich aktualisieren: erhöhe version in package.json und pushe, dann prüft OpenChamber kurz nach dem Start und alle paar Stunden, sagt dem Nutzer einmal Bescheid und bietet das Update in Einstellungen → Erweiterungen an. Ein Link auf openchamber://extensions/install?url=<encoded repository URL> auf einer beliebigen Seite öffnet die Desktop-App mit diesem Repository im Hinzufügen-Dialog, du kannst also einen Installations-Button anbieten. Ein #tag oder #branch in der URL legt fest, welchem es folgt. Die werden in den Datenordner von OpenChamber kopiert und aus dieser Kopie ausgeführt. Liefere also die gebauten Dateien aus, nicht node_modules oder TypeScript-Quellen. Beim Entfernen der Erweiterung wird die Kopie gelöscht. Eine Installation aus einem Ordner läuft direkt aus deinem Ordner, du kannst also bearbeiten, neu bauen und neu laden.
Manifest
{ "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 ist Pflicht und muss Semver sein. Einstellungen → Erweiterungen zeigt sie auf der Karte an.
apiVersion ist 1. Jeden anderen Wert lehnt OpenChamber ab.
engines.openchamber ist optional. Trag die älteste OpenChamber-Version ein, mit der deine Erweiterung funktioniert, als 1.24.0 oder >=1.24.0. Ältere Builds verweigern dann die Installation, statt später zu scheitern.
id, name und icon benennen die Erweiterung. id ist kebab-case und muss unter den installierten Erweiterungen eindeutig sein. icon ist ein Remixicon-Name (aus RiWindowLine wird window) oder eine SVG-Datei in deinem Ordner, etwa icon.svg. contributes.panel ist das Panel der Erweiterung in der Leiste: Sein entry ist die HTML-Datei in deinem Ordner. Eine Erweiterung ohne Panel in der Leiste lässt panel weg. Ältere Pakete behalten id, name und icon in panel, und das funktioniert weiterhin.
contributes.attach ist optional. Es nimmt deine Erweiterung ins +-Menü neben dem Chatfeld auf, damit der Nutzer eine Aufgabe auswählen und an eine Session anhängen kann.
"dialog"öffnet deine Seite in einem Fenstertrueoder"panel"öffnet stattdessen das Panel in der Leiste{ "mode": "dialog", "entry": "panel/attach.html" }öffnet eine separate Seite von dir im Fenster, damit die Auswahl keinen Code mit dem Panel in der Leiste teilen muss- lässt du es weg, bleibt die Erweiterung aus diesem Menü draußen
ctx.surface ist panel, dialog, page, status oder block. Beim Klick auf einen angehängten Eintrag enthält ctx.item dessen Daten, sonst null.
contributes.page: true bietet die Panel-Seite im Menü Erweiterungsseiten über der Sitzungsliste an. { "entry": "panel/page.html", "title": "Board" } verwendet eigene HTML-Dateien und einen optionalen Titel. panel.entry bleibt erforderlich; Sandbox und Berechtigungen bleiben gleich. Nur der Nutzer öffnet die Seite. Neuladen, Pause, Entfernen oder Serverwechsel schließen sie.
Für Boards gibt es host.storage sowie Projekt-, Worktree- und Sitzungslisten mit Live-Zuständen. startSession kann ein anderes Projekt wählen, ohne das Board zu schließen. Siehe Host API.
contributes.integration ist optional. Es fügt unter Einstellungen → Integrationen eine Karte hinzu, auf der der Nutzer ein Konto verbindet. Siehe Konten und Netzwerk.
contributes.service ist optional. Es deklariert einen lokalen Dienst (einen Prozess, den OpenChamber neben der Erweiterung startet) für Dinge, an die eine Webseite nicht herankommt, etwa einen Docker-Socket. Dieser Prozess läuft mit dem vollen Zugriff des Nutzers und ohne Sandbox, deshalb warnt der Bestätigungsdialog davor; deklariere einen nur, wenn die Seite die Aufgabe nicht selbst erledigen kann. Siehe die Paketdatei GUEST_SERVICES.md. Ein Service mit provides: ["browser"] kann außerdem den Browser des Agenten vertreten: Er beantwortet die browser.*-Aktionen auf dem Server, sodass Agenten ohne geöffnete Desktop-App browsen, und der Nutzer wählt ihn unter Einstellungen → OpenChamber-Werkzeuge. Ein solcher Service braucht kein Panel. Ein Service kann außerdem erfahren, was in den Sitzungen des Nutzers passiert: Deklariere contributes.sessionEvents daneben, und OpenChamber schickt dem Service jeden Zug, der endet oder fehlschlägt, und jede Bitte um Erlaubnis und jede Frage des Agenten, aus jedem Projekt und ohne geöffnetes Fenster. Siehe Sitzungsereignisse. Und ein Service mit provides: ["notifications"] stellt die Benachrichtigungen des Nutzers neben Desktop und Telefon zu, an Slack, Matrix oder einen eigenen Server; der Nutzer verbindet das Konto unter Einstellungen → Integrationen, und das Token bleibt in OpenChamber. Siehe Benachrichtigungskanäle. Ein Dienst mit provides: ["tracker"] ist ein Tracker für Arbeitselemente neben Linear, mit seiner Kontokarte unter Erweiterungskonten; siehe Tracker. Und ein Dienst mit provides: ["quota"] meldet die Nutzung neben den eingebauten Anbietern und liest dafür die Zugangsdaten, die OpenCode für die im Manifest aufgezählten Anbieter hält; siehe Nutzungsanbieter.
Berechtigungen
Ein Panel zeichnen und die aktuelle Session lesen braucht keine Berechtigung. Alles, was im Namen des Nutzers handelt, schon. Solche Dinge listest du in contributes.capabilities auf:
| Berechtigung | Was sie erlaubt |
|---|---|
prompt | Nachrichten in die Session des Nutzers senden (prompt({ send: true }), startSession mit text) |
sessions | Projekte, Worktrees und Sitzungszustände auflisten; Sitzungen in registrierten Projekten erstellen und öffnen |
files | Dateien innerhalb des offenen Projekts lesen und schreiben (readFile, writeFile, listDir, stat mit einem relativen Pfad) |
model | einmalige Textgenerierung mit dem Small Model des Nutzers (generate), außerhalb jeder Session |
Elf weitere kommen automatisch dazu: network, wenn du eine integration deklarierst, service, wenn du einen service deklarierst, filesystem, wenn du filesystem-Muster deklarierst, origins, wenn du origins deklarierst, conversation, wenn eine Session-Aktion die Nachrichten anfordert (siehe Aktionen, Befehle und das Badge), transcript, wenn sessionEvents oder sessionControl content anfordert (siehe Sitzungsereignisse), sessionControl, wenn dein Dienst auf Sitzungen einwirkt (siehe OpenChamber aufrufen), sessionAnswers, wenn er im Namen des Nutzers die Berechtigungsanfragen und Formulare des Agenten beantwortet (sessionControl: { answers: true }), instructions, wenn er ergänzt, was Agenten lesen, und skills, wenn du contributes.skills deklarierst, einen Paketordner mit Skills, die jeden Agenten erreichen, solange die Erweiterung aktiv ist, und opencodePlugin, wenn du contributes.opencodePlugin deklarierst (siehe unten).
Der Nutzer sieht die vollständige Liste einmal, bei der Installation der Erweiterung, und bestätigt sie oder entfernt die Erweiterung. Fordert eine neue Version mehr an, erscheint der Dialog erneut. Ein Aufruf, der eine nicht bestätigte Berechtigung braucht, schlägt mit NOT_GRANTED fehl.
Ein OpenCode-Plugin in deinem Paket
Eine Erweiterung kann ein OpenCode-Plugin mitliefern, sodass die Leute eine Sache installieren statt zwei. Deklariere "opencodePlugin": "plugin", einen Ordner im Paket mit index.js oder server.js auf oberster Ebene. OpenCode liest main nicht, für einen Einstieg an anderer Stelle setze also name und exports in der package.json des Ordners. Der Einstieg exportiert per Default { id, setup }, die Form, die OpenCode lädt; er importiert nichts, weil OpenCode ihn mit einem bloßen import() aus deinem Ordner lädt, wo nichts installiert ist. Das input eines Tools ist reines JSON Schema, und sein execute antwortet mit { content }.
{ "contributes": { "panel": { "id": "teapot-tools", "name": "Teapot Tools", "icon": "cup" }, "opencodePlugin": "plugin" }}Der Freigabedialog bekommt eine weitere Zeile, Ein Plugin zu OpenCode hinzufügen, die klar sagt, dass Plugin-Code innerhalb von OpenCode mit dem vollen Zugriff des Nutzers läuft. Nach der Freigabe trägt OpenChamber deinen Ordner in die OpenCode-Konfiguration ein, die es verwaltet, dieselbe Datei wie bei contributes.skills; das von OpenChamber gestartete OpenCode beobachtet diese Datei und lädt das Plugin innerhalb weniger Sekunden. Deaktivieren oder Entfernen der Erweiterung nimmt es auf demselben Weg wieder heraus. In die eigene opencode.json des Nutzers wird nichts geschrieben: Diese Datei gehört dem Nutzer, und ein Plugin darin würde deine Erweiterung überleben und in jedem OpenCode auf dem Rechner laufen.
Wo diese Datei OpenCode nicht erreicht, sagt die Seite der Erweiterung das und bietet Zur globalen Konfiguration hinzufügen an, was deinen Ordner als Eintrag, der dem Nutzer gehört, in die globale OpenCode-Konfiguration schreibt. Das ist der Fall, wenn OpenChamber das OpenCode, mit dem es spricht, nicht gestartet hat, oder wenn die Umgebung des Servers ein eigenes OPENCODE_CONFIG setzt (das Plugin lädt dann beim nächsten Start von OpenCode). Ein OpenCode auf einem anderen Rechner kann keinen Ordner von diesem laden; die Seite sagt das und bietet nichts an. Ein Plugin, das nicht lädt, meldet OpenCode unter Einstellungen → Plugins, sobald es dort einen Eintrag hat; deine Erweiterung funktioniert in jedem Fall weiter. Die Installation lehnt ein Paket ab, dessen Plugin-Ordner fehlt oder dessen package.json keine Einstiegsdatei nennt.
VS Code und die Mobile-Apps laden keine Erweiterungen, also erreicht sie kein Plugin. Eine Sitzung in einem isolierten Space läuft mit einem eigenen OpenCode, das die Konfiguration des Hosts nie liest. Der Enterprise-Modus behandelt das Plugin wie einen lokalen Dienst: Es installiert sich nur aus einem Repository, das der Administrator freigegeben hat. Das SDK hat ein lauffähiges Beispiel, examples/opencode-plugin.
Text erzeugen
Mit der Berechtigung model bittet host.generate das Small Model des Nutzers um eine einmalige Antwort: eine Zusammenfassung, einen Titel, einen Entwurf. Nichts landet in einer Session, es wird kein Verlauf gespeichert, und die Erweiterung wählt nie einen Anbieter. OpenChamber nutzt dasselbe Modell, das es für seine eigene Hintergrundarbeit verwendet, eingestellt unter Einstellungen → Sessions → Small Model, oder das Small Model des Anbieters, mit dem der Nutzer gerade arbeitet, oder das Standardmodell aus seiner OpenCode-Konfiguration.
const { text } = await host.generate({ prompt: task.description, system: "Write a one-line summary. Return only the summary.", maxOutputTokens: 200,});Ist kein Modell verfügbar, schlägt der Aufruf mit NO_MODEL fehl; ein Modell, das einen Fehler geliefert hat, ist MODEL_FAILED. Prompts sind auf 64000 Zeichen begrenzt, und der Aufruf wartet bis zu 90 Sekunden. Der Nutzer sieht diese Berechtigung als „Dein Small Model verwenden“, und es kostet ihn Tokens, halte Prompts also kurz und ruf es bei einem Klick auf, nicht bei jedem Tastendruck.
Konten und Netzwerk
Deine Seite läuft in einer Sandbox und kommt nicht von selbst ins Netz: Skripte, Styles, Bilder, Schriften und Medien laden nur aus deinem Paket, und fetch erreicht nur die Dateien deines Pakets. Liefere Schriften und Bilder im Paket mit, statt sie von einem CDN zu laden.
Um mit einem externen Dienst zu sprechen, deklariere eine integration, und OpenChamber macht die Aufrufe über host.request für dich, mit dem Token des Nutzers. Der Token erreicht deine Seite nie.
Für alles andere, was deine Seite direkt braucht, etwa Schriften oder Bilder von einem CDN oder eine öffentliche API, listest du bis zu 8 https-Origins auf:
"origins": ["https://fonts.example.com"]Der Nutzer sieht sie im Bestätigungsdialog als Adressen, an die deine Erweiterung Daten senden und von denen sie Daten empfangen kann, und bestätigt sie erneut, wenn ein Update eine neue hinzufügt. Bestätigte Origins sind für fetch, Bilder, Schriften, Styles und Medien offen, aber nie für Skripte: Code muss in deinem Paket mitkommen. Der Origin der Seite ist null, daher muss der Server CORS für diesen Origin erlauben, damit du eine fetch-Antwort lesen kannst.
Im Enterprise-Modus lässt sich eine Erweiterung mit origins, einer integration oder einem service nur aus einem Git-Repository installieren, das der Administrator freigegeben hat. Wenn deine Erweiterung ohne sie auskommt, bleibt sie für solche Teams installierbar. Um eine Erweiterung auf so einem Rechner zu entwickeln, bitte den Administrator um allowLocalExtensions; damit kannst du sie aus ihrem Ordner installieren.
token: Der Nutzer fügt auf der Karte unter Einstellungen → Integrationen einen API-Token ein.apiOriginist der einzige Origin, denrequestaufrufen darf.accountist optional: ein GET-Pfad und ein Feldname, damit die Karte anzeigen kann, wer verbunden ist.schemesagt, wie der Token gesendet wird; siehe Tabelle unten. Prüfe in der API-Dokumentation des Anbieters, welchen Header sie erwartet.oauth: Der Nutzer fügt eine Client-ID ein, und OpenChamber führt den Authorize-Flow durch. BrauchtauthorizeUrl,tokenUrlundapiOrigin.host: { "provider": "linear" }: das in OpenChamber bereits verbundene Linear-Konto wiederverwenden. Keine Client-ID nötig.
Was jedes scheme sendet:
scheme | Header, den OpenChamber sendet | Wann verwenden |
|---|---|---|
| weggelassen | Authorization: <token> | die API dokumentiert einen nackten Token im Header |
"bearer" | Authorization: Bearer <token> | die API dokumentiert einen Bearer- oder Personal-Access-Token |
"basic" | Authorization: Basic base64(username:token) | die API dokumentiert HTTP Basic Auth mit Benutzername (oft eine E-Mail) und API-Token; die Karte fragt nach beidem, und usernameLabel beschriftet das erste Feld |
settings fügt der Karte einfache Textfelder hinzu. Ihre Werte kommen in ctx.settings an. Ein Feld mit "secret": true wird auf der Karte maskiert und erreicht nie eine Seite; dein Dienst liest es mit readSettings().
Dateien
Deine Seite kann die Festplatte nicht selbst anfassen. OpenChamber liest und schreibt für sie, innerhalb der Grenzen, die der Nutzer bestätigt hat.
- Ein relativer Pfad (
README.md,src/index.ts,.) meint das offene Projekt. Braucht die Berechtigungfiles. Kein offenes Projekt istNO_DIRECTORY. - Ein Pfad, der mit
/oder~/beginnt, meint irgendwo sonst. Er muss zu einem der Muster passen, die du incontributes.filesystemdeklarierst, und der Nutzer sieht genau diese Muster im Bestätigungsdialog:
"filesystem": ["~/.config/opencode/opencode.json", "~/notes/**"]In Mustern steht * für ein Pfadsegment und ** für beliebige Tiefe. Alles außerhalb davon ist BAD_PATH, auch nach der Bestätigung. .. ist nie erlaubt.
const config = await host.readFile("~/.config/opencode/opencode.json");await host.writeFile("~/.config/opencode/opencode.json", nextJson);const { entries } = await host.listDir(".");Schreibvorgänge sind atomar: OpenChamber schreibt eine temporäre Datei und benennt sie um, ein Leser sieht also nie eine halb geschriebene Datei. Dateien werden als UTF-8-Text gelesen und geschrieben, bis 2 MB.
Ein Abschnitt in Work Status
contributes.statusSection fügt dem Work-Status-Panel neben dem Chat einen eigenen Abschnitt hinzu. Nutzer schalten ihn in der Abschnittsliste dieses Panels ein und aus und verschieben ihn dort, wie die eingebauten Abschnitte.
"statusSection": { "entry": "status/index.html", "title": "Recent commits", "height": 160 }trueverwendetpanel.entry.entryist eine eigene HTML-Seite in deinem Ordner. Damit brauchst du keinpanel.entry, die Erweiterung kann also nur aus diesem Abschnitt bestehen, ohne Icon in der Leiste.titleersetzt den Panel-Namen in der Abschnittsüberschrift, bis zu 60 Zeichen.heightist die Anfangshöhe in Pixeln, 24 bis 320 (Standard 120). Rufehost.setHeight()auf, wenn sich die Größe deines Inhalts ändert.
Deine Seite sieht ctx.surface als status. OpenChamber lädt sie nur, solange das Work-Status-Panel sichtbar und dein Abschnitt aufgeklappt ist, und entlädt sie, wenn der Nutzer ihn zuklappt. Halte dort also nichts, was du nicht neu aufbauen kannst. Eine Erweiterung nur mit Abschnitt darf Berechtigungen, einen Dienst, eine Integration und Dateien nutzen, aber nicht page, attach, actions oder commands. Nur Web und Desktop. Das Beispiel git-graph-status ist ein vollständiger Abschnitt.
Ein Editor für deinen Dateityp
contributes.fileEditors öffnet Dateien in deiner Seite statt im Texteditor. Wenn der Nutzer in Files eine passende Datei öffnet, zeigt OpenChamber dort deinen Editor, mit demselben Quelltext-Umschalter, Vollbild-Button und Speichern-Button wie die eingebauten Viewer.
"fileEditors": [ { "id": "canvas", "title": "Excalidraw", "match": ["*.excalidraw", "*.excalidraw.md"], "entry": "editor/index.html" }]matchvergleicht nur Dateinamen und ignoriert Groß- und Kleinschreibung.*passt auf beliebig viele Zeichen,?auf genau eines. Ein Muster darf kein/enthalten und nicht nur aus Platzhaltern bestehen.entryist eine HTML-Seite in deinem Ordner. Du brauchst keinpanel.entry, die Erweiterung kann also nur aus dem Editor bestehen, ohne Icon in der Leiste.contentist"text"(Standard) oder"binary", siehe unten.- Bis zu 8 Editoren pro Erweiterung, jeweils bis zu 16 Muster.
OpenChamber verwaltet die Datei. Deine Seite liest oder schreibt sie nie und braucht dafür keine Berechtigung. OpenChamber gibt dir den Text, fragt beim Speichern nach dem bearbeiteten Text und kümmert sich um Cmd/Ctrl+S, automatisches Speichern, die Nachfrage bei ungespeicherten Änderungen, Zeilenenden und Änderungen auf der Festplatte:
import { connectHost, createFileSaveTracker } from "@openchamber/sdk";
const host = connectHost();let tracker = createFileSaveTracker(null);
host.onFileOpen((file) => { if (file.encoding !== "text") return; // ein Texteditor bekommt immer Text load(file.content); // auch file.path, file.name, file.readOnly tracker = createFileSaveTracker(versionOf(currentText()));});
// Nach jeder Änderung aufrufen. `edited` hält das automatische Speichern zurück, bis der Nutzer aufhört.const changed = () => host.reportFileChange(tracker.observe(versionOf(currentText())));
host.onFileSnapshot(() => ({ content: currentText(), version: versionOf(currentText()) }));
// Änderungen, die entstehen, während OpenChamber schreibt, lassen die Datei ungespeichert.host.onFileSaved((version) => host.reportFileChange({ dirty: tracker.markSaved(version), edited: false }));version ist ein beliebiger kurzer String, der sich ändert, wenn sich das Dokument ändert, etwa ein Hash. Wenn eine Datei nichts ist, was dein Editor öffnen kann, rufe host.reportFileUnsupported() auf, dann zeigt OpenChamber ihren Quelltext mit einem Hinweis. Dateien über 20 MB bleiben in der Quelltextansicht.
Binärdateien
Setze "content": "binary", um Dateien zu öffnen, die kein Text sind, etwa Tabellen, Dokumente oder Bilder. Dein Editor bekommt die Bytes der Datei und gibt Bytes zurück; alles andere funktioniert genauso.
host.onFileOpen((file) => { if (file.encoding !== "binary") return; open(file.bytes); // ein Uint8Array, genau wie auf der Festplatte});
host.onFileSnapshot(() => ({ bytes: currentBytes(), version: versionOf(currentBytes()) }));Ein binärer Editor kann jede Datei übernehmen, auf die seine Muster passen, auch solche, für die OpenChamber selbst eine Vorschau zeigt, wie Bilder und PDFs. Er hat keine Quelltextansicht, und wenn der Nutzer ihn mit ungespeicherten Änderungen in den Vollbildmodus bringt oder daraus holt, speichert OpenChamber sie vorher. Das Format zu lesen und zu schreiben ist Sache deines Editors: OpenChamber schreibt genau die Bytes zurück, die du zurückgibst.
Deine Seite sieht ctx.surface als file. OpenChamber lädt sie neu und übergibt die Datei erneut, wenn sich die Datei auf der Festplatte ändert, der Nutzer Änderungen verwirft oder der Editor in den Vollbildmodus wechselt oder ihn verlässt. Die erste aktivierte Erweiterung mit passendem Muster gewinnt, noch vor den eigenen Vorschauen von OpenChamber. Eine Erweiterung nur mit Editor darf Berechtigungen, einen Dienst, eine Integration und Dateien nutzen, aber nicht page, attach, actions oder commands. Nur Web und Desktop. Das Beispiel checklist-editor ist ein vollständiger Editor.
Aktionen, Befehle und das Badge
Neben der Leiste und dem +-Menü kann eine Erweiterung an drei weiteren Stellen erscheinen. Alle übergeben der Erweiterung ein item, genau wie ein Klick auf einen Chip.
Aktionen auf Nachrichten und Sessions. Deklariere Menüeinträge, und OpenChamber zeigt sie neben den eingebauten:
"actions": [ { "id": "create-task", "label": "Create task", "icon": "add-line", "where": "message", "roles": ["assistant"] }, { "id": "summarize", "label": "Summarize session", "where": "session", "payload": ["messages"] }]where nimmt auch selection (markierter Text in einer Nachricht), file (eine Zeile in Files) und diff (markierte Zeilen einer Datei oder eines Diffs). Ihre Einträge tragen ein gleichnamiges kind: selection enthält den Text, file den Pfad, diff den Pfad und den Zeilenbereich. Bei einer Auswahl legt display fest, wie die Aktion erscheint: label (der Standard), icon mit dem Label als Tooltip oder both.
Eine Nachrichtenaktion öffnet deine Erweiterung mit einem ctx.item der Art kind: "message": Session-ID und -Titel, Projektordner, Nachrichten-ID, ihre Rolle und ihr Text. Eine Session-Aktion öffnet sie mit kind: "session" sowie Session-ID, Titel und Ordner. Füge "payload": ["messages"] hinzu, und der Eintrag trägt außerdem das ganze Gespräch, älteste Nachricht zuerst, derselbe Text, den der Markdown-Export erzeugt. Dafür braucht es die Berechtigung conversation, die der Nutzer als eigene Zeile im Freigabedialog sieht. item.action sagt dir, welcher Eintrag angeklickt wurde.
Slash-Befehle, die anhängen. Deklariere einen Befehl und behandle ihn in der Seite:
"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;});Wenn der Nutzer /task ABC-12 ins Chatfeld tippt, bittet OpenChamber deine Erweiterung, das aufzulösen, und hängt das Ergebnis als Chip an, ohne etwas zu öffnen. Gib null zurück für “nichts gefunden”. Ist dein Panel geschlossen, lädt OpenChamber es für den Aufruf im Hintergrund. Ein Name, den OpenChamber oder OpenCode schon verwendet, wird ignoriert.
Badge auf dem Symbol in der Leiste. host.setBadge(3) setzt einen Punkt auf dein Symbol und zeigt die Zahl im Tooltip, zum Beispiel offene Aufgaben; host.setBadge(null) entfernt sie. Das Öffnen des Panels entfernt sie ebenfalls.
Deine Tools im Chat
Wenn dein OpenCode-Plugin oder MCP-Server ein Tool hinzufügt, zeigt der Chat dessen Aufrufe mit einem generischen Symbol und roher Ausgabe. Beschreibe stattdessen, wie sie aussehen sollen, ganz ohne Code:
"tools": [ { "match": "mcp.jira.*", "name": "Jira", "icon": "task-line", "title": "{input.key}", "subtitle": "{output.status}", "output": "table", "columns": ["key", "summary", "status"] }]match ist der Tool-Name, wie OpenCode ihn meldet; ein * am Ende passt auf ein Präfix. icon ist ein Remixicon-Name oder eine .svg-Datei in deinem Paket, wie bei panel.icon. title und subtitle sind Vorlagen über input, output und metadata des Aufrufs. output bestimmt, wie der Inhalt dargestellt wird: text, json (als Baum), markdown, code mit einer language, table mit columns (die Zeilen kommen aus dem Ausgabe-Array oder aus output.items) oder auto für die Standarddarstellung. Ein exaktes match schlägt einen Platzhalter, und bei Gleichstand gewinnt die zuerst installierte Erweiterung. Eine Berechtigung ist nicht nötig: das ändert nur, wie Daten gezeichnet werden, die ohnehin schon im Chat sind.
Eine Erweiterung, die nur Tools gestaltet, braucht gar keine Seite: lass panel.entry weg, dann hat sie kein Icon in der Leiste, nur eine Karte unter Einstellungen → Erweiterungen. Ohne Seite darf sie tools und sonst nichts deklarieren. Die andere Ausnahme ist ein Work-Status-Abschnitt oder ein Datei-Editor mit eigenem entry, siehe oben.
Die ersten Zeilen im Panel
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 funktioniert nur innerhalb von OpenChamber. Als einfache Datei geöffnet, wird jeder Aufruf mit HOST_UNAVAILABLE abgewiesen.
Bau das Panel zuerst aus den Bausteinen von @openchamber/sdk/ui: Buttons, Felder, Dropdowns, Tabs, Listen und mehr, gestaltet mit den Farben und Schriften der App, damit das Panel wie ein Teil von OpenChamber wirkt. Eigenes HTML und CSS schreibst du nur für das, was das Kit nicht bietet. Siehe UI-Kit.
Verwandt
- Erweiterungen für SSH-Installationen und die Git-Identität des Servers
- Host API für jede
connectHost-Methode, ihre Grenzen und die Fehlercodes - UI-Kit für die Bausteine
- Beispiel für eine komplette Erweiterung aus drei Dateien zum Kopieren