fix(ui): stop shiki template-call backtracking from OOMing the renderer
Shiki's bundled JS/TS/JSX/TSX grammars carry a `template-call` rule whose triple-nested lookahead sends the Oniguruma WASM engine into exponential backtracking on ordinary backtick templates. The WASM heap grows until the renderer runs out of memory and the window goes black. Strip `template-call` from those four grammars as they are loaded in the Shiki worker. Plain backticks and simple tagged templates still highlight; only the rare `ident<TypeArgs>` tagged-template form loses its specialized type-argument coloring. Add a 5s per-request budget on the worker client as a safety net for any other pathological pattern. Matching is synchronous inside the worker, so the only way to reclaim its heap is to terminate it from the main thread. A timed-out request resolves `null` like any other failure, so the caller keeps plain text and nothing is written to the result cache. Closes #2587
This commit is contained in:
@@ -28,7 +28,7 @@ The tool can list projects and model preferences, create and follow up on sessio
|
||||
|
||||
## Turn the tool on or off
|
||||
|
||||
Open **Settings → General → OpenCode CLI**, change **Agent control tool**, then select **Save + Reload**. The setting applies after the managed OpenCode server restarts.
|
||||
Open **Settings → General → OpenChamber Tools** and change **Agent control tool**. The setting applies once the managed OpenCode server restarts, which OpenChamber offers as **Apply & Restart**.
|
||||
|
||||
The tool is not available when OpenChamber connects to an external OpenCode server through `OPENCODE_HOST` or skip-start, or inside the VS Code extension. Desktop and web installations that use OpenChamber's managed OpenCode server support it automatically.
|
||||
|
||||
@@ -37,3 +37,4 @@ The tool is not available when OpenChamber connects to an external OpenCode serv
|
||||
- [Scheduled Tasks](/scheduled-tasks/)
|
||||
- [Worktree Sessions](/worktrees/)
|
||||
- [Session Goals](/session-goals/)
|
||||
- [Browser Panel](/desktop-browser/) — the OpenChamber Web tool, for looking at and driving a page
|
||||
|
||||
@@ -28,7 +28,7 @@ Das Werkzeug kann Projekte und Modelleinstellungen auflisten, Sitzungen erstelle
|
||||
|
||||
## Werkzeug ein- oder ausschalten
|
||||
|
||||
Öffne **Einstellungen → Allgemein → OpenCode CLI**, ändere **Agent control tool** und wähle dann **Save + Reload**. Die Einstellung gilt, nachdem der verwaltete OpenCode-Server neu gestartet wurde.
|
||||
Öffne **Einstellungen → Allgemein → OpenChamber-Werkzeuge** und ändere **Agent control tool**. Die Einstellung gilt, sobald der verwaltete OpenCode-Server neu startet — OpenChamber bietet das als **Apply & Restart** an.
|
||||
|
||||
Das Werkzeug ist nicht verfügbar, wenn OpenChamber über `OPENCODE_HOST` oder skip-start mit einem externen OpenCode-Server verbunden ist oder innerhalb der VS-Code-Erweiterung läuft. Desktop- und Web-Installationen, die den verwalteten OpenCode-Server von OpenChamber verwenden, unterstützen es automatisch.
|
||||
|
||||
@@ -37,3 +37,4 @@ Das Werkzeug ist nicht verfügbar, wenn OpenChamber über `OPENCODE_HOST` oder s
|
||||
- [Geplante Aufgaben](/scheduled-tasks/)
|
||||
- [Worktree-Sitzungen](/worktrees/)
|
||||
- [Sitzungsziele](/session-goals/)
|
||||
- [Browser-Panel](/desktop-browser/) — das OpenChamber-Web-Werkzeug, um eine Seite anzusehen und zu bedienen
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
---
|
||||
title: Desktop-Browser
|
||||
description: Durchsuche jede Seite in der Desktop-App mit Inspektion und Konsolenaufzeichnung.
|
||||
title: Browser-Panel
|
||||
description: Öffne jede Seite in der App, annotiere sie und lass den Agenten sie bedienen.
|
||||
---
|
||||
|
||||
# Desktop-Browser
|
||||
# Browser-Panel
|
||||
|
||||
Die Desktop-App hat einen eingebauten Browser, damit du jede Seite direkt neben deinem Chat öffnen, auf Elemente zeigen und danach fragen sowie die Konsole der Seite aufzeichnen kannst. Öffne ihn über die Globus-Schaltfläche im App-Kopfbereich.
|
||||
Das Browser-Panel öffnet jede Seite direkt neben deinem Chat. Öffne es über die Globus-Schaltfläche in der Kopfzeile.
|
||||
|
||||
> Der Desktop-Browser ist eine Funktion **nur für den Desktop**. Im Web bietet das [Preview](/preview/)-Panel dieselben Inspektions- und Konsolentools für deinen lokalen Dev-Server.
|
||||
In der Desktop-App ist es ein echter Browser: Deine Logins bleiben erhalten, Hot Reload funktioniert, und die Entwicklerwerkzeuge sind einen Klick entfernt. In einem Browser-Tab zeigt das Panel eine Seite zwar an, kann aber nicht in sie hineinsehen — die Annotationswerkzeuge unten gibt es nur auf dem Desktop.
|
||||
|
||||
## Inspizieren und annotieren
|
||||
Seiten, die hier geöffnet werden, bekommen keinen Zugriff auf Kamera, Mikrofon oder Standort: solche Anfragen werden abgelehnt.
|
||||
|
||||
Aktiviere **inspect** und klicke auf ein beliebiges Element auf der Seite. OpenChamber erstellt dazu eine Notiz — was es ist, welche Stile es hat, wo es sich befindet und einen Screenshot — und hängt sie an deine Chatnachricht an. Das ist der schnellste Weg, dem Agenten zu sagen: „dieses Element, genau hier“.
|
||||
## Die Werkzeugleiste
|
||||
|
||||
## Konsolenaufzeichnung
|
||||
Die Adressleiste merkt sich Seiten, die du in diesem Projekt geöffnet hast, und schlägt sie beim Tippen vor — passend zu einem Teil der Adresse oder des Seitentitels. Mit den Pfeiltasten gehst du durch die Liste, Enter öffnet den markierten Eintrag, und die Schaltfläche in einer Zeile entfernt ihn.
|
||||
|
||||
Der Browser sammelt die Konsolenausgabe der Seite — Fehler, Warnungen und Logs — damit du sie filtern und lesen kannst, ohne die Entwicklertools zu öffnen.
|
||||
Daneben liegt **Neu laden**, dazu ein **hartes Neuladen**, das den Cache übergeht, wenn eine Änderung partout nicht erscheint, sowie eine Zoomsteuerung, die nur die Seite skaliert.
|
||||
|
||||
**Cookies löschen** und **Zwischengespeicherte Daten löschen** gelten allein für dieses Panel. Deine OpenChamber-Sitzung und andere Fenster bleiben unberührt.
|
||||
|
||||
## Eine Seite annotieren
|
||||
|
||||
Drücke **Annotieren**, und über der Seite erscheint eine Leiste mit drei Werkzeugen:
|
||||
|
||||
- **Element** — klicke ein Element an. Ein Klick auf ein anderes verschiebt die Auswahl, ein erneuter Klick auf dasselbe hebt sie auf.
|
||||
- **Bereich** — ziehe einen Rahmen um einen Ausschnitt, wenn es um mehr als ein Element geht.
|
||||
- **Zeichnen** — skizziere frei über die Seite.
|
||||
|
||||
Schreib dein Anliegen in das Feld neben deiner Markierung und drücke **Anhängen** — oder einfach Enter. Deine Chat-Nachricht bekommt eine Karte mit allem Markierten, deiner Notiz und einem Screenshot der sichtbaren Seite mit deinen Markierungen darauf — du kannst also „dieser Button, etwas runder" sagen, statt zu beschreiben, wo er steht.
|
||||
|
||||
Die Seite selbst wird nie verändert — Annotieren markiert nur, was da ist. `Esc` bricht ab und schließt die Leiste.
|
||||
|
||||
## Den Agenten steuern lassen
|
||||
|
||||
Der Agent kann das Browser-Panel selbst benutzen — eine Seite öffnen, lesen, was darauf steht, klicken, tippen, scrollen und zwischen mobiler, Tablet- und Desktop-Ansicht wechseln — um seine eigene Arbeit zu prüfen, statt dich darum zu bitten. Du siehst es im Panel passieren.
|
||||
|
||||
Beliebigen Code kann der Agent auf der Seite nicht ausführen. Der Browser behält deine echten Logins, deshalb bleibt er auf die genannten Aktionen beschränkt.
|
||||
|
||||
Er kann außerdem ein Bild dessen, was er sieht, in `.openchamber/screenshots/` in deinem Projekt speichern und es dir in seiner Antwort zeigen. Genau das macht ein Vorher-Nachher möglich, und die Datei bleibt danach liegen, um sie an einen Pull Request zu hängen.
|
||||
|
||||
Die Browser-Aktionen sind das **OpenChamber-Web-Werkzeug**, das sich unter **Einstellungen → Allgemein → OpenChamber-Werkzeuge** einzeln ein- und ausschalten lässt.
|
||||
|
||||
Dafür braucht es die Desktop-App: eine Seite in einem Browser-Tab lässt sich nicht steuern.
|
||||
|
||||
## Entwicklerwerkzeuge
|
||||
|
||||
Drücke die Terminal-Schaltfläche in der Leiste, um Chromiums eigene Entwicklerwerkzeuge für die Seite zu öffnen — Konsole, Netzwerk, Elemente, alles Gewohnte.
|
||||
|
||||
## Verwandt
|
||||
|
||||
- [Preview & Dev Servers](/preview/) — dieselben Tools für deinen lokalen Dev-Server
|
||||
- [Vorschau & Dev-Server](/preview/) — deine laufende App öffnen, auch auf einem entfernten Rechner
|
||||
- [Agenten-Steuerungswerkzeug](/agent-control-tool/) — Sitzungen, Worktrees und geplante Aufgaben aus dem Chat
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Integrationen
|
||||
description: Nutze dein Claude- oder Cursor-Abo als Provider.
|
||||
---
|
||||
|
||||
# Integrationen
|
||||
|
||||
Eine Integration ist ein kleines Plugin, das OpenChamber einen Provider hinzufügt — auf Basis eines Abos, das du bereits hast. Verwalten kannst du sie unter **Settings → Integrations**.
|
||||
|
||||
> **Experimentelle Funktion.** Wir bemühen uns, die Richtlinien der Anbieter zu respektieren, aber Kontobeschränkungen und Sperrungen liegen bei jedem Anbieter. Nutze Integrationen auf eigenes Risiko.
|
||||
|
||||
Verfügbare Integrationen:
|
||||
|
||||
- **Claude Code** — dein Claude Pro- oder Max-Plan, ohne API-Keys
|
||||
- **Cursor** — die Modell-Limits deines Cursor-Plans
|
||||
|
||||
## Integration installieren
|
||||
|
||||
1. Öffne **Settings → Integrations**.
|
||||
2. Suche die Integration und wähle **Install**.
|
||||
3. Starte OpenCode neu, wenn darum gebeten wird — der Provider erscheint nach dem Neustart.
|
||||
4. Wähle **Set up** und melde dich an. Die Modelle erscheinen danach in der Modellauswahl im Chat.
|
||||
|
||||
Integrationen werden für deinen Benutzer installiert und funktionieren damit in jedem Projekt. Aktualisieren oder entfernen kannst du sie jederzeit über dieselbe Karte.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code nutzt deinen Claude Pro- oder Max-Plan — ohne API-Keys und ohne separate Claude-App.
|
||||
|
||||
1. Installiere die Integration (siehe oben).
|
||||
2. Wähle **Set up** und melde dich an. Wenn du die Claude Code CLI noch nicht hast, bietet die Einrichtung an, sie zuerst zu installieren, und meldet dich danach an.
|
||||
|
||||
Claude Code ist die einzige Integration hier, die ihre Provider-CLI installiert und angemeldet benötigt. Cursor braucht seine CLI nicht.
|
||||
|
||||
**Wie dein Claude-Konto geschützt bleibt:** Diese Integration nutzt das offizielle Claude Agent SDK von Anthropic und deine installierte Claude Code CLI. Sie kapert kein OAuth, extrahiert oder wiederholt keine Browser-Tokens, gibt sich nicht als nicht unterstützter Client aus und umgeht nicht Anthropics Authentifizierung. Sie bleibt auf dem von Anthropic unterstützten Zugriffsweg und trägt daher nicht das mit Token-Hijacking oder unautorisierten Authentifizierungsumgehungen verbundene Sperrrisiko.
|
||||
|
||||
## Cursor
|
||||
|
||||
Cursor macht die Modelle deines Cursor-Plans in OpenChamber nutzbar.
|
||||
|
||||
1. Installiere die Integration (siehe oben).
|
||||
2. Wähle **Set up**, öffne den Link und genehmige den Zugriff im Browser. Kein API-Key nötig. Die Modellliste lädt nach der Anmeldung automatisch.
|
||||
|
||||
## Aktualisieren oder entfernen
|
||||
|
||||
- **Update** installiert die neueste veröffentlichte Version des Plugins.
|
||||
- **Remove** löscht das Plugin aus deiner OpenCode-Konfiguration. Der Provider wird beim nächsten Neuladen von OpenCode nicht mehr geladen.
|
||||
|
||||
Wenn eine Karte meldet, dass Einträge manuell verwaltet werden müssen, wähle **Manage plugins** und bereinige die Duplikate dort.
|
||||
|
||||
## Verwandtes
|
||||
|
||||
- [Anbieter, Modelle und Agenten](/de/providers/) — weitere Provider verbinden und Modelle wählen
|
||||
- [Nutzung und Kontingente](/de/usage/) — verfolge, wie viel du genutzt hast
|
||||
@@ -21,6 +21,64 @@ Einige Prompts haben einen sichtbaren Teil (die Nachricht, die du sehen würdest
|
||||
|
||||
Anders entschieden? Jeder Prompt hat **Auf Standard zurücksetzen**, und es gibt **Alle zurücksetzen**, wenn du überall neu anfangen möchtest.
|
||||
|
||||
## Wo jeder Prompt verwendet wird
|
||||
|
||||
Für jeden Prompt steht unten, wo er läuft und was ihn auslöst. Prüfe den Auslöser vor der Bearbeitung, dann weißt du, welchen Ablauf du änderst.
|
||||
|
||||
### Git
|
||||
|
||||
| Prompt | Wo er läuft | Wann er ausgelöst wird |
|
||||
| --- | --- | --- |
|
||||
| Commit-Erstellung | Die Generieren-Schaltfläche im Commit-Feld der Git-Ansicht und im mobilen Changes-Bildschirm | Du erzeugst eine Commit-Nachricht. Ausgewählte Dateien und die letzten Commit-Betreffs des Branchs werden eingesetzt, sodass die Nachricht zum Stil deines Repos passt. |
|
||||
| PR-Erstellung | Das Pull-Request-Anlegen-Formular im PR-Tab der Git-Ansicht | Du erzeugst Titel und Beschreibung eines PRs. Eingebaut werden Base- und Head-Branch, die Commits und geänderten Dateien dazwischen, dein zusätzlicher Kontext und die PR-Vorlage des Repos, falls vorhanden. |
|
||||
| Merge/Rebase-Konfliktlösung | Der Konflikt-Dialog der Git-Ansicht, wenn ein Merge oder Rebase auf Konflikten stoppt | Du wählst "Resolve in current session" oder "Resolve in new session". Der Agent liest die konfliktbehafteten Dateien, schlägt eine Lösungsstrategie pro Datei vor und wartet auf deine Bestätigung, bevor er etwas ändert, staged oder fortfährt. |
|
||||
| Cherry-pick-Konfliktlösung | Der Bereich "Re-integrate commits" einer Worktree-Sitzung | Beim Übertragen der Sitzungs-Commits auf den Zielbranch entsteht ein Konflikt und du übergibst ihn dem Agenten. Der Agent löst im temporären Worktree, staged die Dateien und setzt den Cherry-pick fort. |
|
||||
|
||||
### GitHub
|
||||
|
||||
| Prompt | Wo er läuft | Wann er ausgelöst wird |
|
||||
| --- | --- | --- |
|
||||
| PR-Review | Der "Link GitHub PR"-Picker im Anhänge-Menü des Composers und der neue Worktree-Dialog | Zwei Auslöser. Hängst du einen PR als Kontext an, werden die Anweisungen erzeugt und mit deiner nächsten Nachricht mitgesendet. Startest du eine Worktree-Sitzung aus einem PR, bildet der Prompt die erste Nachricht dieser Sitzung, mit dem vollständigen PR-Kontext. |
|
||||
| Issue-Review | Der neue Worktree-Dialog, wenn der Worktree aus einem Issue startet | Die erste Nachricht der neuen Sitzung reviewed das Issue, mit Titel, Text und Kommentaren als Kontext. |
|
||||
| Fehlgeschlagene PR-Checks / PR-Kommentare / einzelner PR-Kommentar | — | Wird heute von keinem Ablauf gesendet. Die PR-Ansicht löste sie früher über Ein-Klick-Review-Aktionen aus; fehlgeschlagene Checks und Kommentare werden jetzt als Chat-Kontext-Entwürfe angeheftet. Sie bleiben editierbar, damit bestehende Overrides weiter funktionieren. |
|
||||
|
||||
### Planung
|
||||
|
||||
| Prompt | Wo er läuft | Wann er ausgelöst wird |
|
||||
| --- | --- | --- |
|
||||
| Todo-Planung | Das Todos-Panel in der Projekt-Seitenleiste | Du schickst ein Todo an eine Sitzung oder eine neue Worktree-Sitzung. Der Todo-Text wird zur sichtbaren Nachricht; die Anweisungen machen daraus einen fragegesteuerten Planungsdialog statt sofort loszulegen. |
|
||||
| Plan verbessern | Die Aktion "Improve" für einen gespeicherten Plan in der Plans-Ansicht | Du schickst einen gespeicherten Plan in den Verbesserungsfluss. Der Agent liest zuerst die Plandatei, schlägt dann Änderungen auf Basis des aktuellen Repo-Zustands vor und bietet an, dieselbe Datei zu bearbeiten. |
|
||||
| Plan umsetzen | Die Aktion "Implement" für einen gespeicherten Plan | Du schickst einen gespeicherten Plan in den Umsetzungsfluss. Der Agent liest die Plandatei und setzt sie komplett um, ohne den Rahmen zu sprengen; nötige Plananpassungen schreibt er in dieselbe Datei zurück. |
|
||||
|
||||
### Sitzung
|
||||
|
||||
Die meisten davon treiben Slash-Befehle an, die du im Composer eingibst. Die meisten erscheinen auch als Starter-Chips im Entwurf einer neuen Sitzung.
|
||||
|
||||
| Prompt | Wo er läuft | Wann er ausgelöst wird |
|
||||
| --- | --- | --- |
|
||||
| Codebase-Tour | `/explore` | Du möchtest einen Überblick über die Codebase. |
|
||||
| Sitzungszusammenfassung | `/summary`, optional `/summary <Thema>` | Du fasst die bisherige Konversation zusammen — nützlich zur Übergabe an eine neue Sitzung. Benötigt eine bestehende Sitzung. |
|
||||
| Workspace-Review | `/workspace-review` | Du lässt den Agenten den aktuellen Workspace-Diff auf Absicht, Korrektheit und Sicherheit prüfen. |
|
||||
| Feature-Planung | `/plan-feature` | Du machst aus einer groben Feature-Idee über einen geführten Frage-Antwort-Dialog einen Umsetzungsplan. |
|
||||
| Goal formulieren | `/craft-goal`, optional `/craft-goal <Idee>` | Du machst aus einer Idee ein überprüfbares Goal-Ziel für den Goal-Dialog. |
|
||||
| Catch-up | `/catch-up` | Du kehrst zu einem Projekt zurück und fragst, wo es steht und wie es weitergeht. |
|
||||
| Debugging | `/debug` | Du untersuchst einen Bug: Der Agent bildet Hypothesen, bestätigt die Ursache aus dem Code und schlägt erst dann eine Lösung vor. |
|
||||
| Optionen abwägen | `/weigh` | Du weißt, was du bauen willst, aber nicht wie. Der Agent vergleicht zwei oder drei Ansätze und empfiehlt einen. |
|
||||
| Fusion | Die Aktion "Run fusion" auf einer Multi-run-Gruppe | Du vereinigst die Ausgaben mehrerer Läufe zu einer Antwort. Die Lauf-Ausgaben werden hinter die Anweisungen angehängt. |
|
||||
|
||||
### Prompts ohne Settings-Seite
|
||||
|
||||
Einige Prompts laufen automatisch und haben keine editierbare Seite in den Einstellungen:
|
||||
|
||||
| Prompt | Wann er ausgelöst wird |
|
||||
| --- | --- |
|
||||
| Geplante Aufgabe | `/schedule-task`, optional mit einer ersten Idee. Führt durch den Dialog, der eine geplante Aufgabe definiert. |
|
||||
| Review-Übergabe | `/handoff-review` oder die Review-Schaltfläche in der Diff-Ansicht mit aktivierter Übergabe. Erzeugt die Übergabe in der Arbeitssitzung. |
|
||||
| Startnachricht der Review-Sitzung | Die erste Nachricht der erzeugten Review-Sitzung — mit Übergabe, wenn eine erzeugt wurde, sonst ohne. |
|
||||
| Review-Feedback / Umsetzungsantwort | Bringen Nachrichten zwischen den beiden Sitzungen hin und her: Review-Feedback geht zurück an die umsetzende Sitzung, die Antwort des Umsetzers zurück an die Review-Sitzung. |
|
||||
|
||||
## Weiterführend
|
||||
|
||||
- [Git- & GitHub-Workflows](/git/) — viele dieser Prompts treiben die Git-Abläufe an
|
||||
- [Notizen, Todos & Pläne](/notes-todos-plans/) — die Todos und Pläne hinter den Planungs-Prompts
|
||||
- [Multi-run](/multi-run/) — Laufgruppen und Fusion
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
---
|
||||
title: Vorschau & Entwicklungsserver
|
||||
description: Öffne einen laufenden Entwicklungsserver direkt in OpenChamber.
|
||||
title: Vorschau & Dev-Server
|
||||
description: Öffne einen laufenden Dev-Server direkt in OpenChamber.
|
||||
---
|
||||
|
||||
# Vorschau & Entwicklungsserver
|
||||
# Vorschau & Dev-Server
|
||||
|
||||
Wenn du einen Entwicklungsserver startest, kann OpenChamber ihn direkt in der App öffnen statt in einem separaten Browser-Tab — so kannst du deine Seite neben dem Chat sehen, ihre Konsole aufzeichnen und Elemente anstupsen, um Fragen dazu zu stellen.
|
||||
Wenn du einen Dev-Server startest, kann OpenChamber ihn direkt in der App öffnen statt in einem separaten Browser-Tab — so siehst du deine Seite neben dem Chat und kannst auf Elemente zeigen, um danach zu fragen.
|
||||
|
||||
## Eine Vorschau öffnen
|
||||
## Einen Dev-Server öffnen
|
||||
|
||||
OpenChamber überwacht die Terminalausgabe auf eine lokale Adresse (die `Local:`-Zeile, die Werkzeuge wie Vite, Next.js oder Astro ausgeben). Sobald es eine findet:
|
||||
Öffne das Browser-Panel über die Globus-Schaltfläche in der Kopfzeile. Läuft bereits ein Dev-Server, steht er dort in der Liste und ein Klick öffnet ihn — OpenChamber findet ihn daran, was auf deinem Rechner tatsächlich lauscht, also unabhängig davon, wie du ihn gestartet hast.
|
||||
|
||||
- erscheint im Terminal eine Schaltfläche **Open preview**
|
||||
- öffnet eine [Projektaktion](/project-actions/) mit aktiviertem Auto-Open die Vorschau für dich
|
||||
- kann auch ein lokaler Link in einer Chatnachricht sie öffnen
|
||||
Ein Dev-Server öffnet sich außerdem automatisch, wenn:
|
||||
|
||||
Die Seite lädt im Seitenbereich. Nur lokale Adressen (auf deinem eigenen Rechner) können als Vorschau geöffnet werden.
|
||||
- du im Terminal bei einer lokalen Adresse auf **Vorschau öffnen** drückst
|
||||
- eine [Projektaktion](/project-actions/) mit aktiviertem Auto-Öffnen einen startet
|
||||
- du einem lokalen Link in einer Chat-Nachricht folgst
|
||||
|
||||
## Konsole und Inspektion
|
||||
Du kannst die Adresse jederzeit selbst eintippen. Ein bloßes `localhost:5173` wird als `http://` verstanden, das Schema musst du also nicht mitschreiben.
|
||||
|
||||
Im Vorschau-Bereich kannst du:
|
||||
## Mit einem entfernten OpenChamber arbeiten
|
||||
|
||||
- die **Konsole** der Seite beobachten — Fehler, Warnungen und Protokolle, so gefiltert, wie du möchtest
|
||||
- **inspect** einschalten, auf ein beliebiges Element klicken und eine Notiz dazu senden — Selektor, Stile, Position und ein Screenshot — direkt in den Chat
|
||||
Läuft OpenChamber auf einem anderen Rechner, liegt sein Dev-Server ebenfalls auf *jenem* Rechner — `localhost` auf deinem Laptop führt ganz woandershin. Die Desktop-App erledigt das für dich: Sie öffnet einen lokalen Port, der die Verbindung zum entfernten Dev-Server durchreicht. Die Seite lädt ganz normal, mit funktionierendem Hot Reload und Entwicklerwerkzeugen. Du tippst weiterhin die Adresse, die du erwartest; die Technik dahinter bleibt dir aus dem Weg.
|
||||
|
||||
Das ist der schnellste Weg, dem Agenten "diese Schaltfläche hier" zu sagen, ohne sie beschreiben zu müssen.
|
||||
Dafür brauchst du die Desktop-App. In einem Browser-Tab lassen sich nur Dev-Server auf deinem eigenen Rechner öffnen.
|
||||
|
||||
## Die Seite annotieren
|
||||
|
||||
Wie du auf Elemente zeigst, auf der Seite zeichnest und alles in den Chat schickst, steht unter [Browser-Panel](/desktop-browser/).
|
||||
|
||||
## Verwandt
|
||||
|
||||
- [Projektaktionen](/project-actions/) — einen Server automatisch öffnen, wenn du ihn startest
|
||||
- [Desktop-Browser](/desktop-browser/) — dieselben Werkzeuge für jede Seite, auf dem Desktop
|
||||
- [Projektaktionen](/project-actions/) — einen Server beim Start automatisch öffnen
|
||||
- [Browser-Panel](/desktop-browser/) — Seiten annotieren und den Agenten steuern lassen
|
||||
|
||||
@@ -45,5 +45,6 @@ Provider-Anmeldungen werden von OpenCode gespeichert, nicht von OpenChamber, dah
|
||||
|
||||
## Weiterführend
|
||||
|
||||
- [Integrationen](/integrations/) — nutze ein Claude-, Command-Code- oder Cursor-Abo als Provider
|
||||
- [MCP-Server](/mcp/) — füge Agents zusätzliche Werkzeuge hinzu
|
||||
- [Nutzung & Kontingente](/usage/) — verfolge, wie viel du verbraucht hast
|
||||
|
||||
@@ -12,7 +12,7 @@ Zum Schreiben eigener Skills siehe [Skills](/skills/).
|
||||
## Einen Skill installieren
|
||||
|
||||
1. Öffne den Katalog.
|
||||
2. Durchsuche die eingebauten Quellen — das Anthropic-Skills-Repo und die ClawdHub-Community-Registry — oder nutze die Suche.
|
||||
2. Durchsuche die eingebauten Quellen — wie das Anthropic-Skills-Repo — oder nutze die Suche.
|
||||
3. Wähle einen Skill aus und installiere ihn.
|
||||
4. Entscheide, wo er installiert werden soll: für alles, was du tust, oder nur für das aktuelle Projekt.
|
||||
|
||||
|
||||
@@ -11,6 +11,20 @@ Es erklärt und ordnet. Es bewertet Ihren Code nicht und fällt kein Urteil —
|
||||
|
||||
Öffnen Sie es über das **Walkthrough**-Symbol in der rechten Leiste oder über die Schaltfläche **AI walkthrough** in den Bereichen Changes und Pull Request. Beides öffnet nur das Panel; generiert wird erst, wenn Sie **Generate walkthrough** drücken.
|
||||
|
||||
## Wie ein Stop markiert ist
|
||||
|
||||
Jeder Stop benennt sein Thema, erklärt es in ein bis zwei Sätzen und zeigt danach genau den Code, den er beschreibt. Manche Stops tragen eine kleine Markierung neben dem Titel:
|
||||
|
||||
| Markierung | Bedeutung |
|
||||
| --- | --- |
|
||||
| **Kernänderung** | Dieser Stop trägt die eigentliche Änderung oder den größten Teil ihres Risikos. Lesen Sie ihn genau und zuerst. |
|
||||
| **Kontext** | Eine unterstützende Änderung, damit der Rest verständlich bleibt. Kann überflogen werden. |
|
||||
| *(ohne Markierung)* | Ein gewöhnlicher Schritt in der Lesereihenfolge. |
|
||||
|
||||
Die Markierung sagt, **wo Sie Ihre Aufmerksamkeit investieren sollten**, und nichts über die Qualität des Codes. Ein Stop wird nie markiert, weil darin etwas Falsches gefunden wurde — das Walkthrough meldet keine Funde, keine Schweregrade und keine Urteile. Wenn Code bewertet werden soll, ist das die Aktion **Review** in [Git & GitHub](/git/).
|
||||
|
||||
Die einzigen Markierungen, die tatsächlich auf ein Problem hinweisen, sind **Veraltet** und **Nicht abgedeckt** — und beide betreffen das Veralten des Walkthroughs selbst, nicht Ihren Code. Siehe unten.
|
||||
|
||||
## Was es prüfen kann
|
||||
|
||||
| Bereich | Was enthalten ist |
|
||||
|
||||
@@ -1,22 +1,63 @@
|
||||
---
|
||||
title: Desktop Browser
|
||||
description: Browse any page inside the desktop app, with inspect and console capture.
|
||||
title: Browser Panel
|
||||
description: Browse any page inside the app, annotate it, and let the agent drive it.
|
||||
---
|
||||
|
||||
# Desktop Browser
|
||||
# Browser Panel
|
||||
|
||||
The desktop app has a built-in browser so you can open any page right next to your chat, point at elements to ask about them, and capture the page's console. Open it from the globe button in the app header.
|
||||
The browser panel opens any page right next to your chat. Open it from the globe button in the app header.
|
||||
|
||||
> The desktop browser is a **desktop-only** feature. On the web, the [preview](/preview/) panel offers the same inspect-and-console tools for your local dev server.
|
||||
On the desktop app it is a real browser: your logins persist, hot reload works, and developer tools are one click away. In a web browser tab the panel can still display a page, but it cannot look inside one — the annotation tools below are desktop-only. The VS Code extension has no browser panel at all: VS Code is already an editor with a browser beside it, and everything that makes this panel worth having needs the desktop app.
|
||||
|
||||
## Inspect and annotate
|
||||
Pages opened here cannot use your camera, microphone, or location: those requests are refused.
|
||||
|
||||
Turn on **inspect** and click any element on the page. OpenChamber captures a note about it — what it is, its styles, where it sits, and a screenshot — and attaches it to your chat message. It's the quickest way to tell the agent "this element, right here."
|
||||
## The toolbar
|
||||
|
||||
## Console capture
|
||||
The address bar remembers pages you have opened in this project and offers them as you type, matching part of an address or a page title. Arrow keys move through the list, Enter opens the highlighted entry, and the button on a row removes it.
|
||||
|
||||
The browser collects the page's console output — errors, warnings, and logs — so you can filter and read it without opening developer tools.
|
||||
**Reload** is next to it, along with a **hard reload** that ignores the cache when a change refuses to show up, and zoom controls that scale the page only.
|
||||
|
||||
**Clear cookies** and **Clear cached data** apply to this panel alone. Your OpenChamber session and any other window are untouched.
|
||||
|
||||
## Annotate a page
|
||||
|
||||
Press **Annotate** and a toolbar appears over the page with three tools:
|
||||
|
||||
- **Element** — click an element. Clicking another moves the selection; clicking the same one again clears it.
|
||||
- **Region** — drag a box around an area, when what you mean covers more than one element.
|
||||
- **Draw** — sketch freehand over the page.
|
||||
|
||||
Write what you want in the box that appears beside your mark, and press **Attach** — or just press Enter. Your chat message gets a card with everything you marked, your note, and a screenshot of the visible page with your marks drawn on it — so you can say "this button, a bit rounder" instead of describing where it is.
|
||||
|
||||
The page itself is never modified — annotating marks what is there. `Esc` cancels and closes the toolbar.
|
||||
|
||||
## Let the agent drive
|
||||
|
||||
The agent can use the browser panel itself — opening a page, reading what is on it, clicking, typing, scrolling, and switching between mobile, tablet and desktop layouts — so it can check its own work instead of asking you to. You will see it happening in the panel.
|
||||
|
||||
The agent cannot run arbitrary code in the page. The browser keeps your real logins, so it is limited to the specific actions above.
|
||||
|
||||
It can also save a picture of what it is looking at into `.openchamber/screenshots/` in your project and show it to you in its reply. That is what makes a before-and-after possible, and the file stays there afterwards to attach to a pull request.
|
||||
|
||||
The browser actions are the **OpenChamber Web tool**, which can be turned on and off on its own in **Settings → General → OpenChamber Tools**.
|
||||
|
||||
This needs the desktop app: a page shown in a web browser tab cannot be driven.
|
||||
|
||||
## Size and appearance
|
||||
|
||||
Press the phone button to open the device bar. Pick a preset or type a width and
|
||||
height, and the page is laid out at that size — scaled down to fit the panel
|
||||
when it is bigger, but still measuring itself at the size you asked for.
|
||||
|
||||
The same bar forces the page to light or dark, so a theme can be checked without
|
||||
changing anything on your machine. It leaves DevTools alone; a page can only
|
||||
have one debugger attached, so close DevTools first if it is open.
|
||||
|
||||
## Developer tools
|
||||
|
||||
Press the terminal button in the toolbar to open Chromium's own developer tools for the page — console, network, elements, everything you would expect.
|
||||
|
||||
## Related
|
||||
|
||||
- [Preview & Dev Servers](/preview/) — the same tools for your local dev server
|
||||
- [Preview & Dev Servers](/preview/) — opening your running app, including on a remote machine
|
||||
- [Agent Control Tool](/agent-control-tool/) — sessions, worktrees and scheduled tasks from chat
|
||||
|
||||
@@ -28,7 +28,7 @@ La herramienta puede listar proyectos y preferencias de modelos, crear y continu
|
||||
|
||||
## Activar o desactivar la herramienta
|
||||
|
||||
Abre **Ajustes → General → OpenCode CLI**, cambia **Herramienta de control para agentes** y selecciona **Save + Reload**. El ajuste se aplica cuando se reinicia el servidor OpenCode gestionado.
|
||||
Abre **Ajustes → General → Herramientas de OpenChamber** y cambia **Herramienta de control para agentes**. El ajuste se aplica cuando se reinicia el servidor OpenCode gestionado, que OpenChamber ofrece como **Apply & Restart**.
|
||||
|
||||
La herramienta no está disponible cuando OpenChamber se conecta a un servidor OpenCode externo mediante `OPENCODE_HOST` o skip-start, ni dentro de la extensión de VS Code. Las instalaciones web y de escritorio que usan el servidor OpenCode gestionado por OpenChamber la admiten automáticamente.
|
||||
|
||||
@@ -37,3 +37,4 @@ La herramienta no está disponible cuando OpenChamber se conecta a un servidor O
|
||||
- [Tareas programadas](/es/scheduled-tasks/)
|
||||
- [Sesiones de worktree](/es/worktrees/)
|
||||
- [Objetivos de sesión](/es/session-goals/)
|
||||
- [Panel del navegador](/es/desktop-browser/) — la herramienta OpenChamber Web, para ver una página y manejarla
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
---
|
||||
title: Navegador de escritorio
|
||||
description: Navega cualquier página dentro de la app de escritorio, con inspección y captura de consola.
|
||||
title: Panel del navegador
|
||||
description: Navega cualquier página dentro de la aplicación, anótala y deja que el agente la maneje.
|
||||
---
|
||||
|
||||
# Navegador de escritorio
|
||||
# Panel del navegador
|
||||
|
||||
La app de escritorio tiene un navegador integrado para que abras cualquier página justo al lado de tu chat, señales elementos para preguntar sobre ellos y captures la consola de la página. Ábrelo desde el botón del globo en el encabezado de la app.
|
||||
El panel del navegador abre cualquier página justo al lado del chat. Ábrelo con el botón del globo de la cabecera.
|
||||
|
||||
> El navegador de escritorio es una función **solo de escritorio**. En la web, el panel de [vista previa](/es/preview/) ofrece las mismas herramientas de inspección y consola para tu servidor de desarrollo local.
|
||||
En la aplicación de escritorio es un navegador de verdad: tus sesiones se mantienen, la recarga en caliente funciona y las herramientas de desarrollo están a un clic. En una pestaña del navegador el panel puede mostrar una página, pero no mirar dentro de ella: las herramientas de anotación de abajo son solo de escritorio.
|
||||
|
||||
## Inspecciona y anota
|
||||
Las páginas que abras aquí no pueden usar tu cámara, tu micrófono ni tu ubicación: esas peticiones se rechazan.
|
||||
|
||||
Activa **inspect** y haz clic en cualquier elemento de la página. OpenChamber captura una nota sobre él —qué es, sus estilos, dónde se sitúa y una captura de pantalla— y la adjunta a tu mensaje del chat. Es la forma más rápida de decirle al agente "este elemento, justo aquí".
|
||||
## La barra de herramientas
|
||||
|
||||
## Captura de consola
|
||||
La barra de direcciones recuerda las páginas que has abierto en este proyecto y las ofrece mientras escribes, buscando en parte de la dirección o del título de la página. Las flechas recorren la lista, Enter abre la entrada resaltada y el botón de una fila la quita.
|
||||
|
||||
El navegador recopila la salida de la consola de la página —errores, advertencias y registros— para que puedas filtrarla y leerla sin abrir las herramientas de desarrollo.
|
||||
Al lado está **Recargar**, junto con una **recarga forzada** que ignora la caché cuando un cambio se niega a aparecer, y los controles de zoom, que escalan solo la página.
|
||||
|
||||
**Borrar cookies** y **Borrar datos en caché** afectan únicamente a este panel. Tu sesión de OpenChamber y cualquier otra ventana quedan intactas.
|
||||
|
||||
## Anotar una página
|
||||
|
||||
Pulsa **Anotar** y aparecerá una barra sobre la página con tres herramientas:
|
||||
|
||||
- **Elemento** — haz clic en un elemento. Hacer clic en otro mueve la selección; volver a hacer clic en el mismo la quita.
|
||||
- **Región** — arrastra un recuadro alrededor de una zona cuando te refieras a más de un elemento.
|
||||
- **Dibujar** — traza a mano alzada sobre la página.
|
||||
|
||||
Escribe lo que quieres en el cuadro que aparece junto a tu marca y pulsa **Adjuntar**, o simplemente Enter. Tu mensaje recibe una tarjeta con todo lo que has marcado, tu nota y una captura de la página visible con tus marcas dibujadas encima — así puedes decir "este botón, un poco más redondeado" en vez de describir dónde está.
|
||||
|
||||
La página en sí nunca se modifica: anotar solo marca lo que ya está ahí. `Esc` cancela y cierra la barra.
|
||||
|
||||
## Dejar que el agente maneje
|
||||
|
||||
El agente puede usar el panel del navegador por su cuenta — abrir una página, leer lo que hay en ella, hacer clic, escribir, desplazarse y alternar entre diseño móvil, de tableta y de escritorio — para comprobar su propio trabajo en lugar de pedírtelo a ti. Lo verás ocurrir en el panel.
|
||||
|
||||
El agente no puede ejecutar código arbitrario en la página. El navegador conserva tus sesiones reales, así que se limita a las acciones anteriores.
|
||||
|
||||
También puede guardar una imagen de lo que está viendo en `.openchamber/screenshots/` de tu proyecto y mostrártela en su respuesta. Eso es lo que hace posible un antes y después, y el archivo se queda ahí para adjuntarlo a un pull request.
|
||||
|
||||
Las acciones del navegador son la **herramienta OpenChamber Web**, que se activa y desactiva por separado en **Ajustes → General → Herramientas de OpenChamber**.
|
||||
|
||||
Esto necesita la aplicación de escritorio: una página mostrada en una pestaña del navegador no se puede controlar.
|
||||
|
||||
## Herramientas de desarrollo
|
||||
|
||||
Pulsa el botón de terminal de la barra para abrir las herramientas de desarrollo propias de Chromium — consola, red, elementos, todo lo habitual.
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Vista previa y servidores de desarrollo](/es/preview/) — las mismas herramientas para tu servidor de desarrollo local
|
||||
- [Vista previa y servidores de desarrollo](/preview/) — abrir tu aplicación en marcha, también en una máquina remota
|
||||
- [Herramienta de control para agentes](/es/agent-control-tool/) — sesiones, worktrees y tareas programadas desde el chat
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Integraciones
|
||||
description: Usa tu suscripción de Claude o Cursor como proveedor.
|
||||
---
|
||||
|
||||
# Integraciones
|
||||
|
||||
Una integración es un pequeño plugin que añade un proveedor a OpenChamber usando una suscripción que ya tienes. Las gestionas en **Settings → Integrations**.
|
||||
|
||||
> **Función experimental.** Buscamos respetar las políticas de los proveedores, pero las restricciones y suspensiones de cuentas son decisión de cada proveedor. Usa las integraciones bajo tu propia responsabilidad.
|
||||
|
||||
Integraciones disponibles:
|
||||
|
||||
- **Claude Code** — tu plan Claude Pro o Max, sin claves de API
|
||||
- **Cursor** — los límites de modelos de tu plan de Cursor
|
||||
|
||||
## Instalar una integración
|
||||
|
||||
1. Abre **Settings → Integrations**.
|
||||
2. Busca la integración y elige **Install**.
|
||||
3. Reinicia OpenCode cuando se te pida — el proveedor aparece tras el reinicio.
|
||||
4. Elige **Set up** e inicia sesión. Los modelos aparecerán luego en el selector de modelos del chat.
|
||||
|
||||
Las integraciones se instalan para tu usuario, así que funcionan en todos los proyectos. Puedes actualizarlas o eliminarlas desde la misma tarjeta en cualquier momento.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code usa tu plan Claude Pro o Max — sin claves de API y sin una app de Claude aparte.
|
||||
|
||||
1. Instala la integración (arriba).
|
||||
2. Elige **Set up** e inicia sesión. Si aún no tienes la CLI de Claude Code, la configuración ofrece instalarla primero y luego iniciar sesión.
|
||||
|
||||
Claude Code es la única integración de esta página que requiere tener la CLI de su proveedor instalada y con sesión iniciada. Cursor no requiere su CLI.
|
||||
|
||||
**Cómo se protege tu cuenta de Claude:** esta integración usa el Claude Agent SDK oficial de Anthropic y tu CLI de Claude Code instalada. No secuestra OAuth, no extrae ni reutiliza tokens del navegador, no se hace pasar por un cliente no admitido ni omite la autenticación de Anthropic. Se mantiene en la vía de acceso admitida por Anthropic, por lo que no conlleva el riesgo de baneo asociado al secuestro de tokens o a rodeos de autenticación no autorizados.
|
||||
|
||||
## Cursor
|
||||
|
||||
Cursor hace disponibles en OpenChamber los modelos incluidos en tu plan de Cursor.
|
||||
|
||||
1. Instala la integración (arriba).
|
||||
2. Elige **Set up**, abre el enlace y autoriza el acceso en tu navegador. No necesitas clave de API. La lista de modelos se carga automáticamente tras iniciar sesión.
|
||||
|
||||
## Actualizar o eliminar
|
||||
|
||||
- **Update** instala la última versión publicada del plugin.
|
||||
- **Remove** elimina el plugin de tu configuración de OpenCode. El proveedor deja de cargarse cuando OpenCode se recarga.
|
||||
|
||||
Si una tarjeta indica que las entradas requieren gestión manual, elige **Manage plugins** y limpia ahí los duplicados.
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Proveedores, modelos y agentes](/es/providers/) — conecta otros proveedores y elige modelos
|
||||
- [Uso y cuotas](/es/usage/) — sigue cuánto has usado
|
||||
@@ -21,6 +21,64 @@ Algunos prompts tienen una parte visible (el mensaje que verías) y una parte de
|
||||
|
||||
¿Cambiaste de opinión? Cada prompt tiene **reset to default**, y hay un **reset all** si quieres empezar de cero en todas partes.
|
||||
|
||||
## Dónde se usa cada prompt
|
||||
|
||||
Cada prompt de las tablas indica dónde se ejecuta y qué lo dispara. Revisa el disparador antes de editar, para saber qué flujo estás cambiando.
|
||||
|
||||
### Git
|
||||
|
||||
| Prompt | Dónde se ejecuta | Cuándo se dispara |
|
||||
| --- | --- | --- |
|
||||
| Generación de commit | El botón de generar en el cuadro de commit de la vista git, y la pantalla Changes en móvil | Generas un mensaje de commit. Se rellenan los archivos seleccionados y los asuntos de los commits recientes de la rama, para que el mensaje siga el estilo de tu repositorio. |
|
||||
| Generación de PR | El formulario de creación de pull request en la pestaña PR de la vista git | Generas el título y el cuerpo de un PR. Se rellenan las ramas base y head, los commits y archivos cambiados entre ambas, tu contexto adicional y la plantilla de PR del repositorio si existe. |
|
||||
| Resolución de conflicto de merge/rebase | El diálogo de conflictos en la vista git, cuando un merge o rebase se detiene por conflictos | Eliges "Resolve in current session" o "Resolve in new session". El agente lee los archivos en conflicto, propone una estrategia por archivo y espera tu confirmación antes de editar, hacer stage o continuar la operación. |
|
||||
| Resolución de conflicto de cherry-pick | La sección "Re-integrate commits" de una sesión en worktree | Mover los commits de la sesión a la rama destino produce un conflicto y se lo pasas al agente. El agente resuelve dentro del worktree temporal, hace stage de los archivos y continúa el cherry-pick. |
|
||||
|
||||
### GitHub
|
||||
|
||||
| Prompt | Dónde se ejecuta | Cuándo se dispara |
|
||||
| --- | --- | --- |
|
||||
| Revisión de PR | El selector "Link GitHub PR" en el menú de adjuntos del composer, y el diálogo de nuevo worktree | Dos disparadores. Adjuntar un PR como contexto prepara las instrucciones, que se envían con tu siguiente mensaje. Crear una sesión de worktree desde un PR usa el prompt como primer mensaje de esa sesión, con el contexto completo del PR adjunto. |
|
||||
| Revisión de issue | El diálogo de nuevo worktree, cuando el worktree parte de una issue | El primer mensaje de la nueva sesión revisa la issue, con su cuerpo y comentarios adjuntos como contexto. |
|
||||
| Revisión de checks fallidos / comentarios de PR / comentario único de PR | — | Hoy no los envía ningún flujo. La vista de PR antes los disparaba con acciones de revisión de un clic; ahora los checks fallidos y los comentarios se fijan como borradores de contexto del chat. Siguen siendo editables para que las anulaciones existentes sigan funcionando. |
|
||||
|
||||
### Planning
|
||||
|
||||
| Prompt | Dónde se ejecuta | Cuándo se dispara |
|
||||
| --- | --- | --- |
|
||||
| Planificación desde todo | El panel Todos en la barra lateral del proyecto | Envías un todo a una sesión o a una nueva sesión en worktree. El texto del todo se convierte en el mensaje visible; las instrucciones lo convierten en un diálogo de planificación con preguntas en vez de saltar a implementar. |
|
||||
| Mejorar plan | La acción "Improve" sobre un plan guardado en la vista Plans | Envías un plan guardado al flujo de mejora. El agente lee primero el archivo del plan, luego propone cambios basados en el estado actual del repositorio y se ofrece a editar ese mismo archivo. |
|
||||
| Implementar plan | La acción "Implement" sobre un plan guardado | Envías un plan guardado al flujo de implementación. El agente lee el archivo del plan y lo implementa de principio a fin sin ampliar el alcance, y guarda ajustes del plan en el archivo cuando el propio plan resulta estar mal. |
|
||||
|
||||
### Session
|
||||
|
||||
La mayoría alimentan comandos de barra que se escriben en el composer. La mayoría también aparecen como chips de inicio en el borrador de una sesión nueva.
|
||||
|
||||
| Prompt | Dónde se ejecuta | Cuándo se dispara |
|
||||
| --- | --- | --- |
|
||||
| Tour del código | `/explore` | Pides una orientación general del código. |
|
||||
| Resumen de sesión | `/summary`, opcionalmente `/summary <tema>` | Resumes la conversación hasta ahora, útil para pasar a una sesión nueva. Requiere una sesión existente. |
|
||||
| Revisión del workspace | `/workspace-review` | Pides al agente revisar el diff actual del workspace en cuanto a intención, corrección y seguridad. |
|
||||
| Planificación de feature | `/plan-feature` | Conviertes una idea rough de feature en un plan de implementación mediante un diálogo guiado de preguntas y respuestas. |
|
||||
| Definir Goal | `/craft-goal`, opcionalmente `/craft-goal <idea>` | Conviertes una idea en un objetivo Goal verificable para el diálogo de Goal. |
|
||||
| Ponerse al día | `/catch-up` | Vuelves a un proyecto y preguntas en qué quedó y qué seguir. |
|
||||
| Depuración | `/debug` | Investigas un bug: el agente forma hipótesis, confirma la causa raíz desde el código y solo entonces propone un arreglo. |
|
||||
| Sopesar opciones | `/weigh` | Sabes qué construir pero no cómo. El agente compara dos o tres enfoques y recomienda uno. |
|
||||
| Fusion | La acción "Run fusion" sobre un grupo de multi-run | Combinas los resultados de varias ejecuciones en una respuesta. Los resultados se añaden después de las instrucciones. |
|
||||
|
||||
### Prompts sin página en Settings
|
||||
|
||||
Algunos prompts se disparan automáticamente y no tienen página editable en Settings:
|
||||
|
||||
| Prompt | Cuándo se dispara |
|
||||
| --- | --- |
|
||||
| Tarea programada | `/schedule-task`, opcionalmente con una idea inicial. Guía el diálogo que define una tarea programada. |
|
||||
| Handoff de revisión | `/handoff-review`, o el botón Review en la vista de diff con el handoff activado. Genera el handoff en la sesión de trabajo. |
|
||||
| Mensaje inicial de la sesión de revisión | El primer mensaje de la sesión de revisión generada, con el handoff cuando se produjo, o sin él. |
|
||||
| Feedback de revisión / respuesta de implementación | Llevan mensajes entre las dos sesiones: el feedback del revisor vuelve a la sesión que implementa, y la respuesta del implementador regresa a la sesión de revisión. |
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Flujos de trabajo de Git y GitHub](/es/git/) — muchos de estos prompts impulsan los flujos de git
|
||||
- [Notas, todos y planes](/es/notes-todos-plans/) — los todos y planes detrás de los prompts de Planning
|
||||
- [Multi-run](/es/multi-run/) — grupos de ejecución y fusion
|
||||
|
||||
@@ -5,28 +5,31 @@ description: Abre un servidor de desarrollo en marcha dentro de OpenChamber.
|
||||
|
||||
# Vista previa y servidores de desarrollo
|
||||
|
||||
Cuando inicias un servidor de desarrollo, OpenChamber puede abrirlo dentro de la propia app en lugar de en una pestaña de navegador aparte, para que veas tu sitio junto al chat, captures su consola y señales elementos para preguntar sobre ellos.
|
||||
Cuando arrancas un servidor de desarrollo, OpenChamber puede abrirlo dentro de la propia aplicación en lugar de en una pestaña aparte — así ves tu sitio junto al chat y puedes señalar elementos para preguntar por ellos.
|
||||
|
||||
## Abre una vista previa
|
||||
## Abrir un servidor de desarrollo
|
||||
|
||||
OpenChamber observa la salida de la terminal en busca de una dirección local (la línea `Local:` que imprimen herramientas como Vite, Next.js o Astro). Cuando detecta una:
|
||||
Abre el panel del navegador con el botón del globo de la cabecera. Si ya hay un servidor en marcha, aparece en la lista y se abre con un clic: OpenChamber lo encuentra mirando qué está escuchando de verdad en tu máquina, así que funciona sin importar cómo lo hayas arrancado.
|
||||
|
||||
- en la terminal aparece un botón **Open preview**
|
||||
- una [acción de proyecto](/es/project-actions/) con la apertura automática activada la abre por ti
|
||||
- un enlace local en un mensaje del chat también puede abrirla
|
||||
Un servidor de desarrollo también se abre solo cuando:
|
||||
|
||||
El sitio se carga en el panel lateral. Solo se pueden previsualizar direcciones locales (en tu propia máquina).
|
||||
- pulsas **Abrir vista previa** sobre una dirección local en la terminal
|
||||
- una [acción de proyecto](/project-actions/) con apertura automática arranca uno
|
||||
- sigues un enlace local en un mensaje del chat
|
||||
|
||||
## Consola e inspección
|
||||
Siempre puedes escribir la dirección a mano. Un simple `localhost:5173` se entiende como `http://`, así que no hace falta escribir el esquema.
|
||||
|
||||
En el panel de vista previa puedes:
|
||||
## Trabajar con un OpenChamber remoto
|
||||
|
||||
- ver la **consola** de la página —errores, advertencias y registros— filtrada como prefieras
|
||||
- activar **inspect**, hacer clic en cualquier elemento y enviar una nota sobre él —selector, estilos, posición y una captura de pantalla— directamente al chat
|
||||
Cuando OpenChamber corre en otra máquina, su servidor de desarrollo está en *esa* máquina — `localhost` en tu portátil apunta a otro sitio completamente distinto. La aplicación de escritorio se encarga: abre un puerto local que lleva la conexión hasta el servidor remoto, de modo que la página carga con normalidad, con recarga en caliente y herramientas de desarrollo funcionando. Tú sigues escribiendo la dirección que esperas; la fontanería no te estorba.
|
||||
|
||||
Esta es la forma más rápida de decirle al agente "este botón, aquí" sin describirlo.
|
||||
Esto requiere la aplicación de escritorio. En una pestaña del navegador solo se pueden abrir servidores de tu propia máquina.
|
||||
|
||||
## Anotar la página
|
||||
|
||||
Consulta [Panel del navegador](/desktop-browser/) para señalar elementos, dibujar sobre la página y enviarlo todo al chat.
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Acciones de proyecto](/es/project-actions/) — abre automáticamente un servidor al iniciarlo
|
||||
- [Navegador de escritorio](/es/desktop-browser/) — las mismas herramientas para cualquier página, en el escritorio
|
||||
- [Acciones de proyecto](/project-actions/) — abrir un servidor automáticamente al arrancarlo
|
||||
- [Panel del navegador](/desktop-browser/) — anotar páginas y dejar que el agente las maneje
|
||||
|
||||
@@ -45,5 +45,6 @@ Los inicios de sesión de los proveedores los guarda OpenCode, no OpenChamber, a
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Integraciones](/es/integrations/) — usa una suscripción de Claude o Cursor como proveedor
|
||||
- [Servidores MCP](/es/mcp/) — añade herramientas extra para los agentes
|
||||
- [Uso y cuotas](/es/usage/) — controla cuánto has consumido
|
||||
|
||||
@@ -12,7 +12,7 @@ Para escribir tus propias skills, consulta [Skills](/es/skills/).
|
||||
## Instala una skill
|
||||
|
||||
1. Abre el catálogo.
|
||||
2. Explora las fuentes integradas —el repositorio de skills de Anthropic y el registro comunitario de ClawdHub— o busca.
|
||||
2. Explora las fuentes integradas —como el repositorio de skills de Anthropic— o busca.
|
||||
3. Elige una skill e instálala.
|
||||
4. Elige dónde instalarla: para todo lo que hagas, o solo en el proyecto actual.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ L’outil peut répertorier les projets et les préférences de modèles, créer
|
||||
|
||||
## Activer ou désactiver l’outil
|
||||
|
||||
Ouvrez **Paramètres → Général → OpenCode CLI**, modifiez **Outil de contrôle pour les agents**, puis sélectionnez **Save + Reload**. Le réglage s’applique après le redémarrage du serveur OpenCode géré.
|
||||
Ouvrez **Paramètres → Général → Outils OpenChamber** et modifiez **Outil de contrôle pour les agents**. Le réglage s’applique au redémarrage du serveur OpenCode géré, que OpenChamber propose sous **Apply & Restart**.
|
||||
|
||||
L’outil n’est pas disponible quand OpenChamber se connecte à un serveur OpenCode externe avec `OPENCODE_HOST` ou skip-start, ni dans l’extension VS Code. Les installations desktop et web utilisant le serveur OpenCode géré par OpenChamber le prennent automatiquement en charge.
|
||||
|
||||
@@ -37,3 +37,4 @@ L’outil n’est pas disponible quand OpenChamber se connecte à un serveur Ope
|
||||
- [Tâches planifiées](/fr/scheduled-tasks/)
|
||||
- [Sessions worktree](/fr/worktrees/)
|
||||
- [Objectifs de session](/fr/session-goals/)
|
||||
- [Panneau navigateur](/fr/desktop-browser/) — l'outil OpenChamber Web, pour consulter une page et la piloter
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
---
|
||||
title: Navigateur desktop
|
||||
description: Parcourez n’importe quelle page dans l’application desktop, avec inspection et capture de console.
|
||||
title: Panneau navigateur
|
||||
description: Parcourez n'importe quelle page dans l'application, annotez-la et laissez l'agent la piloter.
|
||||
---
|
||||
|
||||
# Navigateur desktop
|
||||
# Panneau navigateur
|
||||
|
||||
L’application desktop possède un navigateur intégré pour ouvrir n’importe quelle page juste à côté de votre chat, pointer des éléments pour poser des questions à leur sujet et capturer la console de la page. Ouvrez-le avec le bouton globe dans l’en-tête de l’application.
|
||||
Le panneau navigateur ouvre n'importe quelle page juste à côté de votre discussion. Ouvrez-le depuis le bouton globe de l'en-tête.
|
||||
|
||||
> Le navigateur desktop est une fonctionnalité **desktop uniquement**. Sur le web, le panneau [aperçu](/preview/) offre les mêmes outils d’inspection et de console pour votre serveur de dev local.
|
||||
Dans l'application de bureau, c'est un vrai navigateur : vos connexions persistent, le rechargement à chaud fonctionne et les outils de développement sont à un clic. Dans un onglet de navigateur, le panneau affiche bien une page mais ne peut pas regarder à l'intérieur — les outils d'annotation ci-dessous sont réservés au bureau.
|
||||
|
||||
## Inspecter et annoter
|
||||
Les pages ouvertes ici ne peuvent pas utiliser votre caméra, votre micro ni votre position : ces demandes sont refusées.
|
||||
|
||||
Activez **inspect** et cliquez sur n’importe quel élément de la page. OpenChamber capture une note à son sujet — ce que c’est, ses styles, sa position et une capture d’écran — puis l’attache à votre message de chat. C’est le moyen le plus rapide de dire à l’agent « cet élément, juste ici ».
|
||||
## La barre d'outils
|
||||
|
||||
## Capture de console
|
||||
La barre d'adresse retient les pages que vous avez ouvertes dans ce projet et les propose pendant la saisie, en cherchant dans une partie de l'adresse ou du titre de la page. Les flèches parcourent la liste, Entrée ouvre l'entrée surlignée, et le bouton d'une ligne la retire.
|
||||
|
||||
Le navigateur collecte la sortie console de la page — erreurs, avertissements et logs — pour que vous puissiez la filtrer et la lire sans ouvrir les outils développeur.
|
||||
À côté se trouve **Recharger**, ainsi qu'un **rechargement forcé** qui ignore le cache quand un changement refuse d'apparaître, et des commandes de zoom qui agrandissent la page seule.
|
||||
|
||||
## Pages liées
|
||||
**Effacer les cookies** et **Effacer les données en cache** ne concernent que ce panneau. Votre session OpenChamber et les autres fenêtres n'y touchent pas.
|
||||
|
||||
- [Aperçu et serveurs de dev](/preview/) — les mêmes outils pour votre serveur de dev local
|
||||
## Annoter une page
|
||||
|
||||
Appuyez sur **Annoter** : une barre apparaît au-dessus de la page avec trois outils.
|
||||
|
||||
- **Élément** — cliquez sur un élément. Cliquer sur un autre déplace la sélection ; recliquer sur le même la retire.
|
||||
- **Zone** — tracez un cadre autour d'une portion lorsque votre remarque porte sur plusieurs éléments.
|
||||
- **Dessin** — croquez à main levée par-dessus la page.
|
||||
|
||||
Écrivez votre demande dans le champ qui apparaît à côté de votre marque, puis appuyez sur **Joindre** — ou simplement sur Entrée. Votre message reçoit une carte avec tout ce que vous avez marqué, votre note et une capture de la page visible avec vos marques dessinées dessus — vous pouvez donc dire « ce bouton, un peu plus arrondi » au lieu de décrire où il se trouve.
|
||||
|
||||
La page elle-même n'est jamais modifiée : annoter ne fait que marquer ce qui s'y trouve. `Échap` annule et ferme la barre.
|
||||
|
||||
## Laisser l'agent piloter
|
||||
|
||||
L'agent peut se servir lui-même du panneau navigateur — ouvrir une page, lire ce qu'elle contient, cliquer, saisir du texte, faire défiler et basculer entre les mises en page mobile, tablette et bureau — afin de vérifier son propre travail plutôt que de vous le demander. Vous le voyez faire dans le panneau.
|
||||
|
||||
L'agent ne peut pas exécuter de code arbitraire dans la page. Le navigateur conserve vos vraies connexions ; il s'en tient donc aux actions ci-dessus.
|
||||
|
||||
Il peut aussi enregistrer une image de ce qu'il regarde dans `.openchamber/screenshots/` de votre projet et vous la montrer dans sa réponse. C'est ce qui rend un avant-après possible, et le fichier reste ensuite disponible pour l'attacher à une pull request.
|
||||
|
||||
Les actions du navigateur constituent l'**outil OpenChamber Web**, qui s'active et se désactive séparément dans **Réglages → Général → Outils OpenChamber**.
|
||||
|
||||
Cela nécessite l'application de bureau : une page affichée dans un onglet de navigateur ne peut pas être pilotée.
|
||||
|
||||
## Outils de développement
|
||||
|
||||
Appuyez sur le bouton terminal de la barre pour ouvrir les outils de développement de Chromium pour la page — console, réseau, éléments, tout ce à quoi vous vous attendez.
|
||||
|
||||
## Voir aussi
|
||||
|
||||
- [Aperçu et serveurs de développement](/preview/) — ouvrir votre application en cours, y compris sur une machine distante
|
||||
- [Outil de contrôle pour les agents](/fr/agent-control-tool/) — sessions, worktrees et tâches planifiées depuis la discussion
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Intégrations
|
||||
description: Utilise ton abonnement Claude ou Cursor comme fournisseur.
|
||||
---
|
||||
|
||||
# Intégrations
|
||||
|
||||
Une intégration est un petit plugin qui ajoute un fournisseur à OpenChamber à partir d'un abonnement que tu possèdes déjà. Tu les gères dans **Settings → Integrations**.
|
||||
|
||||
> **Fonctionnalité expérimentale.** Nous cherchons à respecter les règles des fournisseurs, mais les restrictions et suspensions de compte relèvent de leur décision. Utilise les intégrations à tes risques.
|
||||
|
||||
Intégrations disponibles :
|
||||
|
||||
- **Claude Code** — ton plan Claude Pro ou Max, sans clés API
|
||||
- **Cursor** — les limites de modèles de ton plan Cursor
|
||||
|
||||
## Installer une intégration
|
||||
|
||||
1. Ouvre **Settings → Integrations**.
|
||||
2. Trouve l'intégration et choisis **Install**.
|
||||
3. Redémarre OpenCode quand c'est demandé — le fournisseur apparaît après le redémarrage.
|
||||
4. Choisis **Set up** et connecte-toi. Les modèles apparaissent ensuite dans le sélecteur de modèles du chat.
|
||||
|
||||
Les intégrations s'installent pour ton utilisateur et fonctionnent donc dans tous les projets. Tu peux les mettre à jour ou les retirer à tout moment depuis la même carte.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code utilise ton plan Claude Pro ou Max — sans clés API et sans application Claude séparée.
|
||||
|
||||
1. Installe l'intégration (ci-dessus).
|
||||
2. Choisis **Set up** et connecte-toi. Si tu n'as pas encore la CLI Claude Code, la configuration propose de l'installer d'abord, puis de te connecter.
|
||||
|
||||
Claude Code est la seule intégration ici qui exige que la CLI de son fournisseur soit installée et connectée. Cursor n'exige pas sa CLI.
|
||||
|
||||
**Comment ton compte Claude reste protégé :** cette intégration utilise le Claude Agent SDK officiel d'Anthropic et ta CLI Claude Code installée. Elle ne détourne pas l'OAuth, n'extrait ni rejoue de tokens de navigateur, ne se fait pas passer pour un client non pris en charge et ne contourne pas l'authentification d'Anthropic. Elle reste sur la voie d'accès prise en charge par Anthropic et ne porte donc pas le risque de bannissement associé au détournement de tokens ou aux contournements d'authentification non autorisés.
|
||||
|
||||
## Cursor
|
||||
|
||||
Cursor rend disponibles dans OpenChamber les modèles inclus dans ton plan Cursor.
|
||||
|
||||
1. Installe l'intégration (ci-dessus).
|
||||
2. Choisis **Set up**, ouvre le lien et autorise l'accès dans ton navigateur. Aucune clé API n'est nécessaire. La liste des modèles se charge automatiquement après la connexion.
|
||||
|
||||
## Mettre à jour ou retirer
|
||||
|
||||
- **Update** installe la dernière version publiée du plugin.
|
||||
- **Remove** supprime le plugin de ta configuration OpenCode. Le fournisseur cesse d'être chargé au prochain rechargement d'OpenCode.
|
||||
|
||||
Si une carte indique que les entrées nécessitent une gestion manuelle, choisis **Manage plugins** et nettoie les doublons à cet endroit.
|
||||
|
||||
## À voir aussi
|
||||
|
||||
- [Fournisseurs, modèles et agents](/fr/providers/) — connecter d'autres fournisseurs et choisir des modèles
|
||||
- [Utilisation et quotas](/fr/usage/) — suis ta consommation
|
||||
@@ -21,6 +21,64 @@ Certains prompts ont une partie visible (le message que vous verriez) et une par
|
||||
|
||||
Vous avez changé d’avis ? Chaque prompt possède **reset to default**, et il existe aussi **reset all** si vous voulez tout reprendre depuis le début.
|
||||
|
||||
## Où chaque prompt est utilisé
|
||||
|
||||
Chaque prompt ci-dessous indique où il s’exécute et ce qui le déclenche. Vérifiez le déclencheur avant de modifier, pour savoir quel flux vous changez.
|
||||
|
||||
### Git
|
||||
|
||||
| Prompt | Où il s’exécute | Quand il se déclenche |
|
||||
| --- | --- | --- |
|
||||
| Génération de commit | Le bouton de génération dans la zone de commit de la vue git, et l’écran Changes sur mobile | Vous générez un message de commit. Les fichiers sélectionnés et les sujets des commits récents de la branche sont insérés, pour que le message respecte le style du dépôt. |
|
||||
| Génération de PR | Le formulaire de création de pull request dans l’onglet PR de la vue git | Vous générez le titre et le corps d’une PR. Sont insérés les branches base et head, les commits et fichiers modifiés entre elles, votre contexte additionnel et le modèle de PR du dépôt s’il existe. |
|
||||
| Résolution de conflit merge/rebase | Le dialogue de conflits dans la vue git, quand un merge ou un rebase s’arrête sur des conflits | Vous choisissez « Resolve in current session » ou « Resolve in new session ». L’agent lit les fichiers en conflit, propose une stratégie par fichier et attend votre confirmation avant de modifier, staging ou poursuivre l’opération. |
|
||||
| Résolution de conflit cherry-pick | La section « Re-integrate commits » d’une session en worktree | Le déplacement des commits de la session vers la branche cible rencontre un conflit et vous le confiez à l’agent. L’agent résout dans le worktree temporaire, stage les fichiers et poursuit le cherry-pick. |
|
||||
|
||||
### GitHub
|
||||
|
||||
| Prompt | Où il s’exécute | Quand il se déclenche |
|
||||
| --- | --- | --- |
|
||||
| Relecture de PR | Le sélecteur « Link GitHub PR » dans le menu de pièces jointes du composer, et le dialogue de nouveau worktree | Deux déclencheurs. Attacher une PR comme contexte prépare les instructions, envoyées avec votre prochain message. Créer une session de worktree depuis une PR utilise le prompt comme premier message de la session, avec le contexte complet de la PR. |
|
||||
| Relecture d’issue | Le dialogue de nouveau worktree, quand le worktree part d’une issue | Le premier message de la nouvelle session relit l’issue, avec son corps et ses commentaires attachés comme contexte. |
|
||||
| Relecture de checks échoués / commentaires de PR / commentaire unique de PR | — | Aucun flux ne les envoie aujourd’hui. La vue PR les déclenchait avant via des actions de relecture en un clic ; désormais les checks échoués et les commentaires s’épinglent comme brouillons de contexte de chat. Ils restent modifiables pour que les overrides existants continuent de fonctionner. |
|
||||
|
||||
### Planning
|
||||
|
||||
| Prompt | Où il s’exécute | Quand il se déclenche |
|
||||
| --- | --- | --- |
|
||||
| Planification depuis un todo | Le panneau Todos dans la barre latérale du projet | Vous envoyez un todo vers une session ou une nouvelle session en worktree. Le texte du todo devient le message visible ; les instructions en font un dialogue de planification guidé par des questions plutôt qu’un passage direct à l’implémentation. |
|
||||
| Améliorer un plan | L’action « Improve » sur un plan enregistré dans la vue Plans | Vous envoyez un plan enregistré dans le flux d’amélioration. L’agent lit d’abord le fichier du plan, propose ensuite des changements ancrés dans l’état actuel du dépôt et propose de modifier le même fichier. |
|
||||
| Implémenter un plan | L’action « Implement » sur un plan enregistré | Vous envoyez un plan enregistré dans le flux d’implémentation. L’agent lit le fichier du plan et l’implémente de bout en bout sans élargir le périmètre, enregistrant les ajustements dans le fichier quand le plan lui-même s’avère erroné. |
|
||||
|
||||
### Session
|
||||
|
||||
La plupart alimentent des commandes slash saisies dans le composer. La plupart apparaissent aussi comme chips de départ sur le brouillon d’une nouvelle session.
|
||||
|
||||
| Prompt | Où il s’exécute | Quand il se déclenche |
|
||||
| --- | --- | --- |
|
||||
| Tour du code | `/explore` | Vous demandez une vue d’ensemble du code. |
|
||||
| Résumé de session | `/summary`, éventuellement `/summary <sujet>` | Vous résumez la conversation en cours — utile pour passer à une nouvelle session. Nécessite une session existante. |
|
||||
| Relecture du workspace | `/workspace-review` | Vous demandez à l’agent de relire le diff actuel du workspace sous l’angle intention, correction et sécurité. |
|
||||
| Planification de fonctionnalité | `/plan-feature` | Vous transformez une idée grossière de fonctionnalité en plan d’implémentation via un dialogue guidé de questions-réponses. |
|
||||
| Formuler un Goal | `/craft-goal`, éventuellement `/craft-goal <idée>` | Vous transformez une idée en objectif Goal vérifiable pour le dialogue Goal. |
|
||||
| Se remettre dans le bain | `/catch-up` | Vous revenez sur un projet et demandez où en sont les choses et quoi reprendre. |
|
||||
| Débogage | `/debug` | Vous investiguez un bug : l’agent forme des hypothèses, confirme la cause racine dans le code et seulement ensuite propose un correctif. |
|
||||
| Peser les options | `/weigh` | Vous savez quoi construire mais pas comment. L’agent compare deux ou trois approches et en recommande une. |
|
||||
| Fusion | L’action « Run fusion » sur un groupe multi-run | Vous combinez les sorties de plusieurs exécutions en une réponse. Les sorties des exécutions sont ajoutées après les instructions. |
|
||||
|
||||
### Prompts sans page dans les Paramètres
|
||||
|
||||
Quelques prompts se déclenchent automatiquement et n’ont pas de page modifiable dans les Paramètres :
|
||||
|
||||
| Prompt | Quand il se déclenche |
|
||||
| --- | --- |
|
||||
| Tâche planifiée | `/schedule-task`, éventuellement avec une idée initiale. Guide le dialogue qui définit une tâche planifiée. |
|
||||
| Handoff de relecture | `/handoff-review`, ou le bouton Review dans la vue diff avec handoff activé. Génère le handoff dans la session de travail. |
|
||||
| Premier message de la session de relecture | Le message d’ouverture de la session de relecture générée — avec le handoff quand il a été produit, sans sinon. |
|
||||
| Retour de relecture / réponse d’implémentation | Font circuler les messages entre les deux sessions : le retour du relecteur revient vers la session qui implémente, et la réponse de l’implémenteur repart vers la session de relecture. |
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Workflows Git et GitHub](/git/) — beaucoup de ces prompts alimentent les flux git
|
||||
- [Notes, todos et plans](/notes-todos-plans/) — les todos et plans derrière les prompts Planning
|
||||
- [Multi-run](/multi-run/) — groupes d’exécution et fusion
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
---
|
||||
title: Aperçu et serveurs de dev
|
||||
description: Ouvrez un serveur de dev en cours d’exécution dans OpenChamber.
|
||||
title: Aperçu et serveurs de développement
|
||||
description: Ouvrez un serveur de développement en cours d'exécution dans OpenChamber.
|
||||
---
|
||||
|
||||
# Aperçu et serveurs de dev
|
||||
# Aperçu et serveurs de développement
|
||||
|
||||
Quand vous démarrez un serveur de dev, OpenChamber peut l’ouvrir directement dans l’application au lieu d’un onglet de navigateur séparé — vous voyez ainsi votre site à côté du chat, vous capturez sa console et vous pouvez pointer des éléments pour poser des questions à leur sujet.
|
||||
Quand vous lancez un serveur de développement, OpenChamber peut l'ouvrir directement dans l'application plutôt que dans un onglet séparé — vous voyez ainsi votre site à côté de la discussion et pouvez désigner des éléments pour poser des questions à leur sujet.
|
||||
|
||||
## Ouvrir un aperçu
|
||||
## Ouvrir un serveur de développement
|
||||
|
||||
OpenChamber surveille la sortie du terminal pour détecter une adresse locale (la ligne `Local:` affichée par des outils comme Vite, Next.js ou Astro). Quand il en trouve une :
|
||||
Ouvrez le panneau navigateur depuis le bouton globe de l'en-tête. Si un serveur tourne déjà, il apparaît dans la liste et un clic suffit à l'ouvrir : OpenChamber le repère à partir de ce qui écoute réellement sur votre machine, quelle que soit la façon dont vous l'avez lancé.
|
||||
|
||||
- dans le terminal, un bouton **Ouvrir l’aperçu** apparaît
|
||||
- une [action de projet](/project-actions/) avec l’ouverture automatique activée l’ouvre pour vous
|
||||
- un lien local dans un message de chat peut aussi l’ouvrir
|
||||
Un serveur s'ouvre également tout seul quand :
|
||||
|
||||
Le site se charge dans le panneau latéral. Seules les adresses locales (sur votre propre machine) peuvent être prévisualisées.
|
||||
- vous appuyez sur **Ouvrir l'aperçu** sur une adresse locale dans le terminal
|
||||
- une [action de projet](/project-actions/) avec ouverture automatique en démarre un
|
||||
- vous suivez un lien local dans un message de la discussion
|
||||
|
||||
## Console et inspection
|
||||
Vous pouvez toujours saisir l'adresse vous-même. Un simple `localhost:5173` est compris comme `http://`, inutile donc d'écrire le schéma.
|
||||
|
||||
Dans le panneau d’aperçu, vous pouvez :
|
||||
## Travailler avec un OpenChamber distant
|
||||
|
||||
- regarder la **console** de la page — erreurs, avertissements et logs, filtrés comme vous le voulez
|
||||
- activer **inspect**, cliquer sur n’importe quel élément et envoyer une note à son sujet — sélecteur, styles, position et capture d’écran — directement dans le chat
|
||||
Quand OpenChamber tourne sur une autre machine, son serveur de développement s'y trouve aussi — `localhost` sur votre portable désigne tout autre chose. L'application de bureau s'en charge : elle ouvre un port local qui achemine la connexion jusqu'au serveur distant, si bien que la page se charge normalement, avec rechargement à chaud et outils de développement fonctionnels. Vous continuez à saisir l'adresse attendue ; la tuyauterie reste hors de votre chemin.
|
||||
|
||||
C’est le moyen le plus rapide de dire à l’agent « ce bouton, ici » sans devoir le décrire.
|
||||
Cela nécessite l'application de bureau. Dans un onglet de navigateur, seuls les serveurs de votre propre machine peuvent être ouverts.
|
||||
|
||||
## Pages liées
|
||||
## Annoter la page
|
||||
|
||||
- [Actions de projet](/project-actions/) — ouvrir automatiquement un serveur quand vous le démarrez
|
||||
- [Navigateur desktop](/desktop-browser/) — les mêmes outils pour n’importe quelle page, sur desktop
|
||||
Voyez [Panneau navigateur](/desktop-browser/) pour désigner des éléments, dessiner sur la page et envoyer le tout dans la discussion.
|
||||
|
||||
## Voir aussi
|
||||
|
||||
- [Actions de projet](/project-actions/) — ouvrir automatiquement un serveur à son démarrage
|
||||
- [Panneau navigateur](/desktop-browser/) — annoter les pages et laisser l'agent les piloter
|
||||
|
||||
@@ -45,5 +45,6 @@ Les connexions aux fournisseurs sont stockées par OpenCode, pas OpenChamber ; e
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Intégrations](/integrations/) — utiliser un abonnement Claude ou Cursor comme fournisseur
|
||||
- [Serveurs MCP](/mcp/) — ajouter des outils supplémentaires aux agents
|
||||
- [Utilisation et quotas](/usage/) — suivre votre consommation
|
||||
|
||||
@@ -12,7 +12,7 @@ Pour écrire vos propres skills, voir [Skills](/skills/).
|
||||
## Installer un skill
|
||||
|
||||
1. Ouvrez le catalogue.
|
||||
2. Parcourez les sources intégrées — le dépôt de skills Anthropic et le registre communautaire ClawdHub — ou lancez une recherche.
|
||||
2. Parcourez les sources intégrées — comme le dépôt de skills Anthropic — ou lancez une recherche.
|
||||
3. Choisissez un skill et installez-le.
|
||||
4. Choisissez où l’installer : pour tout ce que vous faites, ou seulement pour le projet actuel.
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Integrations
|
||||
description: Use your Claude or Cursor subscription as a provider.
|
||||
---
|
||||
|
||||
# Integrations
|
||||
|
||||
An integration is a small plugin that adds a provider to OpenChamber using a subscription you already have. You manage them at **Settings → Integrations**.
|
||||
|
||||
> **Experimental feature.** We aim to respect provider policies, but account restrictions and suspensions remain each provider's decision. Use integrations at your own risk.
|
||||
|
||||
Available integrations:
|
||||
|
||||
- **Claude Code** — your Claude Pro or Max plan, no API keys
|
||||
- **Cursor** — the model limits of your Cursor plan
|
||||
|
||||
## Install an integration
|
||||
|
||||
1. Open **Settings → Integrations**.
|
||||
2. Find the integration and choose **Install**.
|
||||
3. Restart OpenCode when asked — the provider appears after the restart.
|
||||
4. Choose **Set up** and sign in. The models then appear in the chat model picker.
|
||||
|
||||
Integrations install for your user, so they work in every project. You can update or remove them from the same card at any time.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code uses your Claude Pro or Max plan — no API keys and no separate Claude app.
|
||||
|
||||
1. Install the integration (above).
|
||||
2. Choose **Set up** and sign in. If you don't have the Claude Code CLI yet, setup offers to install it first and then sign you in.
|
||||
|
||||
Claude Code is the only integration here that requires its provider CLI to be installed and signed in. Cursor does not require its CLI.
|
||||
|
||||
**How your Claude account stays safe:** this integration uses Anthropic's official Claude Agent SDK and your installed Claude Code CLI. It does not hijack OAuth, extract or replay browser tokens, impersonate an unsupported client, or bypass Anthropic's authentication flow. It stays on Anthropic's supported access path, so it does not carry the account-ban risk of token hijacking or unauthorized authentication workarounds.
|
||||
|
||||
## Cursor
|
||||
|
||||
Cursor makes the models included in your Cursor plan available in OpenChamber.
|
||||
|
||||
1. Install the integration (above).
|
||||
2. Choose **Set up**, open the link, and approve access in your browser. No API key is required. The model list loads automatically after you sign in.
|
||||
|
||||
## Update or remove
|
||||
|
||||
- **Update** installs the latest published version of the plugin.
|
||||
- **Remove** deletes the plugin from your OpenCode config. The provider stops loading after OpenCode refreshes.
|
||||
|
||||
If a card says the entries need manual management, choose **Manage plugins** and clean up the duplicates there.
|
||||
|
||||
## Related
|
||||
|
||||
- [Providers, Models & Agents](/providers/) — connect other providers and pick models
|
||||
- [Usage & Quotas](/usage/) — track how much you've used
|
||||
@@ -28,7 +28,7 @@ description: エージェントがチャットから OpenChamber のセッショ
|
||||
|
||||
## ツールを有効または無効にする
|
||||
|
||||
**設定 → 一般 → OpenCode CLI** を開き、**エージェント制御ツール**を変更して、**Save + Reload** を選択します。この設定は、管理対象の OpenCode サーバーが再起動した後に反映されます。
|
||||
**設定 → 一般 → OpenChamber ツール** を開き、**エージェント制御ツール**を変更します。この設定は、管理対象の OpenCode サーバーが再起動すると反映されます。再起動は OpenChamber が **Apply & Restart** として案内します。
|
||||
|
||||
OpenChamber が `OPENCODE_HOST` または skip-start で外部 OpenCode サーバーに接続している場合や、VS Code 拡張機能内では、このツールを利用できません。OpenChamber が管理する OpenCode サーバーを使用するデスクトップ版と Web 版では自動的に利用できます。
|
||||
|
||||
@@ -37,3 +37,4 @@ OpenChamber が `OPENCODE_HOST` または skip-start で外部 OpenCode サー
|
||||
- [スケジュールタスク](/ja/scheduled-tasks/)
|
||||
- [Worktree セッション](/ja/worktrees/)
|
||||
- [セッションゴール](/ja/session-goals/)
|
||||
- [ブラウザパネル](/ja/desktop-browser/) — ページを見て操作するための OpenChamber Web ツール
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
---
|
||||
title: デスクトップブラウザ
|
||||
description: デスクトップアプリ内で任意のページを開き、検査とコンソール取得を使います。
|
||||
title: ブラウザパネル
|
||||
description: アプリ内で任意のページを開き、注釈を付け、エージェントに操作させます。
|
||||
---
|
||||
|
||||
# デスクトップブラウザ
|
||||
# ブラウザパネル
|
||||
|
||||
デスクトップアプリには組み込みブラウザがあります。チャットのすぐ横で任意のページを開き、要素を指して質問したり、ページのコンソールを取得したりできます。アプリヘッダーの地球儀ボタンから開きます。
|
||||
ブラウザパネルはチャットのすぐ隣に任意のページを開きます。ヘッダーの地球儀ボタンから開いてください。
|
||||
|
||||
> デスクトップブラウザは**デスクトップ専用**機能です。Web では、[プレビュー](/preview/) パネルがローカル開発サーバー向けに同じ検査・コンソールツールを提供します。
|
||||
デスクトップアプリでは本物のブラウザです。ログイン状態は保持され、ホットリロードが動き、開発者ツールはワンクリックで開けます。ブラウザのタブでもページの表示はできますが、中を覗くことはできません。以下の注釈ツールはデスクトップ専用です。
|
||||
|
||||
## 検査して注釈を付ける
|
||||
ここで開いたページは、カメラ・マイク・位置情報を使えません。これらの要求は拒否されます。
|
||||
|
||||
**inspect** をオンにして、ページ上の任意の要素をクリックします。OpenChamber はその要素について、何であるか、スタイル、位置、スクリーンショットを含むメモを取得し、チャットメッセージに添付します。エージェントに「この要素、ここ」と伝える最短の方法です。
|
||||
## ツールバー
|
||||
|
||||
## コンソール取得
|
||||
アドレスバーはこのプロジェクトで開いたページを覚えていて、入力中に候補として出します。アドレスの一部でもページタイトルの一部でも一致します。矢印キーで候補を移動し、Enter で選択中の候補を開き、行のボタンでその候補を消せます。
|
||||
|
||||
ブラウザはページのコンソール出力(エラー、警告、ログ)を集めるので、開発者ツールを開かずにフィルターして読めます。
|
||||
隣には **再読み込み** があり、変更がどうしても反映されないときのためにキャッシュを無視する **強制再読み込み**、そしてページだけを拡大縮小するズーム操作も並びます。
|
||||
|
||||
**Cookie を消去** と **キャッシュを消去** はこのパネルにだけ効きます。OpenChamber のセッションや他のウィンドウには影響しません。
|
||||
|
||||
## ページに注釈を付ける
|
||||
|
||||
**注釈** を押すと、ページの上に3つのツールを備えたバーが表示されます。
|
||||
|
||||
- **要素** — 要素をクリックします。別の要素をクリックすると選択が移り、同じ要素をもう一度クリックすると解除されます。
|
||||
- **範囲** — 複数の要素にまたがる話をしたいときは、領域をドラッグで囲みます。
|
||||
- **描画** — ページの上にフリーハンドで描きます。
|
||||
|
||||
印の隣に現れる入力欄に希望を書き、**添付** を押します。Enter でも送れます。チャットメッセージには、印を付けた内容、あなたのメモ、表示中のページに印を描き込んだスクリーンショットを含むカードが付きます。「このボタン、もう少し角を丸く」と言えば済み、場所を説明する必要はありません。
|
||||
|
||||
ページ自体は変更されません。注釈はそこにあるものに印を付けるだけです。`Esc` で取り消してツールバーを閉じます。
|
||||
|
||||
## エージェントに操作させる
|
||||
|
||||
エージェントはブラウザパネルを自分で使えます。ページを開き、内容を読み、クリックし、文字を入力し、スクロールし、モバイル・タブレット・デスクトップのレイアウトを切り替えて、自分の作業をあなたに頼まず自分で確認します。その様子はパネルで見えます。
|
||||
|
||||
エージェントがページ内で任意のコードを実行することはできません。ブラウザは実際のログイン状態を保持しているため、上記の操作に限定されています。
|
||||
|
||||
見ている内容をプロジェクト内の `.openchamber/screenshots/` に画像として保存し、返答の中で見せることもできます。ビフォー・アフターができるのはこのためで、ファイルはその後もプルリクエストに添付できる形で残ります。
|
||||
|
||||
ブラウザ操作は **OpenChamber Web ツール** で、**設定 → 一般 → OpenChamber ツール** から単独でオン・オフできます。
|
||||
|
||||
これにはデスクトップアプリが必要です。ブラウザのタブに表示したページは操作できません。
|
||||
|
||||
## 開発者ツール
|
||||
|
||||
バーのターミナルボタンを押すと、そのページに対する Chromium 本来の開発者ツールが開きます。コンソール、ネットワーク、要素など、期待どおりのものがすべて使えます。
|
||||
|
||||
## 関連
|
||||
|
||||
- [プレビューと開発サーバー](/preview/) — ローカル開発サーバー向けの同じツール
|
||||
- [プレビューと開発サーバー](/preview/) — リモートマシン上のものも含め、実行中のアプリを開く
|
||||
- [エージェント制御ツール](/ja/agent-control-tool/) — チャットからセッション・worktree・スケジュールタスクを扱う
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: 統合機能
|
||||
description: Claude または Cursor のサブスクリプションをプロバイダーとして使う。
|
||||
---
|
||||
|
||||
# 統合機能
|
||||
|
||||
統合機能(インテグレーション)は、すでに持っているサブスクリプションを使って OpenChamber にプロバイダーを追加する小さなプラグインです。**Settings → Integrations** で管理します。
|
||||
|
||||
> **実験的な機能。** プロバイダーの方針を尊重するよう努めていますが、アカウントの制限や停止は各プロバイダーの判断に委ねられます。自己責任で連携を使用してください。
|
||||
|
||||
利用できる統合機能:
|
||||
|
||||
- **Claude Code** — Claude Pro または Max プラン、API キー不要
|
||||
- **Cursor** — Cursor プランのモデル利用枠
|
||||
|
||||
## 統合機能をインストールする
|
||||
|
||||
1. **Settings → Integrations** を開きます。
|
||||
2. 統合機能を見つけて **Install** を選びます。
|
||||
3. 求められたら OpenCode を再起動します — 再起動後にプロバイダーが現れます。
|
||||
4. **Set up** を選んでサインインします。するとチャットのモデル選択にモデルが表示されます。
|
||||
|
||||
統合機能はユーザー単位でインストールされるため、すべてのプロジェクトで使えます。同じカードからいつでも更新や削除ができます。
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code は Claude Pro または Max プランを使います — API キーも Claude アプリも不要です。
|
||||
|
||||
1. 統合機能をインストールします(上記)。
|
||||
2. **Set up** を選んでサインインします。Claude Code CLI がまだない場合は、セットアップがまずインストールを提案し、その後サインインします。
|
||||
|
||||
Claude Code は、ここで唯一プロバイダーの CLI のインストールとサインインを必要とする統合機能です。Cursor は CLI を必要としません。
|
||||
|
||||
**Claude アカウントが守られる仕組み:** この統合機能は Anthropic の公式 Claude Agent SDK と、インストール済みの Claude Code CLI を使用します。OAuth の乗っ取り、ブラウザートークンの抽出や再生、未対応クライアントへの偽装、Anthropic の認証フローの回避は一切行いません。Anthropic がサポートする正規のアクセス経路を使うため、トークン乗っ取りや不正な認証の回避につきもののアカウント停止リスクはありません。
|
||||
|
||||
## Cursor
|
||||
|
||||
Cursor は Cursor プランに含まれるモデルを OpenChamber で使えるようにします。
|
||||
|
||||
1. 統合機能をインストールします(上記)。
|
||||
2. **Set up** を選び、リンクを開いてブラウザーでアクセスを許可します。API キーは不要です。サインイン後、モデル一覧は自動的に読み込まれます。
|
||||
|
||||
## 更新と削除
|
||||
|
||||
- **Update** はプラグインの最新の公開バージョンをインストールします。
|
||||
- **Remove** は OpenCode の設定からプラグインを削除します。OpenCode が再読み込みされるとプロバイダーは読み込まれなくなります。
|
||||
|
||||
カードに手動での管理が必要だと表示された場合は、**Manage plugins** を選んで重複を整理してください。
|
||||
|
||||
## 関連情報
|
||||
|
||||
- [プロバイダー、モデル、エージェント](/ja/providers/) — 他のプロバイダーの接続とモデルの選択
|
||||
- [使用量とクォータ](/ja/usage/) — 利用量を追跡
|
||||
@@ -21,6 +21,64 @@ OpenChamber は、コミットメッセージの作成、PR の下書き、Issue
|
||||
|
||||
気が変わりましたか?各プロンプトには **reset to default** があり、すべてを最初からやり直したい場合は **reset all** もあります。
|
||||
|
||||
## 各プロンプトが使われる場所
|
||||
|
||||
以下の表は、各プロンプトがどこで実行され、何がきっかけで動くかを示します。編集前にトリガーを確認し、どのフローを変えるのかを把握してください。
|
||||
|
||||
### Git
|
||||
|
||||
| プロンプト | 実行される場所 | 動くタイミング |
|
||||
| --- | --- | --- |
|
||||
| コミット生成 | git ビューのコミット欄にある生成ボタン、およびモバイルの Changes 画面 | コミットメッセージを生成するとき。選択したファイルとブランチの直近コミットの件名が差し込まれ、メッセージがリポジトリの既存スタイルに合います。 |
|
||||
| PR 生成 | git ビュー PR タブの pull request 作成フォーム | PR のタイトルと本文を生成するとき。base と head ブランチ、その間のコミットと変更ファイル、追加コンテキスト、リポジトリに PR テンプレートがあればそれも差し込まれます。 |
|
||||
| merge/rebase コンフリクト解決 | merge や rebase がコンフリクトで止まったときの git ビューのコンフリクトダイアログ | "Resolve in current session" または "Resolve in new session" を選んだとき。エージェントはコンフリクトファイルを読み、ファイルごとの解決戦略を提案し、編集・stage・操作の再開の前に確認を待ちます。 |
|
||||
| cherry-pick コンフリクト解決 | worktree セッションの "Re-integrate commits" セクション | セッションのコミットを対象ブランチへ移す途中でコンフリクトが起き、エージェントに任せたとき。エージェントは一時 worktree の中で解決し、ファイルを stage して cherry-pick を続けます。 |
|
||||
|
||||
### GitHub
|
||||
|
||||
| プロンプト | 実行される場所 | 動くタイミング |
|
||||
| --- | --- | --- |
|
||||
| PR レビュー | composer の添付メニューにある "Link GitHub PR" ピッカー、および新規 worktree ダイアログ | 2 つのトリガー。PR をコンテキストとして添付すると instructions が用意され、次のメッセージと一緒に送られます。PR から worktree セッションを始めると、このプロンプトがそのセッションの最初のメッセージになり、PR の完全なコンテキストが添付されます。 |
|
||||
| Issue レビュー | Issue から worktree を作るときの新規 worktree ダイアログ | 新しいセッションの最初のメッセージが Issue をレビューし、本文とコメントがコンテキストとして添付されます。 |
|
||||
| PR の失敗チェック / PR コメント / 個別 PR コメント | — | 現在はどのフローからも送信されません。以前は PR ビューのワンクリックレビューアクションから起動されましたが、今は失敗チェックとコメントがチャットコンテキストの下書きとしてピン留めされます。既存のオーバーライドが機能し続けるよう、編集可能なまま残っています。 |
|
||||
|
||||
### Planning
|
||||
|
||||
| プロンプト | 実行される場所 | 動くタイミング |
|
||||
| --- | --- | --- |
|
||||
| todo からの計画 | プロジェクトサイドバーの Todos パネル | todo をセッションまたは新しい worktree セッションへ送るとき。todo のテキストが見えるメッセージになり、instructions は実装へ飛ばず、質問主体の計画対話に変えます。 |
|
||||
| 計画の改善 | Plans ビューの保存済み計画に対する "Improve" アクション | 保存済み計画を改善フローへ送るとき。エージェントはまず計画ファイルを読み、リポジトリの現在の状態に即した変更を提案し、同じファイルの編集を申し出ます。 |
|
||||
| 計画の実装 | 保存済み計画に対する "Implement" アクション | 保存済み計画を実装フローへ送るとき。エージェントは計画ファイルを読み、スコープを広げずに最後まで実装し、計画自体に誤りが見つかった場合は調整を同じファイルへ保存します。 |
|
||||
|
||||
### Session
|
||||
|
||||
これらの多くは、composer に入力するスラッシュコマンドとして動きます。多くは新しいセッションの下書き画面でスターターチップとしても表示されます。
|
||||
|
||||
| プロンプト | 実行される場所 | 動くタイミング |
|
||||
| --- | --- | --- |
|
||||
| コードベースツアー | `/explore` | コードベースの概要を把握したいとき。 |
|
||||
| セッション要約 | `/summary`、オプションで `/summary <トピック>` | ここまでの会話を要約します。新しいセッションへの引き継ぎに便利です。既存のセッションが必要です。 |
|
||||
| ワークスペースレビュー | `/workspace-review` | 現在のワークスペース差分を意図・正確性・セキュリティの観点でレビューしてほしいとき。 |
|
||||
| 機能計画 | `/plan-feature` | 大まかな機能アイデアを、質疑応答の対話を通じて実装計画に変えたいとき。 |
|
||||
| Goal 作成 | `/craft-goal`、オプションで `/craft-goal <アイデア>` | アイデアを、Goal ダイアログで使える検証可能な Goal 目標に変えたいとき。 |
|
||||
| キャッチアップ | `/catch-up` | プロジェクトに戻って、どこまで進んでいて次に何をするか知りたいとき。 |
|
||||
| デバッグ | `/debug` | バグを調査するとき。エージェントは仮説を立て、コードから根本原因を確認してから修正を提案します。 |
|
||||
| 選択肢の比較 | `/weigh` | 何を作るかは分かっているが作り方が分からないとき。エージェントが 2〜3 のアプローチを比較し、1 つを推奨します。 |
|
||||
| Fusion | multi-run グループの "Run fusion" アクション | 複数ランの出力を 1 つの回答にまとめるとき。ランの出力は instructions の後に続けて添付されます。 |
|
||||
|
||||
### Settings にページのないプロンプト
|
||||
|
||||
一部のプロンプトは自動的に動き、Settings には編集ページがありません:
|
||||
|
||||
| プロンプト | 動くタイミング |
|
||||
| --- | --- |
|
||||
| スケジュールタスク | `/schedule-task`、オプションで初期アイデアと一緒に。スケジュールタスクを定義する対話を進めます。 |
|
||||
| レビュー用ハンドオフ | `/handoff-review`、またはハンドオフを有効にした diff ビューの Review ボタン。作業セッション内でハンドオフを生成します。 |
|
||||
| レビューセッションの開始メッセージ | 生成されたレビューセッションの最初のメッセージ。ハンドオフが作られた場合はそれを含み、なければ含みません。 |
|
||||
| レビューフィードバック / 実装応答 | 2 つのセッションの間でメッセージを運びます。レビュアーのフィードバックは実装セッションへ、実装者の応答はレビューセッションへ戻ります。 |
|
||||
|
||||
## 関連
|
||||
|
||||
- [Git と GitHub ワークフロー](/git/) — これらのプロンプトの多くが Git フローを支えています
|
||||
- [ノート、todo と計画](/notes-todos-plans/) — Planning プロンプトの背後にある todo と計画
|
||||
- [Multi-run](/multi-run/) — ラングループと fusion
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
---
|
||||
title: プレビューと開発サーバー
|
||||
description: 実行中の開発サーバーを OpenChamber 内で開きます。
|
||||
description: 実行中の開発サーバーを OpenChamber の中で開きます。
|
||||
---
|
||||
|
||||
# プレビューと開発サーバー
|
||||
|
||||
開発サーバーを起動すると、OpenChamber は別のブラウザタブではなくアプリ内で直接開けます。サイトをチャットの横で見ながら、コンソールを取得し、要素を指して質問できます。
|
||||
開発サーバーを起動すると、OpenChamber は別のブラウザタブではなくアプリ内でそれを開けます。チャットの隣にサイトを表示したまま、要素を指し示して質問できます。
|
||||
|
||||
## プレビューを開く
|
||||
## 開発サーバーを開く
|
||||
|
||||
OpenChamber はターミナル出力からローカルアドレスを監視します(Vite、Next.js、Astro などが表示する `Local:` 行)。見つけると次のことができます。
|
||||
ヘッダーの地球儀ボタンからブラウザパネルを開きます。開発サーバーがすでに動いていれば一覧に表示され、クリックひとつで開けます。OpenChamber はマシン上で実際に待ち受けているものから見つけるので、どうやって起動したかに関係なく機能します。
|
||||
|
||||
- ターミナルに **Open preview** ボタンが表示されます
|
||||
- 自動オープンを有効にした [プロジェクトアクション](/project-actions/) が開きます
|
||||
- チャットメッセージ内のローカルリンクからも開けます
|
||||
次の場合にも開発サーバーは自動で開きます。
|
||||
|
||||
サイトはサイドパネルに読み込まれます。プレビューできるのはローカルアドレス(あなたのマシン上)のみです。
|
||||
- ターミナルのローカルアドレスで **プレビューを開く** を押したとき
|
||||
- 自動オープンを有効にした[プロジェクトアクション](/project-actions/)が起動したとき
|
||||
- チャットメッセージ内のローカルリンクをたどったとき
|
||||
|
||||
## コンソールと検査
|
||||
アドレスはいつでも自分で入力できます。`localhost:5173` のようにスキームを省いた入力は `http://` として扱われます。
|
||||
|
||||
プレビューパネルでは次のことができます。
|
||||
## リモートの OpenChamber を使う場合
|
||||
|
||||
- ページの **console** を見る — エラー、警告、ログを好きなようにフィルターできます
|
||||
- **inspect** をオンにし、任意の要素をクリックして、そのメモ(セレクター、スタイル、位置、スクリーンショット)をそのままチャットへ送る
|
||||
OpenChamber が別のマシンで動いているとき、開発サーバーも*そちら*のマシンにあります。手元のノートPCの `localhost` はまったく別の場所を指します。デスクトップアプリはこれを引き受けます。ローカルポートを開いてリモートの開発サーバーまで接続を通すので、ページは普通に読み込まれ、ホットリロードも開発者ツールも動きます。入力するアドレスは期待どおりのままで、裏側の仕組みが邪魔をすることはありません。
|
||||
|
||||
これは「このボタン、ここ」とエージェントに伝える最速の方法です。
|
||||
これにはデスクトップアプリが必要です。ブラウザのタブでは、自分のマシン上の開発サーバーだけを開けます。
|
||||
|
||||
## ページに注釈を付ける
|
||||
|
||||
要素を指し示す、ページに描き込む、それらをまとめてチャットへ送る方法は[ブラウザパネル](/desktop-browser/)を参照してください。
|
||||
|
||||
## 関連
|
||||
|
||||
- [プロジェクトアクション](/project-actions/) — サーバー起動時に自動で開く
|
||||
- [デスクトップブラウザ](/desktop-browser/) — デスクトップで任意のページに同じツールを使う
|
||||
- [プロジェクトアクション](/project-actions/) — 起動時にサーバーを自動で開く
|
||||
- [ブラウザパネル](/desktop-browser/) — ページへの注釈とエージェントによる操作
|
||||
|
||||
@@ -45,5 +45,6 @@ OpenChamber が何かを行うには、少なくとも 1 つの AI プロバイ
|
||||
|
||||
## 関連
|
||||
|
||||
- [統合機能](/integrations/) — Claude または Cursor のサブスクリプションをプロバイダーとして使う
|
||||
- [MCP サーバー](/mcp/) — エージェントに追加ツールを加える
|
||||
- [使用量とクォータ](/usage/) — 使った量を追跡する
|
||||
|
||||
@@ -12,7 +12,7 @@ Skills Catalog では、自分で書く代わりに、他の人が公開した
|
||||
## スキルをインストールする
|
||||
|
||||
1. カタログを開きます。
|
||||
2. 組み込みソース(Anthropic skills repo と ClawdHub community registry)を閲覧するか、検索します。
|
||||
2. 組み込みソース(Anthropic skills repo など)を閲覧するか、検索します。
|
||||
3. スキルを選び、インストールします。
|
||||
4. インストール先を選びます。すべての作業で使うか、現在のプロジェクトだけで使うかです。
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ description: 에이전트가 채팅에서 OpenChamber 세션, worktree, 예약
|
||||
|
||||
## 도구 켜기 또는 끄기
|
||||
|
||||
**설정 → 일반 → OpenCode CLI**를 열고 **에이전트 제어 도구**를 변경한 다음 **Save + Reload**를 선택하세요. 관리형 OpenCode 서버가 다시 시작된 후 설정이 적용됩니다.
|
||||
**설정 → 일반 → OpenChamber 도구**를 열고 **에이전트 제어 도구**를 변경하세요. 설정은 관리형 OpenCode 서버가 다시 시작되면 적용되며, OpenChamber가 **Apply & Restart** 로 안내합니다.
|
||||
|
||||
OpenChamber가 `OPENCODE_HOST` 또는 skip-start를 통해 외부 OpenCode 서버에 연결된 경우와 VS Code 확장에서는 이 도구를 사용할 수 없습니다. OpenChamber의 관리형 OpenCode 서버를 사용하는 데스크톱 및 웹 설치에서는 자동으로 지원됩니다.
|
||||
|
||||
@@ -37,3 +37,4 @@ OpenChamber가 `OPENCODE_HOST` 또는 skip-start를 통해 외부 OpenCode 서
|
||||
- [예약 작업](/ko/scheduled-tasks/)
|
||||
- [Worktree 세션](/ko/worktrees/)
|
||||
- [세션 목표](/ko/session-goals/)
|
||||
- [브라우저 패널](/ko/desktop-browser/) — 페이지를 보고 조작하는 OpenChamber Web 도구
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
---
|
||||
title: 데스크톱 브라우저
|
||||
description: 검사 및 콘솔 캡처 기능과 함께 데스크톱 앱 안에서 임의의 페이지를 탐색하세요.
|
||||
title: 브라우저 패널
|
||||
description: 앱 안에서 아무 페이지나 열고 주석을 달며 에이전트가 조작하게 합니다.
|
||||
---
|
||||
|
||||
# 데스크톱 브라우저
|
||||
# 브라우저 패널
|
||||
|
||||
데스크톱 앱에는 내장 브라우저가 있어 채팅 바로 옆에서 임의의 페이지를 열고, 요소를 가리켜 질문하고, 페이지의 콘솔을 캡처할 수 있습니다. 앱 헤더의 지구본 버튼에서 엽니다.
|
||||
브라우저 패널은 채팅 바로 옆에 아무 페이지나 엽니다. 앱 헤더의 지구본 버튼으로 여세요.
|
||||
|
||||
> 데스크톱 브라우저는 **데스크톱 전용** 기능입니다. 웹에서는 [미리보기](/ko/preview/) 패널이 로컬 개발 서버에 대해 동일한 검사 및 콘솔 도구를 제공합니다.
|
||||
데스크톱 앱에서는 진짜 브라우저입니다. 로그인 상태가 유지되고 핫 리로드가 동작하며 개발자 도구도 클릭 한 번이면 열립니다. 브라우저 탭에서도 페이지를 보여줄 수는 있지만 내부를 들여다볼 수는 없습니다. 아래 주석 도구는 데스크톱 전용입니다.
|
||||
|
||||
## 검사 및 주석
|
||||
여기서 연 페이지는 카메라, 마이크, 위치를 사용할 수 없습니다. 그런 요청은 거부됩니다.
|
||||
|
||||
**inspect**를 켜고 페이지의 임의 요소를 클릭합니다. OpenChamber가 그것이 무엇인지, 스타일, 위치, 스크린샷을 담은 메모를 캡처해 채팅 메시지에 첨부합니다. 에이전트에게 "바로 여기 이 요소"라고 알리는 가장 빠른 방법입니다.
|
||||
## 도구 모음
|
||||
|
||||
## 콘솔 캡처
|
||||
주소창은 이 프로젝트에서 열었던 페이지를 기억해 두었다가 입력하는 동안 제안합니다. 주소의 일부나 페이지 제목의 일부와 맞춰 봅니다. 화살표 키로 목록을 이동하고, Enter로 선택한 항목을 열고, 행의 버튼으로 목록에서 지웁니다.
|
||||
|
||||
브라우저는 페이지의 콘솔 출력(오류, 경고, 로그)을 수집하므로 개발자 도구를 열지 않고도 필터링하여 읽을 수 있습니다.
|
||||
그 옆에는 **새로 고침**이 있고, 변경이 도무지 반영되지 않을 때 캐시를 무시하는 **강력 새로 고침**, 그리고 페이지만 확대·축소하는 확대 조절이 있습니다.
|
||||
|
||||
## 관련 항목
|
||||
**쿠키 지우기**와 **캐시 데이터 지우기**는 이 패널에만 적용됩니다. OpenChamber 세션이나 다른 창은 그대로입니다.
|
||||
|
||||
- [Preview & Dev Servers](/ko/preview/) — 로컬 개발 서버에 대한 동일한 도구
|
||||
## 페이지에 주석 달기
|
||||
|
||||
**주석** 을 누르면 페이지 위에 세 가지 도구가 있는 막대가 나타납니다.
|
||||
|
||||
- **요소** — 요소를 클릭합니다. 다른 요소를 클릭하면 선택이 옮겨가고, 같은 요소를 다시 클릭하면 해제됩니다.
|
||||
- **영역** — 여러 요소에 걸친 이야기를 할 때는 해당 부분을 드래그해 감쌉니다.
|
||||
- **그리기** — 페이지 위에 자유롭게 스케치합니다.
|
||||
|
||||
표시 옆에 나타나는 입력란에 원하는 내용을 적고 **첨부** 를 누르세요. Enter 로도 됩니다. 채팅 메시지에 표시한 모든 것, 남긴 메모, 표시 중인 페이지에 표시를 그려 넣은 스크린샷이 담긴 카드가 붙습니다. 위치를 설명하는 대신 "이 버튼, 조금 더 둥글게"라고 말하면 됩니다.
|
||||
|
||||
페이지 자체는 변경되지 않습니다. 주석은 있는 것을 표시할 뿐입니다. `Esc` 로 취소하고 도구 막대를 닫습니다.
|
||||
|
||||
## 에이전트에게 조작 맡기기
|
||||
|
||||
에이전트는 브라우저 패널을 직접 쓸 수 있습니다. 페이지를 열고, 내용을 읽고, 클릭하고, 입력하고, 스크롤하고, 모바일·태블릿·데스크톱 레이아웃을 바꿔 가며 자기 작업을 여러분에게 부탁하지 않고 스스로 확인합니다. 그 과정은 패널에서 보입니다.
|
||||
|
||||
에이전트가 페이지에서 임의의 코드를 실행할 수는 없습니다. 브라우저가 실제 로그인 상태를 유지하므로 위에 적힌 동작으로만 제한됩니다.
|
||||
|
||||
보고 있는 화면을 프로젝트의 `.openchamber/screenshots/` 에 이미지로 저장하고 답변에서 보여 줄 수도 있습니다. 전후 비교가 가능한 이유가 이것이며, 파일은 그대로 남아 풀 리퀘스트에 첨부할 수 있습니다.
|
||||
|
||||
브라우저 동작은 **OpenChamber Web 도구**이며, **설정 → 일반 → OpenChamber 도구** 에서 따로 켜고 끌 수 있습니다.
|
||||
|
||||
이 기능에는 데스크톱 앱이 필요합니다. 브라우저 탭에 표시된 페이지는 조작할 수 없습니다.
|
||||
|
||||
## 개발자 도구
|
||||
|
||||
막대의 터미널 버튼을 누르면 해당 페이지에 대한 Chromium 자체 개발자 도구가 열립니다. 콘솔, 네트워크, 요소 등 기대하는 모든 기능을 쓸 수 있습니다.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [미리보기와 개발 서버](/preview/) — 원격 컴퓨터의 것을 포함해 실행 중인 앱 열기
|
||||
- [에이전트 제어 도구](/ko/agent-control-tool/) — 채팅에서 세션, worktree, 예약 작업 다루기
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: 통합 기능
|
||||
description: Claude 또는 Cursor 구독을 공급자로 사용하세요.
|
||||
---
|
||||
|
||||
# 통합 기능
|
||||
|
||||
통합 기능(인테그레이션)은 이미 가지고 있는 구독을 사용해 OpenChamber에 공급자를 추가하는 작은 플러그인입니다. **Settings → Integrations**에서 관리합니다.
|
||||
|
||||
> **실험 단계 기능.** 프로바이더 정책을 존중하려 노력하지만, 계정 제한과 정지는 각 프로바이더의 결정입니다. 본인의 책임 아래 통합 기능을 사용하세요.
|
||||
|
||||
사용 가능한 통합 기능:
|
||||
|
||||
- **Claude Code** — Claude Pro 또는 Max 플랜, API 키 불필요
|
||||
- **Cursor** — Cursor 플랜의 모델 한도
|
||||
|
||||
## 통합 기능 설치
|
||||
|
||||
1. **Settings → Integrations**를 엽니다.
|
||||
2. 통합 기능을 찾아 **Install**을 선택합니다.
|
||||
3. 요청되면 OpenCode를 다시 시작합니다 — 재시작 후 공급자가 나타납니다.
|
||||
4. **Set up**를 선택하고 로그인합니다. 그러면 채팅의 모델 선택기에 모델이 나타납니다.
|
||||
|
||||
통합 기능은 사용자 단위로 설치되므로 모든 프로젝트에서 작동합니다. 같은 카드에서 언제든 업데이트하거나 제거할 수 있습니다.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code는 Claude Pro 또는 Max 플랜을 사용합니다 — API 키도 별도의 Claude 앱도 필요 없습니다.
|
||||
|
||||
1. 통합 기능을 설치합니다(위 참고).
|
||||
2. **Set up**를 선택하고 로그인합니다. Claude Code CLI가 아직 없으면 설정에서 먼저 설치를 제안한 뒤 로그인을 진행합니다.
|
||||
|
||||
Claude Code는 여기에서 유일하게 공급자 CLI 설치와 로그인을 필요로 하는 통합 기능입니다. Cursor는 CLI가 필요 없습니다.
|
||||
|
||||
**Claude 계정이 안전하게 유지되는 방식:** 이 통합 기능은 Anthropic의 공식 Claude Agent SDK와 설치된 Claude Code CLI를 사용합니다. OAuth 탈취, 브라우저 토큰 추출·재사용, 지원되지 않는 클라이언트로의 위장, Anthropic 인증 우회를 하지 않습니다. Anthropic이 지원하는 정상 경로를 사용하므로 토큰 탈취나 비인가 인증 우회에 따른 계정 정지 위험이 없습니다.
|
||||
|
||||
## Cursor
|
||||
|
||||
Cursor는 Cursor 플랜에 포함된 모델을 OpenChamber에서 사용할 수 있게 합니다.
|
||||
|
||||
1. 통합 기능을 설치합니다(위 참고).
|
||||
2. **Set up**를 선택하고 링크를 열어 브라우저에서 접근을 승인합니다. API 키는 필요 없습니다. 로그인 후 모델 목록이 자동으로 로드됩니다.
|
||||
|
||||
## 업데이트 및 제거
|
||||
|
||||
- **Update**는 플러그인의 최신 공개 버전을 설치합니다.
|
||||
- **Remove**는 OpenCode 설정에서 플러그인을 삭제합니다. OpenCode가 다시 로드되면 공급자는 더 이상 로드되지 않습니다.
|
||||
|
||||
카드에 항목을 수동으로 관리해야 한다고 표시되면 **Manage plugins**를 선택해 중복 항목을 정리하세요.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [공급자, 모델, 에이전트](/ko/providers/) — 다른 공급자 연결과 모델 선택
|
||||
- [사용량 및 할당량](/ko/usage/) — 사용량 추적
|
||||
@@ -21,6 +21,64 @@ OpenChamber는 커밋 메시지 작성, PR 초안 작성, 이슈 검토, 충돌
|
||||
|
||||
마음이 바뀌었나요? 각 프롬프트에는 **reset to default**가 있고, 모든 곳에서 처음부터 다시 시작하려면 **reset all**이 있습니다.
|
||||
|
||||
## 각 프롬프트가 사용되는 곳
|
||||
|
||||
아래 표의 각 프롬프트는 실행되는 위치와 실행을 일으키는 트리거를 나타냅니다. 편집하기 전에 트리거를 확인해 어떤 흐름을 바꾸는지 알아두세요.
|
||||
|
||||
### Git
|
||||
|
||||
| 프롬프트 | 실행 위치 | 트리거 시점 |
|
||||
| --- | --- | --- |
|
||||
| 커밋 생성 | git 뷰 커밋 상자의 생성 버튼, 모바일 Changes 화면 | 커밋 메시지를 생성할 때. 선택한 파일과 브랜치의 최근 커밋 제목이 채워져 메시지가 저장소 스타일을 따르게 됩니다. |
|
||||
| PR 생성 | git 뷰 PR 탭의 pull request 생성 폼 | PR 제목과 본문을 생성할 때. base와 head 브랜치, 그 사이의 커밋과 변경 파일, 추가 컨텍스트, 저장소에 PR 템플릿이 있으면 그것까지 채워집니다. |
|
||||
| merge/rebase 충돌 해결 | merge나 rebase가 충돌로 멈췄을 때 git 뷰의 충돌 대화상자 | "Resolve in current session" 또는 "Resolve in new session"을 선택할 때. 에이전트가 충돌 파일을 읽고 파일별 해결 전략을 제안하며, 편집·stage·계속 진행 전에 확인을 기다립니다. |
|
||||
| cherry-pick 충돌 해결 | worktree 세션의 "Re-integrate commits" 섹션 | 세션의 커밋을 대상 브랜치로 옮기다 충돌이 나서 에이전트에 맡길 때. 에이전트가 임시 worktree에서 해결하고 파일을 stage한 뒤 cherry-pick을 계속합니다. |
|
||||
|
||||
### GitHub
|
||||
|
||||
| 프롬프트 | 실행 위치 | 트리거 시점 |
|
||||
| --- | --- | --- |
|
||||
| PR 검토 | 작성기 첨부 메뉴의 "Link GitHub PR" 선택기, 새 worktree 대화상자 | 두 가지 트리거. PR을 컨텍스트로 첨부하면 지침이 준비되어 다음 메시지와 함께 전송됩니다. PR에서 worktree 세션을 시작하면 이 프롬프트가 그 세션의 첫 메시지가 되고 전체 PR 컨텍스트가 첨부됩니다. |
|
||||
| 이슈 검토 | worktree를 이슈에서 시작할 때의 새 worktree 대화상자 | 새 세션의 첫 메시지가 이슈를 검토하며, 본문과 댓글이 컨텍스트로 첨부됩니다. |
|
||||
| PR 실패 검사 / PR 댓글 / 단일 PR 댓글 검토 | — | 현재 어떤 흐름도 이것들을 보내지 않습니다. PR 뷰가 이전에는 원클릭 검토 액션으로 실행했지만, 이제 실패한 검사와 댓글은 채팅 컨텍스트 초안으로 고정됩니다. 기존 재정의가 계속 동작하도록 편집 가능한 상태로 남습니다. |
|
||||
|
||||
### Planning
|
||||
|
||||
| 프롬프트 | 실행 위치 | 트리거 시점 |
|
||||
| --- | --- | --- |
|
||||
| todo 계획 | 프로젝트 사이드바의 Todos 패널 | todo를 세션 또는 새 worktree 세션으로 보낼 때. todo 텍스트가 보이는 메시지가 되고, 지침은 바로 구현으로 넘어가지 않고 질문 중심의 계획 대화로 만듭니다. |
|
||||
| 계획 개선 | Plans 뷰에서 저장된 계획의 "Improve" 액션 | 저장된 계획을 개선 흐름으로 보낼 때. 에이전트가 먼저 계획 파일을 읽고, 저장소 현재 상태에 근거한 변경을 제안하며 같은 파일을 편집하겠다고 제안합니다. |
|
||||
| 계획 구현 | 저장된 계획의 "Implement" 액션 | 저장된 계획을 구현 흐름으로 보낼 때. 에이전트가 계획 파일을 읽고 범위를 늘리지 않고 끝까지 구현하며, 계획 자체가 잘못된 것으로 밝혀지면 조정을 같은 파일에 저장합니다. |
|
||||
|
||||
### Session
|
||||
|
||||
대부분 작성기에 입력하는 슬래시 명령으로 동작합니다. 대부분 새 세션 초안 화면의 시작 칩으로도 나타납니다.
|
||||
|
||||
| 프롬프트 | 실행 위치 | 트리거 시점 |
|
||||
| --- | --- | --- |
|
||||
| 코드베이스 투어 | `/explore` | 코드베이스의 전체 개요를 요청할 때. |
|
||||
| 세션 요약 | `/summary`, 선택적으로 `/summary <주제>` | 지금까지의 대화를 요약할 때 — 새 세션으로 넘길 때 유용합니다. 기존 세션이 필요합니다. |
|
||||
| 작업 공간 검토 | `/workspace-review` | 현재 작업 공간 diff를 의도, 정확성, 보안 관점에서 검토해 달라고 요청할 때. |
|
||||
| 기능 계획 | `/plan-feature` | 거친 기능 아이디어를 안내된 질문-답변 대화를 통해 구현 계획으로 만들 때. |
|
||||
| Goal 만들기 | `/craft-goal`, 선택적으로 `/craft-goal <아이디어>` | 아이디어를 Goal 대화상자에 쓸 수 있는 검증 가능한 Goal 목표로 바꿀 때. |
|
||||
| 따라잡기 | `/catch-up` | 프로젝트로 돌아와 어디까지 진행됐고 다음에 무엇을 할지 물을 때. |
|
||||
| 디버깅 | `/debug` | 버그를 조사할 때: 에이전트가 가설을 세우고 코드에서 근본 원인을 확인한 뒤에야 수정을 제안합니다. |
|
||||
| 옵션 저울질 | `/weigh` | 무엇을 만들지는 알지만 어떻게 할지 모를 때. 에이전트가 두세 가지 접근을 비교하고 하나를 추천합니다. |
|
||||
| Fusion | multi-run 그룹의 "Run fusion" 액션 | 여러 실행의 출력을 하나의 답변으로 합칠 때. 실행 출력은 지침 뒤에 추가됩니다. |
|
||||
|
||||
### Settings에 페이지가 없는 프롬프트
|
||||
|
||||
일부 프롬프트는 자동으로 실행되며 Settings에 편집 가능한 페이지가 없습니다:
|
||||
|
||||
| 프롬프트 | 트리거 시점 |
|
||||
| --- | --- |
|
||||
| 예약 작업 | `/schedule-task`, 선택적으로 초기 아이디어와 함께. 예약 작업을 정의하는 대화를 이끕니다. |
|
||||
| 검토 핸드오프 | `/handoff-review`, 또는 핸드오프를 켜고 diff 뷰의 Review 버튼. 작업 세션에서 핸드오프를 생성합니다. |
|
||||
| 검토 세션 시작 메시지 | 생성된 검토 세션의 첫 메시지 — 핸드오프가 만들어졌으면 포함, 아니면 제외. |
|
||||
| 검토 피드백 / 구현 응답 | 두 세션 사이에서 메시지를 전달합니다: 검토자 피드백은 구현 세션으로, 구현자 응답은 검토 세션으로 돌아갑니다. |
|
||||
|
||||
## 관련 항목
|
||||
|
||||
- [Git & GitHub Workflows](/ko/git/) — 이러한 프롬프트 중 다수가 git 흐름을 구동합니다
|
||||
- [노트, todo와 계획](/ko/notes-todos-plans/) — Planning 프롬프트 뒤에 있는 todo와 계획
|
||||
- [Multi-run](/ko/multi-run/) — 실행 그룹과 fusion
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
---
|
||||
title: 미리보기 및 개발 서버
|
||||
description: 실행 중인 개발 서버를 OpenChamber 안에서 여세요.
|
||||
title: 미리보기와 개발 서버
|
||||
description: 실행 중인 개발 서버를 OpenChamber 안에서 엽니다.
|
||||
---
|
||||
|
||||
# 미리보기 및 개발 서버
|
||||
# 미리보기와 개발 서버
|
||||
|
||||
개발 서버를 시작하면 OpenChamber는 별도의 브라우저 탭이 아니라 앱 안에서 바로 열 수 있습니다. 그래서 채팅 옆에서 사이트를 보고, 콘솔을 캡처하고, 요소를 가리켜 질문할 수 있습니다.
|
||||
개발 서버를 띄우면 OpenChamber가 별도의 브라우저 탭 대신 앱 안에서 바로 열어 줍니다. 채팅 옆에 사이트를 두고 요소를 가리키며 물어볼 수 있습니다.
|
||||
|
||||
## 미리보기 열기
|
||||
## 개발 서버 열기
|
||||
|
||||
OpenChamber는 터미널 출력에서 로컬 주소(Vite, Next.js, Astro 같은 도구가 출력하는 `Local:` 줄)를 감시합니다. 발견하면 다음과 같이 동작합니다.
|
||||
앱 헤더의 지구본 버튼으로 브라우저 패널을 엽니다. 개발 서버가 이미 실행 중이면 목록에 나타나고 클릭 한 번으로 열립니다. OpenChamber는 컴퓨터에서 실제로 수신 대기 중인 것을 보고 찾아내므로, 어떻게 실행했든 상관없이 동작합니다.
|
||||
|
||||
- 터미널에 **Open preview** 버튼이 나타납니다
|
||||
- auto-open이 켜진 [프로젝트 액션](/ko/project-actions/)이 대신 열어줍니다
|
||||
- 채팅 메시지의 로컬 링크로도 열 수 있습니다
|
||||
다음 경우에도 개발 서버가 자동으로 열립니다.
|
||||
|
||||
사이트는 사이드 패널에 로드됩니다. 로컬 주소(사용자 자신의 컴퓨터)만 미리볼 수 있습니다.
|
||||
- 터미널의 로컬 주소에서 **미리보기 열기** 를 누를 때
|
||||
- 자동 열기를 켠 [프로젝트 작업](/project-actions/)이 서버를 시작할 때
|
||||
- 채팅 메시지의 로컬 링크를 따라갈 때
|
||||
|
||||
## 콘솔과 검사
|
||||
주소는 언제든 직접 입력할 수 있습니다. `localhost:5173` 처럼 스킴을 생략하면 `http://` 로 처리됩니다.
|
||||
|
||||
미리보기 패널에서 다음을 할 수 있습니다.
|
||||
## 원격 OpenChamber와 함께 쓰기
|
||||
|
||||
- 페이지의 **console**(오류, 경고, 로그)을 원하는 대로 필터링하여 확인
|
||||
- **inspect**를 켜고 임의의 요소를 클릭한 뒤 그에 대한 메모(선택자, 스타일, 위치, 스크린샷)를 바로 채팅으로 전송
|
||||
OpenChamber가 다른 컴퓨터에서 실행 중이면 개발 서버도 *그* 컴퓨터에 있습니다. 노트북의 `localhost`는 전혀 다른 곳을 가리키죠. 데스크톱 앱이 이를 대신 처리합니다. 로컬 포트를 열어 원격 개발 서버까지 연결을 이어 주므로 페이지가 평소처럼 로드되고 핫 리로드와 개발자 도구도 동작합니다. 여러분은 기대한 주소를 그대로 입력하면 되고, 내부 배관은 눈에 띄지 않습니다.
|
||||
|
||||
이것은 설명 없이 에이전트에게 "여기 이 버튼"이라고 알리는 가장 빠른 방법입니다.
|
||||
이 기능에는 데스크톱 앱이 필요합니다. 브라우저 탭에서는 자신의 컴퓨터에 있는 개발 서버만 열 수 있습니다.
|
||||
|
||||
## 관련 항목
|
||||
## 페이지에 주석 달기
|
||||
|
||||
- [Project Actions](/ko/project-actions/) — 서버를 시작할 때 자동으로 열기
|
||||
- [Desktop Browser](/ko/desktop-browser/) — 데스크톱에서 임의의 페이지에 동일한 도구 사용
|
||||
요소를 가리키고, 페이지에 그리고, 스타일 변경을 시험해 보고, 이 모두를 채팅으로 보내는 방법은 [브라우저 패널](/desktop-browser/)을 참고하세요.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [프로젝트 작업](/project-actions/) — 서버 시작 시 자동으로 열기
|
||||
- [브라우저 패널](/desktop-browser/) — 페이지 주석과 에이전트 조작
|
||||
|
||||
@@ -45,5 +45,6 @@ OpenChamber가 무언가를 하려면 먼저 최소한 하나의 AI 공급자가
|
||||
|
||||
## 관련 항목
|
||||
|
||||
- [통합 기능](/ko/integrations/) — Claude 또는 Cursor 구독을 공급자로 사용
|
||||
- [MCP Servers](/ko/mcp/) — 에이전트에 추가 도구를 제공합니다
|
||||
- [Usage & Quotas](/ko/usage/) — 사용량을 추적합니다
|
||||
|
||||
@@ -12,7 +12,7 @@ Skills Catalog를 사용하면 직접 작성하는 대신 다른 사람이 게
|
||||
## 스킬 설치하기
|
||||
|
||||
1. 카탈로그를 엽니다.
|
||||
2. 내장된 소스(Anthropic 스킬 저장소와 ClawdHub 커뮤니티 레지스트리)를 둘러보거나 검색합니다.
|
||||
2. 내장된 소스(예: Anthropic 스킬 저장소)를 둘러보거나 검색합니다.
|
||||
3. 스킬을 선택하고 설치합니다.
|
||||
4. 설치 위치를 선택합니다. 모든 작업에 적용할지, 현재 프로젝트에만 적용할지 선택합니다.
|
||||
|
||||
|
||||
@@ -21,6 +21,64 @@ Some prompts have a visible part (the message you'd see) and an instructions par
|
||||
|
||||
Changed your mind? Each prompt has **reset to default**, and there's a **reset all** if you want to start over everywhere.
|
||||
|
||||
## Where each prompt is used
|
||||
|
||||
Every prompt below lists where it runs and the trigger that fires it. Check the trigger before editing, so you know which flow you're changing.
|
||||
|
||||
### Git
|
||||
|
||||
| Prompt | Where it runs | When it fires |
|
||||
| --- | --- | --- |
|
||||
| Commit generation | The generate button in the git view's commit box, and the mobile Changes screen | You generate a commit message. The selected files and the branch's recent commit subjects are filled in, so the subject matches your repo's existing style. |
|
||||
| PR generation | The create-pull-request form in the git view's PR tab | You generate a PR title and body. Filled with the base and head branches, the commits and changed files between them, your additional context, and the repo's PR template when one exists. |
|
||||
| Merge/rebase conflict resolution | The conflicts dialog in the git view, when a merge or rebase stops on conflicts | You pick "Resolve in current session" or "Resolve in new session". The agent reads the conflicted files, proposes a per-file resolution strategy, and waits for your confirmation before editing, staging, or continuing the operation. |
|
||||
| Cherry-pick conflict resolution | The "Re-integrate commits" section for a worktree session | Moving the session's commits onto the target branch hits a conflict and you hand it to the agent. The agent resolves inside the temporary worktree, stages the resolved files, and continues the cherry-pick. |
|
||||
|
||||
### GitHub
|
||||
|
||||
| Prompt | Where it runs | When it fires |
|
||||
| --- | --- | --- |
|
||||
| PR review | The "Link GitHub PR" picker in the composer's attach menu, and the new worktree dialog | Two triggers. Attaching a PR as context renders the instructions, which go out with your next message. Starting a worktree session from a PR uses the prompt as that session's opening message, with the full PR context attached. |
|
||||
| Issue review | The new worktree dialog, when you start the worktree from an issue | The new session's opening message reviews the issue, with its body and comments attached as context. |
|
||||
| PR failed checks / PR comments / single PR comment | — | Not sent by any flow today. The PR view used to fire these from one-click review actions; failed checks and comments now pin as chat-context drafts instead. They stay editable so existing overrides keep working. |
|
||||
|
||||
### Planning
|
||||
|
||||
| Prompt | Where it runs | When it fires |
|
||||
| --- | --- | --- |
|
||||
| Todo planning | The Todos panel in the project sidebar | You send a todo to a session or a new worktree session. The todo text becomes the visible message; the instructions turn it into a question-first planning dialogue instead of jumping straight to implementation. |
|
||||
| Improve plan | The "Improve" action on a saved plan in the Plans view | You send a saved plan into an improve flow. The agent reads the plan file first, then proposes changes grounded in the current repo state and offers to edit the same file. |
|
||||
| Implement plan | The "Implement" action on a saved plan | You send a saved plan into an implement flow. The agent reads the plan file and implements it end to end without expanding scope, saving plan adjustments back to the file when the plan itself turns out to be wrong. |
|
||||
|
||||
### Session
|
||||
|
||||
Most of these power slash commands typed in the composer. Most also appear as starter chips on a new-session draft.
|
||||
|
||||
| Prompt | Where it runs | When it fires |
|
||||
| --- | --- | --- |
|
||||
| Codebase tour | `/explore` | You ask for a high-level orientation of the codebase. |
|
||||
| Session summary | `/summary`, optionally `/summary <topic>` | You summarize the conversation so far, useful for handing off to a new session. Needs an existing session. |
|
||||
| Workspace review | `/workspace-review` | You ask the agent to review the current workspace diff for intent, correctness, and security. |
|
||||
| Feature planning | `/plan-feature` | You turn a rough feature idea into an implementation plan through a guided question-and-answer dialogue. |
|
||||
| Goal crafting | `/craft-goal`, optionally `/craft-goal <idea>` | You turn an idea into a verifiable Goal objective for the Goal dialog. |
|
||||
| Catch up | `/catch-up` | You return to a project and ask where things stand and what to pick up next. |
|
||||
| Debugging | `/debug` | You investigate a bug: the agent forms hypotheses, confirms the root cause from the code, and only then proposes a fix. |
|
||||
| Weigh options | `/weigh` | You know what you want to build but not how. The agent compares two or three approaches and recommends one. |
|
||||
| Fusion | The "Run fusion" action on a multi-run group | You combine the outputs of several runs into one answer. The run outputs are appended after the instructions. |
|
||||
|
||||
### Prompts without a Settings entry
|
||||
|
||||
A few prompts fire automatically and have no editable page in Settings:
|
||||
|
||||
| Prompt | When it fires |
|
||||
| --- | --- |
|
||||
| Scheduled task | `/schedule-task`, optionally with an initial idea. Guides the dialogue that defines a scheduled task. |
|
||||
| Review handoff | `/handoff-review`, or the Review button in the diff view with handoff enabled. Generates the handoff in the working session. |
|
||||
| Review session starter | The opening message of the generated review session, with the handoff when one was produced and without it otherwise. |
|
||||
| Review feedback / implementation response | Shuttle messages between the two sessions: reviewer feedback goes back to the implementing session, and the implementer's response returns to the review session. |
|
||||
|
||||
## Related
|
||||
|
||||
- [Git & GitHub Workflows](/git/) — many of these prompts power the git flows
|
||||
- [Notes, Todos & Plans](/notes-todos-plans/) — the todos and plans behind the Planning prompts
|
||||
- [Multi-run](/multi-run/) — run groups and fusion
|
||||
|
||||
@@ -28,7 +28,7 @@ Narzędzie może wyświetlać projekty i preferencje modeli, tworzyć i kontynuo
|
||||
|
||||
## Włączanie i wyłączanie narzędzia
|
||||
|
||||
Otwórz **Ustawienia → Ogólne → OpenCode CLI**, zmień **Narzędzie sterowania dla agentów**, a następnie wybierz **Save + Reload**. Ustawienie zacznie działać po ponownym uruchomieniu zarządzanego serwera OpenCode.
|
||||
Otwórz **Ustawienia → Ogólne → Narzędzia OpenChamber** i zmień **Narzędzie sterowania dla agentów**. Ustawienie zacznie działać po ponownym uruchomieniu zarządzanego serwera OpenCode, które OpenChamber proponuje jako **Apply & Restart**.
|
||||
|
||||
Narzędzie nie jest dostępne, gdy OpenChamber łączy się z zewnętrznym serwerem OpenCode przez `OPENCODE_HOST` lub skip-start, ani w rozszerzeniu VS Code. Instalacje desktopowe i webowe korzystające z serwera OpenCode zarządzanego przez OpenChamber obsługują je automatycznie.
|
||||
|
||||
@@ -37,3 +37,4 @@ Narzędzie nie jest dostępne, gdy OpenChamber łączy się z zewnętrznym serwe
|
||||
- [Zaplanowane zadania](/pl/scheduled-tasks/)
|
||||
- [Sesje worktree](/pl/worktrees/)
|
||||
- [Cele sesji](/pl/session-goals/)
|
||||
- [Panel przeglądarki](/pl/desktop-browser/) — narzędzie OpenChamber Web — oglądanie strony i sterowanie nią
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
---
|
||||
title: Przeglądarka na komputerze
|
||||
description: Przeglądaj dowolną stronę wewnątrz aplikacji na komputerze, z inspekcją i przechwytywaniem konsoli.
|
||||
title: Panel przeglądarki
|
||||
description: Przeglądaj dowolną stronę w aplikacji, dodawaj do niej adnotacje i pozwól agentowi nią sterować.
|
||||
---
|
||||
|
||||
# Przeglądarka na komputerze
|
||||
# Panel przeglądarki
|
||||
|
||||
Aplikacja na komputerze ma wbudowaną przeglądarkę, dzięki czemu możesz otworzyć dowolną stronę tuż obok czatu, wskazywać elementy, aby o nie zapytać, oraz przechwytywać konsolę strony. Otwórz ją z przycisku globusa w nagłówku aplikacji.
|
||||
Panel przeglądarki otwiera dowolną stronę tuż obok czatu. Otwórz go przyciskiem globusa w nagłówku aplikacji.
|
||||
|
||||
> Przeglądarka na komputerze to funkcja **tylko na komputerze**. W wersji webowej panel [podglądu](/pl/preview/) oferuje te same narzędzia inspekcji i konsoli dla Twojego lokalnego serwera deweloperskiego.
|
||||
W aplikacji desktopowej to prawdziwa przeglądarka: twoje logowania są zachowywane, przeładowanie na gorąco działa, a narzędzia deweloperskie są o jedno kliknięcie. W karcie przeglądarki panel wyświetli stronę, ale nie zajrzy do jej wnętrza — poniższe narzędzia adnotacji są dostępne tylko na desktopie.
|
||||
|
||||
## Inspekcja i adnotacje
|
||||
Strony otwarte tutaj nie mogą użyć twojej kamery, mikrofonu ani lokalizacji: takie prośby są odrzucane.
|
||||
|
||||
Włącz **inspect** i kliknij dowolny element na stronie. OpenChamber przechwytuje o nim notatkę — czym jest, jakie ma style, gdzie się znajduje, oraz zrzut ekranu — i dołącza ją do Twojej wiadomości czatu. To najszybszy sposób, by powiedzieć agentowi „ten element, dokładnie tutaj”.
|
||||
## Pasek narzędzi
|
||||
|
||||
## Przechwytywanie konsoli
|
||||
Pasek adresu pamięta strony otwierane w tym projekcie i podpowiada je podczas pisania, dopasowując fragment adresu albo tytułu strony. Strzałki przechodzą po liście, Enter otwiera podświetloną pozycję, a przycisk w wierszu usuwa ją z listy.
|
||||
|
||||
Przeglądarka zbiera wynik konsoli strony — błędy, ostrzeżenia i logi — dzięki czemu możesz go filtrować i czytać bez otwierania narzędzi deweloperskich.
|
||||
Obok jest **odświeżenie**, a także **twarde odświeżenie**, które pomija pamięć podręczną, gdy zmiana uparcie się nie pokazuje, oraz sterowanie powiększeniem działające tylko na stronę.
|
||||
|
||||
**Wyczyść ciasteczka** i **Wyczyść dane w pamięci podręcznej** dotyczą wyłącznie tego panelu. Twoja sesja OpenChamber i pozostałe okna zostają nietknięte.
|
||||
|
||||
## Dodawanie adnotacji
|
||||
|
||||
Naciśnij **Adnotuj** — nad stroną pojawi się pasek z trzema narzędziami:
|
||||
|
||||
- **Element** — kliknij element. Kliknięcie innego przenosi zaznaczenie, ponowne kliknięcie tego samego je zdejmuje.
|
||||
- **Obszar** — obrysuj fragment ramką, gdy chodzi o więcej niż jeden element.
|
||||
- **Rysuj** — szkicuj odręcznie po stronie.
|
||||
|
||||
Napisz, czego oczekujesz, w polu obok swojego oznaczenia i naciśnij **Dołącz** — albo po prostu Enter. Do wiadomości trafi karta ze wszystkim, co zaznaczyłeś, z twoją notatką i ze zrzutem widocznej strony z naniesionymi oznaczeniami — możesz więc powiedzieć „ten przycisk, trochę bardziej zaokrąglony" zamiast opisywać, gdzie jest.
|
||||
|
||||
Sama strona nie jest zmieniana — adnotacja tylko oznacza to, co już tam jest. `Esc` anuluje i zamyka pasek.
|
||||
|
||||
## Sterowanie przez agenta
|
||||
|
||||
Agent może sam korzystać z panelu przeglądarki — otworzyć stronę, odczytać jej zawartość, klikać, wpisywać tekst, przewijać i przełączać układ mobilny, tabletowy i desktopowy — żeby sprawdzić własną pracę zamiast prosić o to ciebie. Zobaczysz to w panelu.
|
||||
|
||||
Agent nie może uruchamiać dowolnego kodu na stronie. Przeglądarka zachowuje twoje prawdziwe logowania, więc ogranicza się do powyższych działań.
|
||||
|
||||
Może też zapisać obraz tego, co widzi, w `.openchamber/screenshots/` w twoim projekcie i pokazać go w odpowiedzi. To właśnie umożliwia porównanie przed i po, a plik zostaje, by dołączyć go do pull requesta.
|
||||
|
||||
Działania w przeglądarce to **narzędzie OpenChamber Web**, które włącza się i wyłącza osobno w **Ustawienia → Ogólne → Narzędzia OpenChamber**.
|
||||
|
||||
Wymaga to aplikacji desktopowej: stroną pokazaną w karcie przeglądarki nie da się sterować.
|
||||
|
||||
## Narzędzia deweloperskie
|
||||
|
||||
Naciśnij przycisk terminala na pasku, aby otworzyć własne narzędzia deweloperskie Chromium dla strony — konsolę, sieć, elementy, wszystko czego oczekujesz.
|
||||
|
||||
## Powiązane
|
||||
|
||||
- [Podgląd i serwery deweloperskie](/pl/preview/) — te same narzędzia dla Twojego lokalnego serwera deweloperskiego
|
||||
- [Podgląd i serwery deweloperskie](/preview/) — otwieranie działającej aplikacji, także na zdalnym komputerze
|
||||
- [Narzędzie sterowania dla agentów](/pl/agent-control-tool/) — sesje, worktree i zaplanowane zadania prosto z czatu
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Integracje
|
||||
description: Używaj subskrypcji Claude lub Cursor jako dostawcy.
|
||||
---
|
||||
|
||||
# Integracje
|
||||
|
||||
Integracja to mała wtyczka, która dodaje dostawcę do OpenChamber na podstawie subskrypcji, którą już masz. Zarządzasz nimi w **Settings → Integrations**.
|
||||
|
||||
> **Funkcja eksperymentalna.** Staramy się przestrzegać zasad dostawców, ale ograniczenia i zawieszenia kont pozostają decyzją każdego dostawcy. Używaj integracji na własne ryzyko.
|
||||
|
||||
Dostępne integracje:
|
||||
|
||||
- **Claude Code** — Twój plan Claude Pro lub Max, bez kluczy API
|
||||
- **Cursor** — limity modeli z Twojego planu Cursor
|
||||
|
||||
## Instalacja integracji
|
||||
|
||||
1. Otwórz **Settings → Integrations**.
|
||||
2. Znajdź integrację i wybierz **Install**.
|
||||
3. Uruchom OpenCode ponownie, gdy o to poproszą — dostawca pojawi się po restarcie.
|
||||
4. Wybierz **Set up** i zaloguj się. Modele pojawią się potem w selektorze modeli na czacie.
|
||||
|
||||
Integracje instalują się dla Twojego użytkownika, więc działają w każdym projekcie. W każdej chwili możesz je zaktualizować lub usunąć z tej samej karty.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code korzysta z Twojego planu Claude Pro lub Max — bez kluczy API i bez osobnej aplikacji Claude.
|
||||
|
||||
1. Zainstaluj integrację (patrz wyżej).
|
||||
2. Wybierz **Set up** i zaloguj się. Jeśli nie masz jeszcze Claude Code CLI, konfiguracja zaoferuje najpierw jego instalację, a potem logowanie.
|
||||
|
||||
Claude Code jest jedyną integracją tutaj, która wymaga zainstalowanego i zalogowanego CLI swojego dostawcy. Cursor nie wymaga swojego CLI.
|
||||
|
||||
**Jak chronione jest Twoje konto Claude:** ta integracja używa oficjalnego Claude Agent SDK od Anthropic i Twojego zainstalowanego Claude Code CLI. Nie przechwytuje OAuth, nie wyodrębnia ani nie odtwarza tokenów przeglądarki, nie podszywa się pod nieobsługiwany klient i nie omija uwierzytelniania Anthropic. Działa na obsługiwanej przez Anthropic ścieżce dostępu, więc nie niesie ryzyka zablokowania konta związanego z przechwytywaniem tokenów lub nieautoryzowanymi obejściami uwierzytelniania.
|
||||
|
||||
## Cursor
|
||||
|
||||
Cursor udostępnia w OpenChamber modele zawarte w Twoim planie Cursor.
|
||||
|
||||
1. Zainstaluj integrację (patrz wyżej).
|
||||
2. Wybierz **Set up**, otwórz link i zatwierdź dostęp w przeglądarce. Klucz API nie jest potrzebny. Lista modeli wczyta się automatycznie po zalogowaniu.
|
||||
|
||||
## Aktualizacja i usuwanie
|
||||
|
||||
- **Update** instaluje najnowszą opublikowaną wersję wtyczki.
|
||||
- **Remove** usuwa wtyczkę z konfiguracji OpenCode. Dostawca przestanie się ładować po odświeżeniu OpenCode.
|
||||
|
||||
Jeśli karta wskazuje, że wpisy wymagają ręcznego zarządzania, wybierz **Manage plugins** i wyczyść tam duplikaty.
|
||||
|
||||
## Powiązane
|
||||
|
||||
- [Dostawcy, modele i agenci](/pl/providers/) — podłączanie innych dostawców i wybór modeli
|
||||
- [Zużycie i limity](/pl/usage/) — śledź, ile wykorzystałeś
|
||||
@@ -21,6 +21,64 @@ Niektóre prompty mają część widoczną (wiadomość, którą zobaczysz) i cz
|
||||
|
||||
Zmieniłeś zdanie? Każdy prompt ma **reset to default**, a jest też **reset all**, jeśli chcesz zacząć wszystko od nowa.
|
||||
|
||||
## Gdzie używany jest każdy prompt
|
||||
|
||||
Dla każdego prompta poniżej podano, gdzie się wykonuje i co go uruchamia. Sprawdź wyzwalacz przed edycją, żeby wiedzieć, który przepływ zmieniasz.
|
||||
|
||||
### Git
|
||||
|
||||
| Prompt | Gdzie się wykonuje | Kiedy się uruchamia |
|
||||
| --- | --- | --- |
|
||||
| Generowanie commita | Przycisk generowania w polu commita w widoku git oraz ekran Changes na mobile | Generujesz komunikat commita. Wstawiane są wybrane pliki i tematy ostatnich commitów gałęzi, dzięki czemu komunikat trzyma styl twojego repozytorium. |
|
||||
| Generowanie PR | Formularz tworzenia pull requesta w zakładce PR widoku git | Generujesz tytuł i treść PR. Wstawiane są gałęzie base i head, commity i zmienione pliki między nimi, twój dodatkowy kontekst oraz szablon PR repozytorium, jeśli istnieje. |
|
||||
| Rozwiązywanie konfliktu merge/rebase | Okno konfliktów w widoku git, gdy merge lub rebase zatrzyma się na konfliktach | Wybierasz "Resolve in current session" albo "Resolve in new session". Agent czyta pliki z konfliktem, proponuje strategię dla każdego pliku i czeka na twoje potwierdzenie przed edycją, stage'owaniem lub kontynuowaniem operacji. |
|
||||
| Rozwiązywanie konfliktu cherry-pick | Sekcja "Re-integrate commits" sesji w worktree | Przenoszenie commitów sesji na gałąź docelową trafia na konflikt i przekazujesz go agentowi. Agent rozwiązuje konflikty w tymczasowym worktree, robi stage plików i kontynuuje cherry-pick. |
|
||||
|
||||
### GitHub
|
||||
|
||||
| Prompt | Gdzie się wykonuje | Kiedy się uruchamia |
|
||||
| --- | --- | --- |
|
||||
| Review PR | Selektor "Link GitHub PR" w menu załączników kompozytora oraz okno nowego worktree | Dwa wyzwalacze. Przypięcie PR jako kontekstu przygotowuje instrukcje, które wychodzą z twoją następną wiadomością. Utworzenie sesji worktree z PR używa prompta jako pierwszej wiadomości tej sesji, z pełnym kontekstem PR w załączeniu. |
|
||||
| Review issue | Okno nowego worktree, gdy worktree startuje z issue | Pierwsza wiadomość nowej sesji przegląda issue, z jej treścią i komentarzami jako kontekstem. |
|
||||
| Review nieudanych checków PR / komentarzy PR / pojedynczego komentarza PR | — | Dziś żaden przepływ ich nie wysyła. Widok PR uruchamiał je kiedyś akcjami review jednym kliknięciem; teraz nieudane checki i komentarze są przypinane jako szkice kontekstu czatu. Zostają edytowalne, aby istniejące nadpisania dalej działały. |
|
||||
|
||||
### Planning
|
||||
|
||||
| Prompt | Gdzie się wykonuje | Kiedy się uruchamia |
|
||||
| --- | --- | --- |
|
||||
| Planowanie z todo | Panel Todos w pasku bocznym projektu | Wysyłasz todo do sesji albo nowej sesji w worktree. Tekst todo staje się widoczną wiadomością; instrukcje zamieniają go w planistyczny dialog oparty na pytaniach, zamiast skakać od razu do implementacji. |
|
||||
| Ulepsz plan | Akcja "Improve" na zapisanym planie w widoku Plans | Wysyłasz zapisany plan do przepływu ulepszania. Agent najpierw czyta plik planu, potem proponuje zmiany zakorzenione w aktualnym stanie repozytorium i proponuje edycję tego samego pliku. |
|
||||
| Zaimplementuj plan | Akcja "Implement" na zapisanym planie | Wysyłasz zapisany plan do przepływu implementacji. Agent czyta plik planu i implementuje go od początku do końca bez rozszerzania zakresu, zapisując korekty planu z powrotem do pliku, gdy sam plan okaże się błędny. |
|
||||
|
||||
### Session
|
||||
|
||||
Większość z nich zasila komendy z ukośnikiem wpisywane w kompozytorze. Większość pojawia się też jako startowe chipy na szkicu nowej sesji.
|
||||
|
||||
| Prompt | Gdzie się wykonuje | Kiedy się uruchamia |
|
||||
| --- | --- | --- |
|
||||
| Tour po kodzie | `/explore` | Prosisz o ogólną orientację w bazie kodu. |
|
||||
| Podsumowanie sesji | `/summary`, opcjonalnie `/summary <temat>` | Podsumowujesz dotychczasową rozmowę — przydatne do przekazania do nowej sesji. Wymaga istniejącej sesji. |
|
||||
| Review workspace | `/workspace-review` | Prosisz agenta o przegląd aktualnego diffu workspace pod kątem intencji, poprawności i bezpieczeństwa. |
|
||||
| Planowanie funkcji | `/plan-feature` | Zamieniasz surowy pomysł na funkcję w plan implementacji przez prowadzony dialog pytań i odpowiedzi. |
|
||||
| Formułowanie Goal | `/craft-goal`, opcjonalnie `/craft-goal <pomysł>` | Zamieniasz pomysł w weryfikowalny cel Goal do okna Goal. |
|
||||
| Nadrobienie bieżące | `/catch-up` | Wracasz do projektu i pytasz, na czym stanęło i co dalej. |
|
||||
| Debugowanie | `/debug` | Badasz buga: agent stawia hipotezy, potwierdza przyczynę źródłową w kodzie i dopiero wtedy proponuje poprawkę. |
|
||||
| Ważenie opcji | `/weigh` | Wiesz, co zbudować, ale nie jak. Agent porównuje dwa-trzy podejścia i poleca jedno. |
|
||||
| Fusion | Akcja "Run fusion" na grupie multi-run | Łączysz wyniki kilku uruchomień w jedną odpowiedź. Wyniki uruchomień są doklejane po instrukcjach. |
|
||||
|
||||
### Prompty bez strony w Settings
|
||||
|
||||
Kilka promptów uruchamia się automatycznie i nie ma edytowalnej strony w Settings:
|
||||
|
||||
| Prompt | Kiedy się uruchamia |
|
||||
| --- | --- |
|
||||
| Zaplanowane zadanie | `/schedule-task`, opcjonalnie z początkowym pomysłem. Prowadzi dialog, który definiuje zaplanowane zadanie. |
|
||||
| Handoff do review | `/handoff-review` albo przycisk Review w widoku diff z włączonym handoffem. Generuje handoff w sesji roboczej. |
|
||||
| Wiadomość startowa sesji review | Pierwsza wiadomość wygenerowanej sesji review — z handoffem, gdy powstał, bez niego w przeciwnym razie. |
|
||||
| Feedback z review / odpowiedź implementacji | Przenoszą wiadomości między dwiema sesjami: feedback recenzenta wraca do sesji implementującej, a odpowiedź implementatora wraca do sesji review. |
|
||||
|
||||
## Powiązane
|
||||
|
||||
- [Przepływy Git i GitHub](/pl/git/) — wiele z tych promptów napędza przepływy git
|
||||
- [Notatki, todo i plany](/pl/notes-todos-plans/) — todo i plany stojące za promptami Planning
|
||||
- [Multi-run](/pl/multi-run/) — grupy uruchomień i fusion
|
||||
|
||||
@@ -5,28 +5,31 @@ description: Otwórz działający serwer deweloperski wewnątrz OpenChamber.
|
||||
|
||||
# Podgląd i serwery deweloperskie
|
||||
|
||||
Gdy uruchamiasz serwer deweloperski, OpenChamber może otworzyć go bezpośrednio w aplikacji zamiast w osobnej karcie przeglądarki — dzięki czemu widzisz swoją witrynę obok czatu, przechwytujesz jej konsolę i wskazujesz elementy, aby o nie zapytać.
|
||||
Gdy uruchamiasz serwer deweloperski, OpenChamber może otworzyć go od razu w aplikacji zamiast w osobnej karcie przeglądarki — widzisz swoją stronę obok czatu i możesz wskazywać elementy, żeby o nie zapytać.
|
||||
|
||||
## Otwórz podgląd
|
||||
## Otwieranie serwera deweloperskiego
|
||||
|
||||
OpenChamber obserwuje wynik terminala pod kątem adresu lokalnego (wiersz `Local:`, który drukują narzędzia takie jak Vite, Next.js czy Astro). Gdy go wykryje:
|
||||
Otwórz panel przeglądarki przyciskiem globusa w nagłówku aplikacji. Jeśli serwer już działa, znajdziesz go na liście i otworzysz jednym kliknięciem — OpenChamber rozpoznaje go po tym, co faktycznie nasłuchuje na twoim komputerze, więc działa niezależnie od sposobu uruchomienia.
|
||||
|
||||
- w terminalu pojawia się przycisk **Open preview**
|
||||
- [akcja projektu](/pl/project-actions/) z włączonym auto-open otwiera go za Ciebie
|
||||
- lokalny link w wiadomości czatu również może go otworzyć
|
||||
Serwer otwiera się też sam, gdy:
|
||||
|
||||
Witryna ładuje się w panelu bocznym. Podglądać można wyłącznie adresy lokalne (na Twojej własnej maszynie).
|
||||
- naciśniesz **Otwórz podgląd** przy lokalnym adresie w terminalu
|
||||
- [akcja projektu](/project-actions/) z włączonym automatycznym otwieraniem go uruchomi
|
||||
- klikniesz lokalny odnośnik w wiadomości na czacie
|
||||
|
||||
## Konsola i inspekcja
|
||||
Adres zawsze możesz wpisać ręcznie. Samo `localhost:5173` jest traktowane jako `http://`, więc schematu nie musisz podawać.
|
||||
|
||||
W panelu podglądu możesz:
|
||||
## Praca ze zdalnym OpenChamber
|
||||
|
||||
- obserwować **konsolę** strony — błędy, ostrzeżenia i logi, filtrowane wedle uznania
|
||||
- włączyć **inspect**, kliknąć dowolny element i wysłać o nim notatkę — selektor, style, pozycję i zrzut ekranu — prosto do czatu
|
||||
Gdy OpenChamber działa na innym komputerze, jego serwer deweloperski też jest na *tamtym* komputerze — `localhost` na twoim laptopie prowadzi zupełnie gdzie indziej. Aplikacja desktopowa załatwia to za ciebie: otwiera lokalny port, którym prowadzi połączenie do zdalnego serwera, dzięki czemu strona ładuje się normalnie, z działającym przeładowaniem na gorąco i narzędziami deweloperskimi. Nadal wpisujesz adres, którego się spodziewasz; technikalia nie wchodzą ci w drogę.
|
||||
|
||||
To najszybszy sposób, by powiedzieć agentowi „ten przycisk, tutaj”, bez opisywania go.
|
||||
Wymaga to aplikacji desktopowej. W karcie przeglądarki otworzysz tylko serwery na własnym komputerze.
|
||||
|
||||
## Adnotacje na stronie
|
||||
|
||||
O wskazywaniu elementów, rysowaniu po stronie, przymierzaniu zmian stylów i wysyłaniu tego wszystkiego na czat przeczytasz w [Panelu przeglądarki](/desktop-browser/).
|
||||
|
||||
## Powiązane
|
||||
|
||||
- [Akcje projektu](/pl/project-actions/) — automatycznie otwórz serwer przy uruchomieniu
|
||||
- [Przeglądarka na komputerze](/pl/desktop-browser/) — te same narzędzia dla dowolnej strony, na komputerze
|
||||
- [Akcje projektu](/project-actions/) — automatyczne otwarcie serwera po uruchomieniu
|
||||
- [Panel przeglądarki](/desktop-browser/) — adnotacje stron i sterowanie przez agenta
|
||||
|
||||
@@ -45,5 +45,6 @@ Logowania dostawców są przechowywane przez OpenCode, a nie OpenChamber, więc
|
||||
|
||||
## Powiązane
|
||||
|
||||
- [Integracje](/pl/integrations/) — używaj subskrypcji Claude lub Cursor jako dostawcy
|
||||
- [Serwery MCP](/pl/mcp/) — dodaj agentom dodatkowe narzędzia
|
||||
- [Zużycie i limity](/pl/usage/) — śledź, ile już wykorzystałeś
|
||||
|
||||
@@ -12,7 +12,7 @@ Aby pisać własne skille, zobacz [Skille](/pl/skills/).
|
||||
## Zainstaluj skill
|
||||
|
||||
1. Otwórz katalog.
|
||||
2. Przeglądaj wbudowane źródła — repozytorium skilli Anthropic oraz rejestr społeczności ClawdHub — albo wyszukaj.
|
||||
2. Przeglądaj wbudowane źródła — na przykład repozytorium skilli Anthropic — albo wyszukaj.
|
||||
3. Wybierz skill i zainstaluj go.
|
||||
4. Wybierz, gdzie go zainstalować: dla wszystkiego, co robisz, albo tylko dla bieżącego projektu.
|
||||
|
||||
|
||||
@@ -5,28 +5,31 @@ description: Open a running dev server inside OpenChamber.
|
||||
|
||||
# Preview & Dev Servers
|
||||
|
||||
When you start a dev server, OpenChamber can open it right inside the app instead of a separate browser tab — so you can see your site next to the chat, capture its console, and point at elements to ask about them.
|
||||
When you start a dev server, OpenChamber can open it right inside the app instead of a separate browser tab — so you can see your site next to the chat and point at elements to ask about them.
|
||||
|
||||
## Open a preview
|
||||
## Open a dev server
|
||||
|
||||
OpenChamber watches terminal output for a local address (the `Local:` line that tools like Vite, Next.js, or Astro print). When it spots one:
|
||||
Open the browser panel from the globe button in the app header. If a dev server is already running, it is listed there and one click opens it — OpenChamber finds it by looking at what is actually listening on your machine, so it works no matter how you started it.
|
||||
|
||||
- in the terminal, an **Open preview** button appears
|
||||
- a [project action](/project-actions/) with auto-open turned on opens it for you
|
||||
- a local link in a chat message can open it too
|
||||
A dev server also opens automatically when:
|
||||
|
||||
The site loads in the side panel. Only local addresses (on your own machine) can be previewed.
|
||||
- you press **Open preview** on a local address in the terminal
|
||||
- a [project action](/project-actions/) with auto-open turned on starts one
|
||||
- you follow a local link in a chat message
|
||||
|
||||
## Console and inspect
|
||||
You can always type an address yourself. A bare `localhost:5173` is treated as `http://`, so you do not have to type the scheme.
|
||||
|
||||
In the preview panel you can:
|
||||
## Working with a remote OpenChamber
|
||||
|
||||
- watch the page's **console** — errors, warnings, and logs, filtered however you like
|
||||
- turn on **inspect**, click any element, and send a note about it — selector, styles, position, and a screenshot — straight into chat
|
||||
When OpenChamber runs on another machine, its dev server is on *that* machine — `localhost` on your laptop is somewhere else entirely. The desktop app handles this for you: it opens a local port that carries the connection through to the remote dev server, so the page loads normally, with working hot reload and developer tools. You keep typing the address you expect; the plumbing stays out of your way.
|
||||
|
||||
This is the fastest way to tell the agent "this button, here" without describing it.
|
||||
This needs the desktop app. In a web browser tab, only dev servers on your own machine can be opened.
|
||||
|
||||
## Annotate the page
|
||||
|
||||
See [Browser Panel](/desktop-browser/) for pointing at elements, drawing on the page, and sending it all to chat.
|
||||
|
||||
## Related
|
||||
|
||||
- [Project Actions](/project-actions/) — auto-open a server when you start it
|
||||
- [Desktop Browser](/desktop-browser/) — the same tools for any page, on desktop
|
||||
- [Browser Panel](/desktop-browser/) — annotating pages and letting the agent drive
|
||||
|
||||
@@ -57,5 +57,6 @@ Provider sign-ins are stored by OpenCode, not OpenChamber, so they're shared wit
|
||||
|
||||
## Related
|
||||
|
||||
- [Integrations](/integrations/) — use a Claude or Cursor subscription as a provider
|
||||
- [MCP Servers](/mcp/) — add extra tools for agents
|
||||
- [Usage & Quotas](/usage/) — track how much you've used
|
||||
|
||||
@@ -28,7 +28,7 @@ A ferramenta pode listar projetos e preferências de modelos, criar e continuar
|
||||
|
||||
## Ativar ou desativar a ferramenta
|
||||
|
||||
Abra **Configurações → Geral → OpenCode CLI**, altere **Ferramenta de controle para agentes** e selecione **Save + Reload**. A configuração entra em vigor depois que o servidor OpenCode gerenciado reinicia.
|
||||
Abra **Configurações → Geral → Ferramentas do OpenChamber** e altere **Ferramenta de controle para agentes**. A configuração entra em vigor quando o servidor OpenCode gerenciado reinicia, o que o OpenChamber oferece como **Apply & Restart**.
|
||||
|
||||
A ferramenta não está disponível quando o OpenChamber se conecta a um servidor OpenCode externo por `OPENCODE_HOST` ou skip-start, nem na extensão do VS Code. As instalações desktop e web que usam o servidor OpenCode gerenciado pelo OpenChamber têm suporte automático.
|
||||
|
||||
@@ -37,3 +37,4 @@ A ferramenta não está disponível quando o OpenChamber se conecta a um servido
|
||||
- [Tarefas agendadas](/pt-br/scheduled-tasks/)
|
||||
- [Sessões de worktree](/pt-br/worktrees/)
|
||||
- [Objetivos de sessão](/pt-br/session-goals/)
|
||||
- [Painel do navegador](/pt-br/desktop-browser/) — a ferramenta OpenChamber Web, para ver uma página e conduzi-la
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
---
|
||||
title: Navegador no Desktop
|
||||
description: Navegue por qualquer página dentro do app de desktop, com inspeção e captura de console.
|
||||
title: Painel do navegador
|
||||
description: Navegue em qualquer página dentro do aplicativo, anote-a e deixe o agente conduzi-la.
|
||||
---
|
||||
|
||||
# Navegador no Desktop
|
||||
# Painel do navegador
|
||||
|
||||
O app de desktop tem um navegador integrado para você abrir qualquer página logo ao lado do seu chat, apontar para elementos para perguntar sobre eles e capturar o console da página. Abra-o pelo botão de globo no cabeçalho do app.
|
||||
O painel do navegador abre qualquer página bem ao lado do seu chat. Abra-o pelo botão do globo no cabeçalho.
|
||||
|
||||
> O navegador no desktop é um recurso **apenas para desktop**. Na web, o painel de [preview](/pt-br/preview/) oferece as mesmas ferramentas de inspeção e console para o seu servidor de desenvolvimento local.
|
||||
No aplicativo desktop é um navegador de verdade: seus logins persistem, a recarga a quente funciona e as ferramentas de desenvolvedor estão a um clique. Em uma aba do navegador o painel exibe a página, mas não consegue olhar dentro dela — as ferramentas de anotação abaixo são exclusivas do desktop.
|
||||
|
||||
## Inspecionar e anotar
|
||||
Páginas abertas aqui não podem usar sua câmera, seu microfone nem sua localização: esses pedidos são recusados.
|
||||
|
||||
Ative o **inspect** e clique em qualquer elemento da página. O OpenChamber captura uma nota sobre ele — o que é, seus estilos, onde fica e uma captura de tela — e a anexa à sua mensagem no chat. É a forma mais rápida de dizer ao agente "este elemento, bem aqui".
|
||||
## A barra de ferramentas
|
||||
|
||||
## Captura de console
|
||||
A barra de endereço lembra as páginas que você abriu neste projeto e as oferece enquanto você digita, procurando por parte do endereço ou do título da página. As setas percorrem a lista, Enter abre o item destacado e o botão da linha o remove.
|
||||
|
||||
O navegador coleta a saída do console da página — erros, avisos e logs — para que você possa filtrá-la e lê-la sem abrir as ferramentas de desenvolvedor.
|
||||
Ao lado fica **Recarregar**, junto de uma **recarga forçada** que ignora o cache quando uma mudança teima em não aparecer, e os controles de zoom, que ampliam apenas a página.
|
||||
|
||||
**Limpar cookies** e **Limpar dados em cache** valem só para este painel. Sua sessão do OpenChamber e as outras janelas ficam intactas.
|
||||
|
||||
## Anotar uma página
|
||||
|
||||
Pressione **Anotar** e uma barra aparece sobre a página com três ferramentas:
|
||||
|
||||
- **Elemento** — clique em um elemento. Clicar em outro move a seleção; clicar no mesmo de novo a remove.
|
||||
- **Região** — arraste um retângulo em volta de uma área quando o assunto envolver mais de um elemento.
|
||||
- **Desenhar** — rabisque à mão livre sobre a página.
|
||||
|
||||
Escreva o que quer no campo que aparece ao lado da sua marcação e pressione **Anexar** — ou apenas Enter. Sua mensagem ganha um cartão com tudo o que você marcou, sua nota e uma captura da página visível com suas marcações desenhadas nela — assim você pode dizer "este botão, um pouco mais arredondado" em vez de descrever onde ele está.
|
||||
|
||||
A página em si nunca é modificada — anotar apenas marca o que já está lá. `Esc` cancela e fecha a barra.
|
||||
|
||||
## Deixar o agente conduzir
|
||||
|
||||
O agente pode usar o painel do navegador sozinho — abrir uma página, ler o que há nela, clicar, digitar, rolar e alternar entre layout móvel, de tablet e de desktop — para conferir o próprio trabalho em vez de pedir isso a você. Você vê acontecendo no painel.
|
||||
|
||||
O agente não pode executar código arbitrário na página. O navegador guarda seus logins reais, então ele fica limitado às ações acima.
|
||||
|
||||
Ele também pode salvar uma imagem do que está vendo em `.openchamber/screenshots/` no seu projeto e mostrá-la na resposta. É isso que torna possível um antes e depois, e o arquivo continua lá para anexar a um pull request.
|
||||
|
||||
As ações do navegador são a **ferramenta OpenChamber Web**, que pode ser ligada e desligada por conta própria em **Configurações → Geral → Ferramentas do OpenChamber**.
|
||||
|
||||
Isso exige o aplicativo de desktop: uma página exibida numa aba do navegador não pode ser controlada.
|
||||
|
||||
## Ferramentas de desenvolvedor
|
||||
|
||||
Pressione o botão de terminal na barra para abrir as ferramentas de desenvolvedor do próprio Chromium para a página — console, rede, elementos, tudo o que você espera.
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Preview e Servidores de Desenvolvimento](/pt-br/preview/) — as mesmas ferramentas para o seu servidor de desenvolvimento local
|
||||
- [Pré-visualização e servidores de desenvolvimento](/preview/) — abrir seu aplicativo em execução, inclusive em uma máquina remota
|
||||
- [Ferramenta de controle para agentes](/pt-br/agent-control-tool/) — sessões, worktrees e tarefas agendadas pelo chat
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Integrações
|
||||
description: Use sua assinatura Claude ou Cursor como provedor.
|
||||
---
|
||||
|
||||
# Integrações
|
||||
|
||||
Uma integração é um pequeno plugin que adiciona um provedor ao OpenChamber usando uma assinatura que você já tem. Você as gerencia em **Settings → Integrations**.
|
||||
|
||||
> **Recurso experimental.** Buscamos respeitar as políticas dos provedores, mas restrições e suspensões de conta continuam sendo decisão de cada provedor. Use as integrações por sua conta e risco.
|
||||
|
||||
Integrações disponíveis:
|
||||
|
||||
- **Claude Code** — seu plano Claude Pro ou Max, sem chaves de API
|
||||
- **Cursor** — os limites de modelos do seu plano Cursor
|
||||
|
||||
## Instalar uma integração
|
||||
|
||||
1. Abra **Settings → Integrations**.
|
||||
2. Encontre a integração e escolha **Install**.
|
||||
3. Reinicie o OpenCode quando solicitado — o provedor aparece após a reinicialização.
|
||||
4. Escolha **Set up** e faça login. Os modelos aparecem então no seletor de modelos do chat.
|
||||
|
||||
As integrações são instaladas para o seu usuário, então funcionam em todos os projetos. Você pode atualizá-las ou removê-las do mesmo cartão a qualquer momento.
|
||||
|
||||
## Claude Code
|
||||
|
||||
O Claude Code usa seu plano Claude Pro ou Max — sem chaves de API e sem um app Claude separado.
|
||||
|
||||
1. Instale a integração (acima).
|
||||
2. Escolha **Set up** e faça login. Se você ainda não tem a CLI do Claude Code, a configuração oferece instalá-la primeiro e depois fazer login.
|
||||
|
||||
O Claude Code é a única integração aqui que exige que a CLI do provedor esteja instalada e autenticada. Cursor não exige sua CLI.
|
||||
|
||||
**Como sua conta Claude fica protegida:** esta integração usa o Claude Agent SDK oficial da Anthropic e a CLI do Claude Code instalada em sua máquina. Ela não sequestra OAuth, não extrai nem reproduz tokens do navegador, não se passa por um cliente não suportado e não contorna a autenticação da Anthropic. Ela permanece no caminho de acesso suportado pela Anthropic, portanto não traz o risco de banimento de conta associado a sequestro de tokens ou a contornos de autenticação não autorizados.
|
||||
|
||||
## Cursor
|
||||
|
||||
O Cursor torna disponíveis no OpenChamber os modelos incluídos no seu plano Cursor.
|
||||
|
||||
1. Instale a integração (acima).
|
||||
2. Escolha **Set up**, abra o link e autorize o acesso no navegador. Nenhuma chave de API é necessária. A lista de modelos carrega automaticamente após o login.
|
||||
|
||||
## Atualizar ou remover
|
||||
|
||||
- **Update** instala a versão publicada mais recente do plugin.
|
||||
- **Remove** exclui o plugin da sua configuração do OpenCode. O provedor deixa de ser carregado quando o OpenCode é recarregado.
|
||||
|
||||
Se um cartão indicar que as entradas precisam de gerenciamento manual, escolha **Manage plugins** e limpe as duplicatas lá.
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Provedores, modelos e agentes](/pt-br/providers/) — conecte outros provedores e escolha modelos
|
||||
- [Uso e cotas](/pt-br/usage/) — acompanhe quanto você usou
|
||||
@@ -21,6 +21,64 @@ Alguns prompts têm uma parte visível (a mensagem que você veria) e uma parte
|
||||
|
||||
Mudou de ideia? Cada prompt tem **reset to default**, e há um **reset all** se você quiser recomeçar em tudo.
|
||||
|
||||
## Onde cada prompt é usado
|
||||
|
||||
Cada prompt nas tabelas abaixo indica onde ele roda e o que o dispara. Confira o gatilho antes de editar, para saber qual fluxo você está mudando.
|
||||
|
||||
### Git
|
||||
|
||||
| Prompt | Onde roda | Quando dispara |
|
||||
| --- | --- | --- |
|
||||
| Geração de commit | O botão de gerar na caixa de commit da vista git, e a tela Changes no mobile | Você gera uma mensagem de commit. São preenchidos os arquivos selecionados e os assuntos dos commits recentes do branch, para a mensagem seguir o estilo do seu repositório. |
|
||||
| Geração de PR | O formulário de criação de pull request na aba PR da vista git | Você gera título e corpo de um PR. São preenchidos os branches base e head, os commits e arquivos alterados entre eles, o contexto adicional que você escreveu e o template de PR do repositório, quando existe. |
|
||||
| Resolução de conflito de merge/rebase | O diálogo de conflitos na vista git, quando um merge ou rebase para em conflitos | Você escolhe "Resolve in current session" ou "Resolve in new session". O agente lê os arquivos em conflito, propõe uma estratégia por arquivo e aguarda sua confirmação antes de editar, fazer stage ou continuar a operação. |
|
||||
| Resolução de conflito de cherry-pick | A seção "Re-integrate commits" de uma sessão em worktree | Mover os commits da sessão para o branch de destino esbarra em um conflito e você passa para o agente. O agente resolve dentro do worktree temporário, faz stage dos arquivos e continua o cherry-pick. |
|
||||
|
||||
### GitHub
|
||||
|
||||
| Prompt | Onde roda | Quando dispara |
|
||||
| --- | --- | --- |
|
||||
| Revisão de PR | O seletor "Link GitHub PR" no menu de anexos do composer, e o diálogo de novo worktree | Dois gatilhos. Anexar um PR como contexto renderiza as instruções, que saem com a sua próxima mensagem. Criar uma sessão de worktree a partir de um PR usa o prompt como primeira mensagem dessa sessão, com o contexto completo do PR anexado. |
|
||||
| Revisão de issue | O diálogo de novo worktree, quando o worktree parte de uma issue | A primeira mensagem da nova sessão revisa a issue, com o corpo e os comentários anexados como contexto. |
|
||||
| Revisão de checks falhos / comentários de PR / comentário único de PR | — | Hoje nenhum fluxo os envia. A vista de PR antes os disparava com ações de revisão de um clique; agora checks falhos e comentários são fixados como rascunhos de contexto do chat. Continuam editáveis para que overrides existentes mantenham efeito. |
|
||||
|
||||
### Planning
|
||||
|
||||
| Prompt | Onde roda | Quando dispara |
|
||||
| --- | --- | --- |
|
||||
| Planejamento a partir de todo | O painel Todos na barra lateral do projeto | Você envia um todo para uma sessão ou uma nova sessão em worktree. O texto do todo vira a mensagem visível; as instruções transformam isso em um diálogo de planejamento guiado por perguntas, em vez de pular direto para a implementação. |
|
||||
| Melhorar plano | A ação "Improve" sobre um plano salvo na vista Plans | Você envia um plano salvo para o fluxo de melhoria. O agente lê primeiro o arquivo do plano, propõe mudanças ancoradas no estado atual do repositório e se oferece para editar o mesmo arquivo. |
|
||||
| Implementar plano | A ação "Implement" sobre um plano salvo | Você envia um plano salvo para o fluxo de implementação. O agente lê o arquivo do plano e o implementa do início ao fim sem ampliar o escopo, salvando ajustes do plano no arquivo quando o próprio plano se mostra errado. |
|
||||
|
||||
### Session
|
||||
|
||||
A maioria alimenta comandos de barra digitados no composer. A maioria também aparece como chips de partida no rascunho de nova sessão.
|
||||
|
||||
| Prompt | Onde roda | Quando dispara |
|
||||
| --- | --- | --- |
|
||||
| Tour pelo código | `/explore` | Você pede uma orientação geral do código. |
|
||||
| Resumo de sessão | `/summary`, opcionalmente `/summary <tópico>` | Você resume a conversa até aqui — útil para passar para uma nova sessão. Precisa de uma sessão existente. |
|
||||
| Revisão do workspace | `/workspace-review` | Você pede ao agente para revisar o diff atual do workspace quanto a intenção, correção e segurança. |
|
||||
| Planejamento de feature | `/plan-feature` | Você transforma uma ideia grosseira de feature em um plano de implementação por um diálogo guiado de perguntas e respostas. |
|
||||
| Construir Goal | `/craft-goal`, opcionalmente `/craft-goal <ideia>` | Você transforma uma ideia em um objetivo Goal verificável para o diálogo de Goal. |
|
||||
| Retomar o fio | `/catch-up` | Você volta a um projeto e pergunta onde as coisas pararam e o que fazer a seguir. |
|
||||
| Depuração | `/debug` | Você investiga um bug: o agente levanta hipóteses, confirma a causa raiz no código e só então propõe uma correção. |
|
||||
| Pesar opções | `/weigh` | Você sabe o que construir, mas não como. O agente compara duas ou três abordagens e recomenda uma. |
|
||||
| Fusion | A ação "Run fusion" em um grupo de multi-run | Você combina as saídas de várias execuções em uma resposta. As saídas das execuções são anexadas depois das instruções. |
|
||||
|
||||
### Prompts sem página no Settings
|
||||
|
||||
Alguns prompts disparam automaticamente e não têm página editável no Settings:
|
||||
|
||||
| Prompt | Quando dispara |
|
||||
| --- | --- |
|
||||
| Tarefa agendada | `/schedule-task`, opcionalmente com uma ideia inicial. Conduz o diálogo que define uma tarefa agendada. |
|
||||
| Handoff de revisão | `/handoff-review`, ou o botão Review na vista de diff com handoff ativado. Gera o handoff na sessão de trabalho. |
|
||||
| Mensagem inicial da sessão de revisão | A primeira mensagem da sessão de revisão gerada — com o handoff quando um foi produzido, sem ele caso contrário. |
|
||||
| Feedback de revisão / resposta de implementação | Levam mensagens entre as duas sessões: o feedback do revisor volta para a sessão que implementa, e a resposta do implementador retorna à sessão de revisão. |
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Fluxos de Git e GitHub](/pt-br/git/) — muitos desses prompts alimentam os fluxos de git
|
||||
- [Notas, todos e planos](/pt-br/notes-todos-plans/) — os todos e planos por trás dos prompts de Planning
|
||||
- [Multi-run](/pt-br/multi-run/) — grupos de execução e fusion
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
---
|
||||
title: Preview e Servidores de Desenvolvimento
|
||||
title: Pré-visualização e servidores de desenvolvimento
|
||||
description: Abra um servidor de desenvolvimento em execução dentro do OpenChamber.
|
||||
---
|
||||
|
||||
# Preview e Servidores de Desenvolvimento
|
||||
# Pré-visualização e servidores de desenvolvimento
|
||||
|
||||
Quando você inicia um servidor de desenvolvimento, o OpenChamber pode abri-lo logo dentro do app em vez de uma aba separada do navegador — para que você veja seu site ao lado do chat, capture seu console e aponte para elementos para perguntar sobre eles.
|
||||
Quando você sobe um servidor de desenvolvimento, o OpenChamber pode abri-lo dentro do próprio aplicativo em vez de uma aba separada — assim você vê seu site ao lado do chat e pode apontar para elementos para perguntar sobre eles.
|
||||
|
||||
## Abrir um preview
|
||||
## Abrir um servidor de desenvolvimento
|
||||
|
||||
O OpenChamber observa a saída do terminal em busca de um endereço local (a linha `Local:` que ferramentas como Vite, Next.js ou Astro imprimem). Quando ele detecta um:
|
||||
Abra o painel do navegador pelo botão do globo no cabeçalho. Se já houver um servidor rodando, ele aparece na lista e um clique o abre: o OpenChamber o encontra olhando o que está de fato escutando na sua máquina, então funciona independentemente de como você o iniciou.
|
||||
|
||||
- no terminal, aparece um botão **Open preview**
|
||||
- uma [ação de projeto](/pt-br/project-actions/) com auto-open ativado o abre para você
|
||||
- um link local em uma mensagem do chat também pode abri-lo
|
||||
Um servidor também abre sozinho quando:
|
||||
|
||||
O site carrega no painel lateral. Apenas endereços locais (na sua própria máquina) podem ter preview.
|
||||
- você pressiona **Abrir pré-visualização** em um endereço local no terminal
|
||||
- uma [ação de projeto](/project-actions/) com abertura automática inicia um
|
||||
- você segue um link local em uma mensagem do chat
|
||||
|
||||
## Console e inspeção
|
||||
Você sempre pode digitar o endereço à mão. Um simples `localhost:5173` é entendido como `http://`, então não precisa escrever o esquema.
|
||||
|
||||
No painel de preview você pode:
|
||||
## Trabalhando com um OpenChamber remoto
|
||||
|
||||
- acompanhar o **console** da página — erros, avisos e logs, filtrados como você quiser
|
||||
- ativar o **inspect**, clicar em qualquer elemento e enviar uma nota sobre ele — seletor, estilos, posição e uma captura de tela — direto no chat
|
||||
Quando o OpenChamber roda em outra máquina, o servidor de desenvolvimento está *nessa* máquina — `localhost` no seu notebook aponta para outro lugar completamente. O aplicativo desktop resolve isso: ele abre uma porta local que leva a conexão até o servidor remoto, de modo que a página carrega normalmente, com recarga a quente e ferramentas de desenvolvedor funcionando. Você continua digitando o endereço que espera; o encanamento não atrapalha.
|
||||
|
||||
Esta é a forma mais rápida de dizer ao agente "este botão, aqui" sem descrevê-lo.
|
||||
Isso exige o aplicativo desktop. Em uma aba do navegador, só é possível abrir servidores da sua própria máquina.
|
||||
|
||||
## Anotar a página
|
||||
|
||||
Veja [Painel do navegador](/desktop-browser/) para apontar elementos, desenhar sobre a página e mandar tudo para o chat.
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Ações de Projeto](/pt-br/project-actions/) — abra um servidor automaticamente ao iniciá-lo
|
||||
- [Navegador no Desktop](/pt-br/desktop-browser/) — as mesmas ferramentas para qualquer página, no desktop
|
||||
- [Ações de projeto](/project-actions/) — abrir um servidor automaticamente ao iniciá-lo
|
||||
- [Painel do navegador](/desktop-browser/) — anotar páginas e deixar o agente conduzir
|
||||
|
||||
@@ -45,5 +45,6 @@ Os logins de provedores são armazenados pelo OpenCode, não pelo OpenChamber, e
|
||||
|
||||
## Relacionado
|
||||
|
||||
- [Integrações](/pt-br/integrations/) — use uma assinatura Claude ou Cursor como provedor
|
||||
- [Servidores MCP](/pt-br/mcp/) — adicione ferramentas extras para os agentes
|
||||
- [Uso e Cotas](/pt-br/usage/) — acompanhe quanto você já usou
|
||||
|
||||
@@ -12,7 +12,7 @@ Para escrever suas próprias skills, veja [Skills](/pt-br/skills/).
|
||||
## Instalar uma skill
|
||||
|
||||
1. Abra o catálogo.
|
||||
2. Navegue pelas fontes integradas — o repositório de skills da Anthropic e o registro comunitário ClawdHub — ou pesquise.
|
||||
2. Navegue pelas fontes integradas — como o repositório de skills da Anthropic — ou pesquise.
|
||||
3. Escolha uma skill e instale-a.
|
||||
4. Escolha onde instalá-la: para tudo o que você faz, ou apenas no projeto atual.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ A scheduled task runs a prompt for you on a schedule — for example, a daily "s
|
||||
- **daily** — at one or more times each day
|
||||
- **weekly** — on chosen weekdays and times
|
||||
- **once** — a single date and time
|
||||
- **cron** — an arbitrary cron expression
|
||||
4. Set what it does: the prompt to send, and the provider, model, and agent to use. The prompt can be a slash command, like `/review`.
|
||||
5. Save, and make sure the task is enabled.
|
||||
|
||||
@@ -22,6 +23,48 @@ You can run any task immediately with **run now** to check it does what you expe
|
||||
|
||||
Check **Run as goal** to make the run pursue its prompt to completion instead of stopping after one reply — see [Session Goals](/session-goals/).
|
||||
|
||||
## Loops: scheduled tasks as markdown files
|
||||
|
||||
A **loop** is a scheduled task defined as a portable markdown file you can commit to your repo. Drop a file into `.agents/loops/` and open the Scheduled Tasks list to sync it — no server restart needed:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: daily-digest
|
||||
schedule: "0 9 * * *"
|
||||
enabled: true
|
||||
model: anthropic/claude-sonnet-4-5
|
||||
agent: plan
|
||||
timezone: Europe/Kyiv
|
||||
---
|
||||
Summarize repository changes since yesterday and post the digest.
|
||||
```
|
||||
|
||||
### Where files live
|
||||
|
||||
- **Project scope** — `.agents/loops/*.md` in the project directory or any ancestor directory up to the git worktree root.
|
||||
- **User scope** — `~/.agents/loops/*.md` applies to every project you open.
|
||||
|
||||
If a project loop and a user loop share a name, the project loop wins.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `name` | Task name (required, max 80 characters). |
|
||||
| `schedule` | Cron expression (required) — loop files are cron-only. |
|
||||
| `enabled` | Set `true` to run. Loops are **off by default**, so committing a file never starts running a task on its own. |
|
||||
| `model` | `provider/model` (required), e.g. `anthropic/claude-sonnet-4-5`. |
|
||||
| `agent` | Agent to use (optional). |
|
||||
| `timezone` | IANA timezone (optional, defaults to the server zone). |
|
||||
| body | The execution prompt (required). Can be a slash command, like `/review src/`. |
|
||||
|
||||
### How loops behave
|
||||
|
||||
- The **file is authoritative** while it exists. **Edit** opens it in the built-in file editor, the enabled toggle updates its frontmatter, and deleting the task deletes the markdown file after confirmation. **Run now** remains available.
|
||||
- Runtime state (last run, next run, status) lives in the project config and is never written back into the markdown file.
|
||||
- Renaming the `name` field renames the task in place. If a loop file temporarily fails to parse (mid-edit, merge conflict), its task is kept with the last good definition until the file is fixed.
|
||||
- `daily`/`weekly`/`once` schedules and goal settings remain UI-only; loop files are always cron.
|
||||
|
||||
## What success looks like
|
||||
|
||||
After a run, the task shows when it last ran, whether it succeeded, and a link to the session it created. If a run fails, the error is shown there too.
|
||||
|
||||
@@ -12,7 +12,7 @@ For writing your own skills, see [Skills](/skills/).
|
||||
## Install a skill
|
||||
|
||||
1. Open the catalog.
|
||||
2. Browse the built-in sources — the Anthropic skills repo and the ClawdHub community registry — or search.
|
||||
2. Browse the built-in sources — like the Anthropic skills repo — or search.
|
||||
3. Pick a skill and install it.
|
||||
4. Choose where to install it: for everything you do, or just the current project.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ description: Дозвольте агенту керувати сесіями, wo
|
||||
|
||||
## Увімкнення та вимкнення
|
||||
|
||||
Відкрийте **Налаштування → Загальні → OpenCode CLI**, змініть **Інструмент керування для агентів**, потім виберіть **Save + Reload**. Налаштування застосовується після перезапуску керованого сервера OpenCode.
|
||||
Відкрийте **Налаштування → Загальні → Інструменти OpenChamber** і змініть **Інструмент керування для агентів**. Налаштування застосовується після перезапуску керованого сервера OpenCode, який OpenChamber запропонує як **Apply & Restart**.
|
||||
|
||||
Інструмент недоступний, коли OpenChamber підключається до зовнішнього сервера OpenCode через `OPENCODE_HOST` чи skip-start, а також у розширенні VS Code. Десктопні та вебінсталяції з керованим OpenChamber сервером OpenCode підтримують його автоматично.
|
||||
|
||||
@@ -37,3 +37,4 @@ description: Дозвольте агенту керувати сесіями, wo
|
||||
- [Заплановані задачі](/uk/scheduled-tasks/)
|
||||
- [Сесії worktree](/uk/worktrees/)
|
||||
- [Цілі сесії](/uk/session-goals/)
|
||||
- [Панель браузера](/uk/desktop-browser/) — інструмент OpenChamber Web — дивитися на сторінку й керувати нею
|
||||
|
||||
@@ -1,22 +1,64 @@
|
||||
---
|
||||
title: Десктопний браузер
|
||||
description: Переглядайте будь-яку сторінку всередині десктопного застосунку, з інспекцією та перехопленням консолі.
|
||||
title: Панель браузера
|
||||
description: Переглядайте будь-яку сторінку в застосунку, анотуйте її та дозвольте агенту нею керувати.
|
||||
---
|
||||
|
||||
# Десктопний браузер
|
||||
# Панель браузера
|
||||
|
||||
Десктопний застосунок має вбудований браузер, тож ви можете відкрити будь-яку сторінку просто поруч із чатом, вказувати на елементи, щоб запитати про них, і перехоплювати консоль сторінки. Відкрийте його з кнопки глобуса в заголовку застосунку.
|
||||
Панель браузера відкриває будь-яку сторінку просто поруч із чатом. Відкрийте її кнопкою глобуса в заголовку застосунку.
|
||||
|
||||
> Десктопний браузер — це функція **лише для десктопа**. У вебі панель [перегляду](/uk/preview/) пропонує ті самі інструменти інспекції та консолі для вашого локального dev-сервера.
|
||||
У десктопному застосунку це справжній браузер: ваші входи зберігаються, гаряче перезавантаження працює, а інструменти розробника — за один клік. У вкладці веббраузера панель теж покаже сторінку, але не зможе зазирнути всередину — інструменти анотацій нижче доступні лише на десктопі.
|
||||
|
||||
## Інспекція та анотування
|
||||
Сторінки, відкриті тут, не можуть скористатися вашою камерою, мікрофоном чи місцем перебування: такі запити відхиляються.
|
||||
|
||||
Увімкніть **inspect** і клікніть будь-який елемент на сторінці. OpenChamber перехоплює нотатку про нього — що це, його стилі, де він розташований, і знімок екрана — і прикріплює її до вашого повідомлення в чаті. Це найшвидший спосіб сказати агентові «ось цей елемент, прямо тут».
|
||||
## Панель інструментів
|
||||
|
||||
## Перехоплення консолі
|
||||
Адресний рядок памʼятає сторінки, які ви відкривали в цьому проєкті, і пропонує їх під час набору, звіряючись із частиною адреси або назвою сторінки. Стрілки рухають списком, Enter відкриває підсвічений запис, а кнопка в рядку прибирає його зі списку.
|
||||
|
||||
Браузер збирає вивід консолі сторінки — помилки, попередження та логи — щоб ви могли фільтрувати й читати його, не відкриваючи інструменти розробника.
|
||||
Поруч — **перезавантаження**, а також **жорстке перезавантаження**, яке ігнорує кеш, коли зміна вперто не показується, і керування масштабом, що змінює лише сторінку.
|
||||
|
||||
## Пов'язане
|
||||
**Очистити куки** та **Очистити кеш** стосуються тільки цієї панелі. Вашої сесії OpenChamber та інших вікон це не торкається.
|
||||
|
||||
- [Перегляд і dev-сервери](/uk/preview/) — ті самі інструменти для вашого локального dev-сервера
|
||||
## Анотувати сторінку
|
||||
|
||||
Натисніть **Анотувати** — над сторінкою зʼявиться панель із трьома інструментами:
|
||||
|
||||
- **Елемент** — клікніть на елемент. Клік по іншому переносить вибір, повторний клік по тому самому — знімає його.
|
||||
- **Область** — обведіть ділянку рамкою, коли йдеться більш ніж про один елемент.
|
||||
- **Малювання** — малюйте від руки поверх сторінки.
|
||||
|
||||
Опишіть бажане в полі, що зʼявляється поруч із позначкою, і натисніть **Додати** — або просто Enter. У повідомленні чату зʼявиться картка з усім, що ви позначили, з вашою нотаткою і зі знімком видимої сторінки, на якому намальовано твої позначки — тож можна сказати «оця кнопка, трохи круглішу», а не описувати, де вона.
|
||||
|
||||
Сама сторінка при цьому не змінюється — анотація лише позначає те, що є. `Esc` скасовує й закриває панель.
|
||||
|
||||
## Дозволити агенту керувати
|
||||
|
||||
Агент може сам користуватися панеллю браузера — відкривати сторінку, читати, що на ній, клікати, вводити текст, прокручувати й перемикатися між мобільним, планшетним і десктопним розкладами — щоб перевіряти власну роботу, а не просити про це вас. Ви бачитимете це в панелі.
|
||||
|
||||
Агент не може виконувати довільний код на сторінці. Браузер зберігає ваші справжні входи, тож агент обмежений переліченими діями.
|
||||
|
||||
Він також може зберегти знімок того, що бачить, у `.openchamber/screenshots/` вашого проєкту й показати його у відповіді. Саме це робить можливим «до і після», а файл лишається на місці, щоб потім прикріпити його до pull request.
|
||||
|
||||
Дії з браузером — це **інструмент OpenChamber Web**, який вмикається й вимикається окремо в **Налаштування → Загальні → Інструменти OpenChamber**.
|
||||
|
||||
Для цього потрібен десктопний застосунок: сторінкою у вкладці веббраузера керувати не вийде.
|
||||
|
||||
## Розмір і оформлення
|
||||
|
||||
Натисніть кнопку телефона, щоб відкрити панель пристроїв. Оберіть пресет або
|
||||
введіть ширину й висоту — сторінка буде викладена саме в цьому розмірі, а якщо
|
||||
не вміщається в панель, її буде зменшено візуально. Сама сторінка при цьому
|
||||
вимірює себе в тому розмірі, який ви задали.
|
||||
|
||||
Там же можна змусити сторінку показатися світлою чи темною, не змінюючи нічого
|
||||
на своїй машині. З DevTools вони не поєднуються: у сторінки може бути лише один
|
||||
приєднаний зневаджувач, тож DevTools доведеться спершу закрити.
|
||||
|
||||
## Інструменти розробника
|
||||
|
||||
Натисніть кнопку термінала на панелі, щоб відкрити власні інструменти розробника Chromium для сторінки — консоль, мережу, елементи, усе як зазвичай.
|
||||
|
||||
## Повʼязане
|
||||
|
||||
- [Перегляд і dev-сервери](/preview/) — як відкрити запущений застосунок, зокрема на віддаленій машині
|
||||
- [Інструмент керування для агентів](/uk/agent-control-tool/) — сесії, worktree й заплановані задачі просто з чату
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Інтеграції
|
||||
description: Використовуйте підписки Claude або Cursor як провайдерів.
|
||||
---
|
||||
|
||||
# Інтеграції
|
||||
|
||||
Інтеграція — це невеликий плагін, що додає провайдера до OpenChamber на основі підписки, яка в вас уже є. Керувати ними можна в **Settings → Integrations**.
|
||||
|
||||
> **Експериментальна функція.** Ми прагнемо дотримуватися політик провайдерів, але обмеження та блокування облікових записів залишаються рішенням кожного провайдера. Використовуйте інтеграції на власний ризик.
|
||||
|
||||
Доступні інтеграції:
|
||||
|
||||
- **Claude Code** — ваша підписка Claude Pro або Max, без API-ключів
|
||||
- **Cursor** — ліміти моделей вашої підписки Cursor
|
||||
|
||||
## Встановлення інтеграції
|
||||
|
||||
1. Відкрийте **Settings → Integrations**.
|
||||
2. Знайдіть інтеграцію та натисніть **Install**.
|
||||
3. Перезапустіть OpenCode, коли про це попросять, — провайдер з'явиться після перезапуску.
|
||||
4. Натисніть **Set up** і увійдіть. Після цього моделі з'являться в перемикачі моделей у чаті.
|
||||
|
||||
Інтеграції встановлюються для вашого користувача, тож працюють у всіх проєктах. Оновити або видалити їх можна в будь-який момент з тієї ж карточки.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code використовує вашу підписку Claude Pro або Max — без API-ключів і без окремого застосунку Claude.
|
||||
|
||||
1. Встановіть інтеграцію (вище).
|
||||
2. Натисніть **Set up** і увійдіть. Якщо у вас ще немає Claude Code CLI, програма встановлення спершу запропонує його встановити, а потім виконає вхід.
|
||||
|
||||
Claude Code — єдина інтеграція тут, яка вимагає встановленого та залогіненого CLI свого провайдера. Для Cursor CLI не потрібен.
|
||||
|
||||
**Як захищається ваш обліковий запис Claude:** ця інтеграція використовує офіційний Claude Agent SDK від Anthropic і ваш встановлений Claude Code CLI. Вона не перехоплює OAuth, не витягує й не відтворює браузерні токени, не видає себе за непідтримуваний клієнт і не обходить процес автентифікації Anthropic. Усе працює через підтримуваний Anthropic шлях доступу, тож інтеграція не несе ризику блокування облікового запису, пов'язаного з перехопленням токенів або несанкціонованими способами автентифікації.
|
||||
|
||||
## Cursor
|
||||
|
||||
Cursor робить доступними в OpenChamber моделі, що входять у вашу підписку Cursor.
|
||||
|
||||
1. Встановіть інтеграцію (вище).
|
||||
2. Натисніть **Set up**, відкрийте посилання та підтвердьте доступ у браузері. API-ключ не потрібен. Список моделей завантажиться автоматично після входу.
|
||||
|
||||
## Оновлення та видалення
|
||||
|
||||
- **Update** встановлює найновішу опубліковану версію плагіна.
|
||||
- **Remove** видаляє плагін із конфігурації OpenCode. Провайдер перестане завантажуватися після оновлення OpenCode.
|
||||
|
||||
Якщо карточка повідомляє, що записи потребують ручного керування, натисніть **Manage plugins** і приберіть дублікати там.
|
||||
|
||||
## Пов'язане
|
||||
|
||||
- [Провайдери, моделі та агенти](/uk/providers/) — підключення інших провайдерів і вибір моделей
|
||||
- [Використання та квоти](/uk/usage/) — відстежуйте, скільки ви витратили
|
||||
@@ -21,6 +21,64 @@ OpenChamber використовує вбудовані промпти за ла
|
||||
|
||||
Передумали? Кожен промпт має **reset to default**, а ще є **reset all**, якщо хочете почати спочатку всюди.
|
||||
|
||||
## Де використовується кожен промпт
|
||||
|
||||
Для кожного промпту нижче вказано, де він виконується та який тригер його запускає. Перевірте тригер перед редагуванням, щоб розуміти, який процес ви змінюєте.
|
||||
|
||||
### Git
|
||||
|
||||
| Промпт | Де виконується | Коли спрацьовує |
|
||||
| --- | --- | --- |
|
||||
| Генерація коміту | Кнопка генерації в полі коміту у git-поданні та на мобільному екрані Changes | Ви генеруєте повідомлення коміту. Підставляються вибрані файли та теми нещодавніх комітів гілки, щоб стиль відповідав вашому репозиторію. |
|
||||
| Генерація PR | Форма створення pull request у вкладці PR git-подання | Ви генеруєте заголовок і опис PR. Підставляються базова та головна гілки, коміти та змінені файли між ними, ваш додатковий контекст і PR-шаблон репозиторію, якщо він є. |
|
||||
| Розв'язання конфліктів merge/rebase | Діалог конфліктів у git-поданні, коли merge або rebase зупинився на конфліктах | Ви обираєте "Resolve in current session" або "Resolve in new session". Агент читає конфліктні файли, пропонує стратегію розв'язання для кожного файлу та чекає на ваше підтвердження перед редагуванням, індексацією чи продовженням операції. |
|
||||
| Розв'язання конфліктів cherry-pick | Розділ "Re-integrate commits" для сесії у worktree | Перенесення комітів сесії на цільову гілку впирається в конфлікт, і ви передаєте його агентові. Агент розв'язує конфлікти в тимчасовому worktree, індексує файли та продовжує cherry-pick. |
|
||||
|
||||
### GitHub
|
||||
|
||||
| Промпт | Де виконується | Коли спрацьовує |
|
||||
| --- | --- | --- |
|
||||
| Рев'ю PR | Пікер "Link GitHub PR" у меню вкладень композера та діалог нового worktree | Два тригери. Прикріплення PR як контексту готує інструкції, які надсилаються разом із вашим наступним повідомленням. Створення сесії у worktree з PR використовує промпт як перше повідомлення сесії з повним контекстом PR. |
|
||||
| Рев'ю issue | Діалог нового worktree, коли worktree створюється з issue | Перше повідомлення нової сесії рев'ю issue, з тілом і коментарями як контекстом. |
|
||||
| Рев'ю провалених перевірок PR / коментарів PR / окремого коментаря PR | — | Нині не надсилаються жодним процесом. Подання PR раніше запускало їх кнопками швидкого рев'ю; тепер провалені перевірки й коментарі прикріплюються як чернетки контексту чату. Вони лишаються редагованими, щоб наявні перевизначення продовжували працювати. |
|
||||
|
||||
### Planning
|
||||
|
||||
| Промпт | Де виконується | Коли спрацьовує |
|
||||
| --- | --- | --- |
|
||||
| Планування з todo | Панель Todos у проєктній бічній панелі | Ви надсилаєте todo в сесію або нову сесію у worktree. Текст todo стає видимим повідомленням; інструкції перетворюють його на планувальний діалог із питаннями, а не стрибок одразу в імплементацію. |
|
||||
| Покращення плану | Дія "Improve" для збереженого плану у поданні Plans | Ви надсилаєте збережений план у потік покращення. Агент спершу читає файл плану, потім пропонує зміни на основі поточного стану репозиторію та пропонує відредагувати той самий файл. |
|
||||
| Імплементація плану | Дія "Implement" для збереженого плану | Ви надсилаєте збережений план у потік імплементації. Агент читає файл плану та імплементує його від початку до кінця без розширення обсягу, зберігаючи корективи плану назад у файл, якщо сам план виявився хибним. |
|
||||
|
||||
### Session
|
||||
|
||||
Більшість із них живлять слеш-команди, які вводяться в композері. Більшість також доступні як стартові чіпи на чернетці нової сесії.
|
||||
|
||||
| Промпт | Де виконується | Коли спрацьовує |
|
||||
| --- | --- | --- |
|
||||
| Тур кодовою базою | `/explore` | Ви просите загальний огляд кодової бази. |
|
||||
| Підсумок сесії | `/summary`, необов'язково `/summary <тема>` | Ви підсумовуєте поточну розмову — зручно для передачі в нову сесію. Потрібна наявна сесія. |
|
||||
| Рев'ю робочої області | `/workspace-review` | Ви просите агента переглянути поточний diff робочої області на намір, коректність і безпеку. |
|
||||
| Планування фічі | `/plan-feature` | Ви перетворюєте грубу ідею фічі на план імплементації через керований діалог питань і відповідей. |
|
||||
| Формулювання Goal | `/craft-goal`, необов'язково `/craft-goal <ідея>` | Ви перетворюєте ідею на перевірювану ціль Goal для діалогу Goal. |
|
||||
| Catch up | `/catch-up` | Ви повертаєтеся до проєкту й питаєте, на чому зупинилися і що робити далі. |
|
||||
| Дебаг | `/debug` | Ви досліджуєте баг: агент формує гіпотези, підтверджує кореневу причину з коду і лише тоді пропонує виправлення. |
|
||||
| Зважування варіантів | `/weigh` | Ви знаєте, що будувати, але не як. Агент порівнює два-три підходи та рекомендує один. |
|
||||
| Fusion | Дія "Run fusion" на групі multi-run | Ви об'єднуєте виводи кількох запусків в одну відповідь. Виводи запусків додаються після інструкцій. |
|
||||
|
||||
### Промпти без сторінки в Settings
|
||||
|
||||
Кілька промптів спрацьовують автоматично й не мають редагованих сторінок у Settings:
|
||||
|
||||
| Промпт | Коли спрацьовує |
|
||||
| --- | --- |
|
||||
| Заплановане завдання | `/schedule-task`, необов'язково з початковою ідеєю. Веде діалог, який визначає заплановане завдання. |
|
||||
| Handoff для рев'ю | `/handoff-review` або кнопка Review у поданні diff із увімкненим handoff. Генерує handoff у робочій сесії. |
|
||||
| Стартове повідомлення сесії рев'ю | Перше повідомлення згенерованої сесії рев'ю — з handoff, якщо він створений, або без нього. |
|
||||
| Відгук рев'ю / відповідь імплементатора | Переносять повідомлення між двома сесіями: відгук рев'юера повертається в сесію імплементації, а відповідь імплементатора — назад у сесію рев'ю. |
|
||||
|
||||
## Пов'язане
|
||||
|
||||
- [Робочі процеси Git і GitHub](/uk/git/) — багато з цих промптів живлять git-процеси
|
||||
- [Нотатки, todo та плани](/uk/notes-todos-plans/) — todo і плани за промптами групи Planning
|
||||
- [Multi-run](/uk/multi-run/) — групи запусків і fusion
|
||||
|
||||
@@ -5,28 +5,31 @@ description: Відкрийте запущений dev-сервер усеред
|
||||
|
||||
# Перегляд і dev-сервери
|
||||
|
||||
Коли ви запускаєте dev-сервер, OpenChamber може відкрити його просто всередині застосунку замість окремої вкладки браузера — щоб ви могли бачити свій сайт поруч із чатом, перехоплювати його консоль і вказувати на елементи, щоб запитати про них.
|
||||
Коли ви запускаєте dev-сервер, OpenChamber може відкрити його просто всередині застосунку замість окремої вкладки браузера — щоб ви бачили свій сайт поруч із чатом і могли вказувати на елементи, щоб запитати про них.
|
||||
|
||||
## Відкриття перегляду
|
||||
## Відкрити dev-сервер
|
||||
|
||||
OpenChamber слідкує за виводом терміналу на предмет локальної адреси (рядок `Local:`, який друкують інструменти на кшталт Vite, Next.js чи Astro). Коли він її помічає:
|
||||
Відкрийте панель браузера кнопкою глобуса в заголовку застосунку. Якщо dev-сервер уже запущено, він буде у списку — один клік, і він відкриється. OpenChamber знаходить його за тим, що насправді слухає порти на вашій машині, тож це працює незалежно від того, як саме ви його запустили.
|
||||
|
||||
- у терміналі з'являється кнопка **Open preview**
|
||||
- [дія проєкту](/uk/project-actions/) з увімкненим автовідкриттям відкриває її за вас
|
||||
- локальне посилання в повідомленні чату теж може її відкрити
|
||||
Dev-сервер також відкривається автоматично, коли:
|
||||
|
||||
Сайт завантажується в бічній панелі. Переглянути можна лише локальні адреси (на вашій власній машині).
|
||||
- ви натискаєте **Відкрити перегляд** на локальній адресі в терміналі
|
||||
- [дія проєкту](/project-actions/) з увімкненим автовідкриттям запускає його
|
||||
- ви переходите за локальним посиланням у повідомленні чату
|
||||
|
||||
## Консоль та інспекція
|
||||
Адресу завжди можна ввести вручну. Простий `localhost:5173` трактується як `http://`, тож схему писати не обовʼязково.
|
||||
|
||||
У панелі перегляду ви можете:
|
||||
## Робота з віддаленим OpenChamber
|
||||
|
||||
- спостерігати за **консоллю** сторінки — помилки, попередження та логи, відфільтровані як вам зручно
|
||||
- увімкнути **inspect**, клікнути будь-який елемент і надіслати нотатку про нього — селектор, стилі, позицію та знімок екрана — прямо в чат
|
||||
Коли OpenChamber працює на іншій машині, його dev-сервер теж на *тій* машині — `localhost` на вашому ноутбуці веде зовсім не туди. Десктопний застосунок робить це за вас: відкриває локальний порт, який проводить зʼєднання до віддаленого dev-сервера, тож сторінка вантажиться як звичайно, з робочим гарячим перезавантаженням і інструментами розробника. Ви й далі вводите очікувану адресу; технічні деталі вам не заважають.
|
||||
|
||||
Це найшвидший спосіб сказати агентові «ось ця кнопка, тут», не описуючи її.
|
||||
Для цього потрібен десктопний застосунок. У вкладці браузера можна відкрити лише dev-сервери на вашій власній машині.
|
||||
|
||||
## Пов'язане
|
||||
## Анотації на сторінці
|
||||
|
||||
- [Дії проєкту](/uk/project-actions/) — автоматично відкривайте сервер при запуску
|
||||
- [Десктопний браузер](/uk/desktop-browser/) — ті самі інструменти для будь-якої сторінки на десктопі
|
||||
Про те, як вказувати на елементи, малювати на сторінці й надсилати все це в чат, читайте в розділі [Панель браузера](/desktop-browser/).
|
||||
|
||||
## Повʼязане
|
||||
|
||||
- [Дії проєкту](/project-actions/) — автовідкриття сервера після запуску
|
||||
- [Панель браузера](/desktop-browser/) — анотації сторінок і керування агентом
|
||||
|
||||
@@ -45,5 +45,6 @@ description: Підключайте AI-провайдерів, обирайте
|
||||
|
||||
## Пов'язане
|
||||
|
||||
- [Інтеграції](/uk/integrations/) — використовуйте підписки Claude або Cursor як провайдерів
|
||||
- [MCP Servers](/uk/mcp/) — додайте агентам додаткові інструменти
|
||||
- [Використання та квоти](/uk/usage/) — відстежуйте, скільки ви витратили
|
||||
|
||||
@@ -12,7 +12,7 @@ description: Переглядайте та встановлюйте готові
|
||||
## Встановлення навички
|
||||
|
||||
1. Відкрийте каталог.
|
||||
2. Перегляньте вбудовані джерела — репозиторій навичок Anthropic та спільнотний реєстр ClawdHub — або скористайтеся пошуком.
|
||||
2. Перегляньте вбудовані джерела — наприклад репозиторій навичок Anthropic — або скористайтеся пошуком.
|
||||
3. Оберіть навичку й установіть її.
|
||||
4. Виберіть, куди встановити: для всього, що ви робите, чи лише для поточного проєкту.
|
||||
|
||||
|
||||
@@ -11,6 +11,20 @@ It explains and orders. It does not judge your code or hand out verdicts — tha
|
||||
|
||||
Open it from the **Walkthrough** icon in the right rail, or from the **AI walkthrough** button in the Changes and Pull Request panels. Both just open the panel; nothing is generated until you press **Generate walkthrough**.
|
||||
|
||||
## How a stop is marked
|
||||
|
||||
Each stop names what it is about, explains it in a sentence or two, and then shows exactly the code it describes. Some stops carry a small tag next to the title:
|
||||
|
||||
| Tag | What it means |
|
||||
| --- | --- |
|
||||
| **Key change** | This stop drives the rest of the change, or carries most of its risk. Read it closely and read it first. |
|
||||
| **Context** | A supporting change, included so the rest makes sense. Safe to skim. |
|
||||
| *(no tag)* | An ordinary step in the reading order. |
|
||||
|
||||
The tag is about **where to spend your attention**, not about the quality of the code. A stop is never marked because something was found wrong in it — the walkthrough reports no findings, no severities, and no verdicts. If you want code judged, that is the **Review** action in [Git & GitHub](/git/).
|
||||
|
||||
The only marks that do report a problem are **Outdated** and **Not covered**, and both are about the walkthrough itself going out of date rather than about your code — see below.
|
||||
|
||||
## What it can review
|
||||
|
||||
| Scope | What it covers |
|
||||
|
||||
@@ -28,7 +28,7 @@ description: 让智能体从聊天中管理 OpenChamber 会话、worktree 和计
|
||||
|
||||
## 开启或关闭工具
|
||||
|
||||
打开 **设置 → 常规 → OpenCode CLI**,更改 **智能体控制工具**,然后选择 **Save + Reload**。该设置会在托管的 OpenCode 服务器重启后生效。
|
||||
打开 **设置 → 常规 → OpenChamber 工具**,更改 **智能体控制工具**。该设置会在托管的 OpenCode 服务器重启后生效,OpenChamber 会以 **Apply & Restart** 的形式提示重启。
|
||||
|
||||
当 OpenChamber 通过 `OPENCODE_HOST` 或 skip-start 连接外部 OpenCode 服务器时,或在 VS Code 扩展中,此工具不可用。使用 OpenChamber 托管 OpenCode 服务器的桌面端和 Web 安装会自动支持此工具。
|
||||
|
||||
@@ -37,3 +37,4 @@ description: 让智能体从聊天中管理 OpenChamber 会话、worktree 和计
|
||||
- [计划任务](/zh-cn/scheduled-tasks/)
|
||||
- [Worktree 会话](/zh-cn/worktrees/)
|
||||
- [会话目标](/zh-cn/session-goals/)
|
||||
- [浏览器面板](/zh-cn/desktop-browser/) — 用于查看并操作页面的 OpenChamber Web 工具
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
---
|
||||
title: 桌面浏览器
|
||||
description: 在桌面应用内部浏览任意页面,并提供检查和控制台捕获功能。
|
||||
title: 浏览器面板
|
||||
description: 在应用内浏览任意页面、为其添加标注,并让智能体操作它。
|
||||
---
|
||||
|
||||
# 桌面浏览器
|
||||
# 浏览器面板
|
||||
|
||||
桌面应用内置了浏览器,因此你可以在聊天旁边直接打开任意页面、指向元素来询问相关问题,并捕获页面的控制台。从应用标题栏的地球按钮打开它。
|
||||
浏览器面板会在聊天旁边打开任意页面。用应用标题栏的地球按钮打开它。
|
||||
|
||||
> 桌面浏览器是一项**仅限桌面**的功能。在网页端,[预览](/zh-cn/preview/) 面板为你的本地开发服务器提供相同的检查与控制台工具。
|
||||
在桌面应用中,它是一个真正的浏览器:登录状态会保留,热重载可用,开发者工具一键即达。在浏览器标签页中,面板仍能显示页面,但无法查看其内部——下面的标注工具仅限桌面端。
|
||||
|
||||
## 检查与标注
|
||||
在这里打开的页面无法使用你的摄像头、麦克风或位置:这类请求会被拒绝。
|
||||
|
||||
开启 **inspect** 并点击页面上的任意元素。OpenChamber 会捕获关于它的说明 — 它是什么、它的样式、它所处的位置,以及一张截图 — 并将其附加到你的聊天消息中。这是告诉智能体“就这个元素,就在这里”的最快方式。
|
||||
## 工具栏
|
||||
|
||||
## 控制台捕获
|
||||
地址栏会记住你在这个项目里打开过的页面,并在你输入时给出候选,按地址或页面标题的任意片段匹配。方向键在列表中移动,回车打开选中的一项,行上的按钮把它从列表中移除。
|
||||
|
||||
浏览器会收集页面的控制台输出 — 错误、警告和日志 — 因此你无需打开开发者工具即可筛选和阅读它。
|
||||
旁边是**重新加载**,还有在改动怎么都不出现时忽略缓存的**强制重新加载**,以及只缩放页面的缩放控件。
|
||||
|
||||
## 相关内容
|
||||
**清除 Cookie** 和**清除缓存数据**只作用于这个面板。你的 OpenChamber 会话和其他窗口不受影响。
|
||||
|
||||
- [预览与开发服务器](/zh-cn/preview/) — 为你的本地开发服务器提供相同的工具
|
||||
## 为页面添加标注
|
||||
|
||||
按 **标注**,页面上方会出现一个包含三种工具的工具条:
|
||||
|
||||
- **元素** — 点击某个元素。点击另一个会移动选择,再次点击同一个则取消。
|
||||
- **区域** — 当要说的内容不止一个元素时,拖拽出矩形框住那块区域。
|
||||
- **绘制** — 在页面上自由手绘。
|
||||
|
||||
在标记旁出现的输入框里写下你的想法,然后按 **附加**,或直接按回车。你的聊天消息会收到一张卡片,包含你标记的全部内容、你的备注,以及一张画上你的标记的可见页面截图——于是你可以说"这个按钮,再圆一点",而不必描述它在哪里。
|
||||
|
||||
页面本身不会被修改——标注只是标记已有的内容。按 `Esc` 取消并关闭工具条。
|
||||
|
||||
## 让智能体操作
|
||||
|
||||
智能体可以自己使用浏览器面板——打开页面、读取页面内容、点击、输入、滚动,并在移动端、平板和桌面端布局之间切换——从而自行检查工作成果,而不必来问你。你可以在面板中看到这个过程。
|
||||
|
||||
智能体无法在页面中执行任意代码。浏览器保留着你真实的登录状态,因此它只能执行上述操作。
|
||||
|
||||
它还可以把当前看到的画面保存为图片,放进项目里的 `.openchamber/screenshots/`,并在回复中展示给你。「改动前后」的对比正是靠这一点,而文件会留在那里,方便附到 pull request 上。
|
||||
|
||||
浏览器相关的动作属于 **OpenChamber Web 工具**,可在**设置 → 通用 → OpenChamber 工具**中单独开关。
|
||||
|
||||
这需要桌面应用:在浏览器标签页中显示的页面无法被操作。
|
||||
|
||||
## 开发者工具
|
||||
|
||||
按工具条上的终端按钮,即可打开 Chromium 自带的页面开发者工具——控制台、网络、元素,一应俱全。
|
||||
|
||||
## 相关
|
||||
|
||||
- [预览与开发服务器](/preview/) — 打开正在运行的应用,包括远程机器上的
|
||||
- [智能体控制工具](/zh-cn/agent-control-tool/) — 在聊天中管理会话、worktree 和计划任务
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: 集成
|
||||
description: 将你的 Claude 或 Cursor 订阅用作提供商。
|
||||
---
|
||||
|
||||
# 集成
|
||||
|
||||
集成是一个小型插件,它使用你已有的订阅为 OpenChamber 添加一个提供商。你可以在 **Settings → Integrations** 中管理它们。
|
||||
|
||||
> **实验性功能。**我们力求遵守提供商的政策,但帐户限制和暂停仍由各提供商决定。请自行承担使用集成的风险。
|
||||
|
||||
可用的集成:
|
||||
|
||||
- **Claude Code** — 你的 Claude Pro 或 Max 套餐,无需 API 密钥
|
||||
- **Cursor** — 你的 Cursor 套餐的模型额度
|
||||
|
||||
## 安装集成
|
||||
|
||||
1. 打开 **Settings → Integrations**。
|
||||
2. 找到所需的集成并选择 **Install**。
|
||||
3. 在提示时重启 OpenCode — 重启后提供商就会出现。
|
||||
4. 选择 **Set up** 并登录。之后模型会出现在聊天室的模型选择器中。
|
||||
|
||||
集成按用户安装,因此在所有项目中都可用。你可以随时在同一张卡片上更新或移除它们。
|
||||
|
||||
## Claude Code
|
||||
|
||||
Claude Code 使用你的 Claude Pro 或 Max 套餐 — 无需 API 密钥,也无需单独的 Claude 应用。
|
||||
|
||||
1. 安装集成(见上文)。
|
||||
2. 选择 **Set up** 并登录。如果你还没有 Claude Code CLI,安装向导会先提供安装,然后再登录。
|
||||
|
||||
Claude Code 是这里唯一要求安装并登录其提供商 CLI 的集成。Cursor 不需要其 CLI。
|
||||
|
||||
**你的 Claude 账户如何受到保护:** 此集成使用 Anthropic 官方的 Claude Agent SDK 和你已安装的 Claude Code CLI。它不会劫持 OAuth,不会提取或重放浏览器令牌,不会冒充不受支持的客户端,也不会绕过 Anthropic 的身份验证。它始终运行在 Anthropic 支持的访问路径上,因此不会带来与令牌劫持或未授权身份验证变通手段相关的封号风险。
|
||||
|
||||
## Cursor
|
||||
|
||||
Cursor 让你的 Cursor 套餐中包含的模型可以在 OpenChamber 中使用。
|
||||
|
||||
1. 安装集成(见上文)。
|
||||
2. 选择 **Set up**,打开链接并在浏览器中授权访问。无需 API 密钥。登录后模型列表会自动加载。
|
||||
|
||||
## 更新或移除
|
||||
|
||||
- **Update** 安装插件的最新发布版本。
|
||||
- **Remove** 从你的 OpenCode 配置中删除该插件。OpenCode 重新加载后,该提供商将不再加载。
|
||||
|
||||
如果卡片提示条目需要手动管理,请选择 **Manage plugins** 并在那里清理重复项。
|
||||
|
||||
## 相关内容
|
||||
|
||||
- [提供商、模型与智能体](/zh-cn/providers/) — 连接其他提供商并选择模型
|
||||
- [用量与配额](/zh-cn/usage/) — 跟踪你的使用量
|
||||
@@ -21,6 +21,64 @@ description: 自定义 OpenChamber 自动化流程背后的内置提示词。
|
||||
|
||||
改主意了?每个提示词都有 **reset to default**,如果你想在所有地方重新开始,还有一个 **reset all**。
|
||||
|
||||
## 每个提示词在哪里使用
|
||||
|
||||
下表列出每个提示词的运行位置和触发时机。编辑前先看清触发条件,你就知道自己在改哪个流程。
|
||||
|
||||
### Git
|
||||
|
||||
| 提示词 | 运行位置 | 触发时机 |
|
||||
| --- | --- | --- |
|
||||
| 提交信息生成 | git 视图提交框中的生成按钮,以及移动端 Changes 页面 | 你生成提交信息时。会填入选中的文件和分支最近的提交主题,让提交信息符合仓库的现有风格。 |
|
||||
| PR 生成 | git 视图 PR 标签页中的创建 pull request 表单 | 你生成 PR 标题和正文时。会填入 base 和 head 分支、两者之间的提交和变更文件、你补充的附加上下文,以及仓库的 PR 模板(如果存在)。 |
|
||||
| merge/rebase 冲突解决 | git 视图中的冲突对话框,当 merge 或 rebase 因冲突停止时 | 你选择 "Resolve in current session" 或 "Resolve in new session"。智能体会阅读冲突文件,为每个文件提出解决策略,并等你确认后才编辑、暂存或继续操作。 |
|
||||
| cherry-pick 冲突解决 | worktree 会话的 "Re-integrate commits" 区域 | 把会话的提交迁移到目标分支时遇到冲突并交给智能体。智能体在临时 worktree 中解决冲突、暂存文件并继续 cherry-pick。 |
|
||||
|
||||
### GitHub
|
||||
|
||||
| 提示词 | 运行位置 | 触发时机 |
|
||||
| --- | --- | --- |
|
||||
| PR 审阅 | 输入框附件菜单中的 "Link GitHub PR" 选择器,以及新 worktree 对话框 | 两个触发点。把 PR 附为上下文时会渲染指令,并随你的下一条消息发出。从 PR 创建 worktree 会话时,该提示词成为会话的开场消息,并附上完整 PR 上下文。 |
|
||||
| issue 审阅 | 新 worktree 对话框,当你从 issue 创建 worktree 时 | 新会话的开场消息会审阅该 issue,并将其正文和评论附为上下文。 |
|
||||
| PR 失败检查 / PR 评论 / 单条 PR 评论 | — | 目前没有任何流程发送它们。PR 视图过去通过一键审阅操作触发这些提示词;现在失败的检查和评论会改为固定为聊天上下文草稿。保留它们是为了让已有的覆盖配置继续生效。 |
|
||||
|
||||
### Planning
|
||||
|
||||
| 提示词 | 运行位置 | 触发时机 |
|
||||
| --- | --- | --- |
|
||||
| todo 规划 | 项目侧边栏中的 Todos 面板 | 你把一个 todo 发送到会话或新的 worktree 会话。todo 文本成为可见消息;指令会把它变成先提问的规划对话,而不是直接开始实现。 |
|
||||
| 改进计划 | Plans 视图中已保存计划上的 "Improve" 操作 | 你把已保存的计划送入改进流程。智能体先读取计划文件,再基于仓库当前状态提出修改,并主动提出编辑同一个文件。 |
|
||||
| 实现计划 | 已保存计划上的 "Implement" 操作 | 你把已保存的计划送入实现流程。智能体读取计划文件并端到端地实现它,不扩大范围;当计划本身有问题时,会把计划调整保存回该文件。 |
|
||||
|
||||
### Session
|
||||
|
||||
其中大多数由在输入框中输入的斜杠命令驱动。大多数也会以启动芯片的形式出现在新会话草稿页上。
|
||||
|
||||
| 提示词 | 运行位置 | 触发时机 |
|
||||
| --- | --- | --- |
|
||||
| 代码库导览 | `/explore` | 你想要一份代码库的高层次概览。 |
|
||||
| 会话总结 | `/summary`,可选 `/summary <主题>` | 你总结目前的对话 — 适合移交给新会话。需要已存在的会话。 |
|
||||
| 工作区审阅 | `/workspace-review` | 你让智能体从意图、正确性和安全性角度审阅当前的工作区 diff。 |
|
||||
| 功能规划 | `/plan-feature` | 你通过引导式问答对话,把粗略的功能想法变成实现计划。 |
|
||||
| Goal 制定 | `/craft-goal`,可选 `/craft-goal <想法>` | 你把一个想法变成可用于 Goal 对话框的可验证 Goal 目标。 |
|
||||
| 快速追平 | `/catch-up` | 你回到一个项目,想知道进展如何、接下来做什么。 |
|
||||
| 调试 | `/debug` | 你调查一个 bug:智能体提出假设、从代码确认根因,然后才提出修复方案。 |
|
||||
| 权衡选项 | `/weigh` | 你知道要做什么,但不知道怎么做。智能体会比较两三种方案并推荐其一。 |
|
||||
| Fusion | multi-run 组上的 "Run fusion" 操作 | 你把多次运行的输出合并成一个答案。运行输出会附加在指令之后。 |
|
||||
|
||||
### 没有 Settings 页面的提示词
|
||||
|
||||
少数提示词会自动触发,在 Settings 中没有可编辑的页面:
|
||||
|
||||
| 提示词 | 触发时机 |
|
||||
| --- | --- |
|
||||
| 计划任务 | `/schedule-task`,可选附带初始想法。引导完成定义计划任务的对话。 |
|
||||
| 审阅交接 | `/handoff-review`,或 diff 视图中启用交接时的 Review 按钮。在当前工作会话中生成交接内容。 |
|
||||
| 审阅会话开场消息 | 生成的审阅会话的开场消息 — 生成了交接内容时包含它,否则不包含。 |
|
||||
| 审阅反馈 / 实现响应 | 在两个会话之间传递消息:审阅者的反馈回到实现会话,实现者的响应返回审阅会话。 |
|
||||
|
||||
## 相关内容
|
||||
|
||||
- [Git 与 GitHub 工作流](/zh-cn/git/) — 其中许多提示词为 git 流程提供动力
|
||||
- [笔记、todo 与计划](/zh-cn/notes-todos-plans/) — Planning 提示词背后的 todo 和计划
|
||||
- [Multi-run](/zh-cn/multi-run/) — 运行组与 fusion
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
---
|
||||
title: 预览与开发服务器
|
||||
description: 在 OpenChamber 内部打开正在运行的开发服务器。
|
||||
description: 在 OpenChamber 内打开正在运行的开发服务器。
|
||||
---
|
||||
|
||||
# 预览与开发服务器
|
||||
|
||||
当你启动开发服务器时,OpenChamber 可以直接在应用内部打开它,而不是在单独的浏览器标签页中 — 这样你就可以在聊天旁边查看你的站点、捕获它的控制台,并指向元素来询问相关问题。
|
||||
启动开发服务器后,OpenChamber 可以直接在应用内打开它,而不是另开一个浏览器标签页——这样你就能在聊天旁边看到自己的站点,并指着元素提问。
|
||||
|
||||
## 打开预览
|
||||
## 打开开发服务器
|
||||
|
||||
OpenChamber 会监视终端输出中的本地地址(像 Vite、Next.js 或 Astro 这类工具打印的 `Local:` 行)。当它发现一个时:
|
||||
用应用标题栏的地球按钮打开浏览器面板。如果开发服务器已在运行,它会出现在列表里,点一下即可打开:OpenChamber 是根据你机器上实际正在监听的端口找到它的,因此无论你用什么方式启动都能识别。
|
||||
|
||||
- 终端中会出现一个 **Open preview** 按钮
|
||||
- 一个开启了自动打开的 [项目操作](/zh-cn/project-actions/) 会为你打开它
|
||||
- 聊天消息中的本地链接也可以打开它
|
||||
以下情况开发服务器也会自动打开:
|
||||
|
||||
站点会在侧面板中加载。只有本地地址(在你自己的机器上)才能被预览。
|
||||
- 你在终端的本地地址上按 **打开预览**
|
||||
- 开启了自动打开的[项目操作](/project-actions/)启动了一个
|
||||
- 你点击了聊天消息中的本地链接
|
||||
|
||||
## 控制台与检查
|
||||
你随时可以自己输入地址。直接写 `localhost:5173` 会按 `http://` 处理,不必输入协议。
|
||||
|
||||
在预览面板中你可以:
|
||||
## 配合远程 OpenChamber 使用
|
||||
|
||||
- 查看页面的**控制台** — 错误、警告和日志,可按你喜欢的方式筛选
|
||||
- 开启 **inspect**,点击任意元素,并将关于它的说明 — 选择器、样式、位置和一张截图 — 直接发送到聊天
|
||||
当 OpenChamber 运行在另一台机器上时,它的开发服务器也在*那台*机器上——你笔记本上的 `localhost` 指向的完全是别处。桌面应用会替你处理:它打开一个本地端口,把连接一路接到远程开发服务器,于是页面照常加载,热重载和开发者工具都能用。你仍然输入你预期的地址,底层的管道不会碍事。
|
||||
|
||||
这是告诉智能体“就这个按钮,这里”而无需描述它的最快方式。
|
||||
这需要桌面应用。在浏览器标签页中,只能打开你自己机器上的开发服务器。
|
||||
|
||||
## 相关内容
|
||||
## 为页面添加标注
|
||||
|
||||
- [项目操作](/zh-cn/project-actions/) — 启动服务器时自动打开它
|
||||
- [桌面浏览器](/zh-cn/desktop-browser/) — 在桌面端为任意页面提供相同的工具
|
||||
指向元素、在页面上绘制并把这些一起发到聊天的方法,见[浏览器面板](/desktop-browser/)。
|
||||
|
||||
## 相关
|
||||
|
||||
- [项目操作](/project-actions/) — 启动服务器时自动打开
|
||||
- [浏览器面板](/desktop-browser/) — 标注页面并让智能体操作
|
||||
|
||||
@@ -45,5 +45,6 @@ description: 连接 AI 提供商、选择模型并设置智能体。
|
||||
|
||||
## 相关内容
|
||||
|
||||
- [集成](/zh-cn/integrations/) — 将 Claude 或 Cursor 订阅用作提供商
|
||||
- [MCP Servers](/zh-cn/mcp/) — 为智能体添加额外工具
|
||||
- [用量与配额](/zh-cn/usage/) — 跟踪你已使用的量
|
||||
|
||||
@@ -12,7 +12,7 @@ Skills 目录让你能够安装其他人发布的 skill,而不必自己编写
|
||||
## 安装 skill
|
||||
|
||||
1. 打开目录。
|
||||
2. 浏览内置来源 — Anthropic skills 仓库和 ClawdHub 社区注册表 — 或进行搜索。
|
||||
2. 浏览内置来源 — 例如 Anthropic skills 仓库 — 或进行搜索。
|
||||
3. 选择一个 skill 并安装它。
|
||||
4. 选择安装位置:用于你的所有工作,或仅用于当前项目。
|
||||
|
||||
|
||||
@@ -349,6 +349,21 @@
|
||||
"de": "Anbieter, Modelle und Agenten"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Integrations",
|
||||
"link": "/integrations/",
|
||||
"translations": {
|
||||
"uk": "Інтеграції",
|
||||
"zh-CN": "集成",
|
||||
"es": "Integraciones",
|
||||
"pt-BR": "Integrações",
|
||||
"ko": "통합 기능",
|
||||
"pl": "Integracje",
|
||||
"fr": "Intégrations",
|
||||
"ja": "統合機能",
|
||||
"de": "Integrationen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "MCP Servers",
|
||||
"link": "/mcp/",
|
||||
@@ -638,18 +653,18 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Desktop Browser",
|
||||
"label": "Browser Panel",
|
||||
"link": "/desktop-browser/",
|
||||
"translations": {
|
||||
"uk": "Десктопний браузер",
|
||||
"zh-CN": "桌面浏览器",
|
||||
"es": "Navegador de escritorio",
|
||||
"pt-BR": "Navegador desktop",
|
||||
"ko": "데스크톱 브라우저",
|
||||
"pl": "Przeglądarka na pulpicie",
|
||||
"fr": "Navigateur desktop",
|
||||
"ja": "デスクトップブラウザ",
|
||||
"de": "Desktop-Browser"
|
||||
"uk": "Панель браузера",
|
||||
"zh-CN": "浏览器面板",
|
||||
"es": "Panel del navegador",
|
||||
"pt-BR": "Painel do navegador",
|
||||
"ko": "브라우저 패널",
|
||||
"pl": "Panel przeglądarki",
|
||||
"fr": "Panneau navigateur",
|
||||
"ja": "ブラウザパネル",
|
||||
"de": "Browser-Panel"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ bun run electron:dev
|
||||
|
||||
The Electron workspace package trusts Electron's install script so `bun install` downloads the platform runtime in fresh checkouts and worktrees.
|
||||
|
||||
Electron's postinstall (`node install.js`) is run by `bun install` with the system Node. Under Node 24, the bundled `extract-zip@2.0.1` silently unpacks only the first entry of the Electron zip, leaving `dist/` without the binary and `path.txt` missing. To keep this from blocking desktop work:
|
||||
Electron's postinstall (`node install.js`) is run by `bun install` with the system Node. Older Electron releases bundled `extract-zip@2.0.1`, which under Node 24 silently unpacked only the first entry of the Electron zip, leaving `dist/` without the binary and `path.txt` missing. Electron 43+ ships its own fixed extractor (`@electron-internal/extract-zip`), but to keep interrupted or wrong-architecture installs from blocking desktop work:
|
||||
|
||||
- The root `postinstall` runs `ensure-electron.mjs --best-effort`, which detects an incomplete Electron install (missing binary, stale `dist/version`/`path.txt`, or a binary of the wrong architecture) and repairs it by re-running the postinstall under Bun (which extracts correctly), falling back to Node.
|
||||
- `electron-dev.mjs` runs the same check (fail-fast, not best-effort) before launching, so `bun run electron:dev` self-heals even when an install was interrupted.
|
||||
@@ -98,12 +98,16 @@ Desktop clears AppImage `ARGV0` from `process.env` before probing the login shel
|
||||
|
||||
Linux updates are supported only when the packaged app is running from a writable AppImage. Update checks, downloads, and installation report an actionable error when `APPIMAGE` is missing, invalid, or read-only; a missing release feed (`latest-linux.yml` 404 before the first Linux publish) is treated as “no update available”. macOS and Windows updater behavior is unchanged. Release builds keep `latest-linux.yml` (x64) and `latest-linux-arm64.yml` separate and validate each manifest against its AppImage before upload. Linux AppImages download full updates (no `.blockmap` differential channel yet).
|
||||
|
||||
`desktop_restart` does not answer the renderer before the install is decided. On the apply-update path it calls `quitAndInstall()` and keeps the IPC call open until the app quits or `autoUpdater` emits `error`, which the platform installers do asynchronously (a rejected code signature, or a Squirrel session disabled by an earlier failure). A failed install rejects the IPC call so the update dialog can show it, and the quit/install flags are rolled back because the app is staying up. A still-running app after the grace period resolves the call.
|
||||
|
||||
### Updater End-to-End Fixture
|
||||
|
||||
A loopback-only updater fixture is available for contributor QA of N-to-N+1 AppImage replacement and restart behavior. It is test infrastructure, not a user-configurable update source. See [`scripts/updater-e2e-fixture.md`](./scripts/updater-e2e-fixture.md) for the controlled test procedure. Unit tests cover feed selection, check failures, no-update results, and fixture generation; actual AppImage replacement and restart remains a manual native N-to-N+1 release boundary because it requires executing two packaged versions on each supported architecture.
|
||||
|
||||
The package supports macOS, Windows, and Linux desktop features. Linux AppImage builds include in-app window controls, auto-update, system tray (right-click Show / Hide / Close), and launch-at-login (XDG autostart). Opening files in installed apps, installed-app discovery, and FreeDesktop icon lookup (including the default file manager) work on macOS, Windows, and Linux.
|
||||
|
||||
On Windows and Linux, the General setting persisted as `desktopMinimizeToTrayEnabled` keeps the app running in the tray when the main window is **closed**. Minimize — the in-app control, the native title-bar button, and the taskbar — always performs a normal window minimize, so the taskbar entry stays available.
|
||||
|
||||
The macOS menu bar item is enabled by default and can be disabled in General settings. The setting applies after restart; while disabled, Desktop does not create the native tray controller or start the renderer subscriptions, polling, quota refresh, or IPC updates that feed it.
|
||||
|
||||
## Bundled OpenCode CLI
|
||||
@@ -141,8 +145,10 @@ Use an explicit override when testing a different OpenCode CLI build or when a u
|
||||
## Native Features Owned Here
|
||||
|
||||
- Floating Mini Chat windows.
|
||||
- New Mini Chat windows default to the managed Chats target. Explicit project/worktree drafts retain their target, existing managed chat sessions reopen in their own directory, and the compact header omits project/branch metadata for Chats. Opening a managed draft back in the main window preserves that target.
|
||||
- Multiple native windows.
|
||||
- Native notifications.
|
||||
- User-confirmed local folder selection. The shared UI supplies the requested directory as the picker `defaultPath`; confirmation is required before filesystem access is retried.
|
||||
- One-click open/reveal/open-in-app actions.
|
||||
- Desktop host switcher and deep-link imports.
|
||||
- Local and remote instance handling.
|
||||
@@ -150,6 +156,15 @@ Use an explicit override when testing a different OpenCode CLI build or when a u
|
||||
- SSH uses OpenSSH ControlMaster on macOS/Linux. Windows uses independent hidden OpenSSH processes for setup commands and each long-lived forward because Win32 OpenSSH does not support ControlMaster reliably.
|
||||
- Tunnel lifecycle integration through the web server runtime.
|
||||
- Auto-update checks, downloads, and restart/apply flow.
|
||||
- The browser panel's own session (`persist:openchamber-browser`): its storage is
|
||||
cleared only through the scoped clear-data command, and camera, microphone,
|
||||
location, and device-picker requests from pages shown there are denied. Electron
|
||||
grants permission requests by default when no handler is set, and the panel
|
||||
loads whatever address the user types. Tab favicons are fetched in this
|
||||
session too, so icons behind the page's own login resolve and the app's origin
|
||||
never requests anything from a third-party host. Self-signed loopback HTTPS
|
||||
pages may use an untrusted certificate authority; certificate failures for
|
||||
external hosts and all other certificate errors remain blocked.
|
||||
|
||||
## IPC Pattern
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']);
|
||||
|
||||
export const shouldAllowBrowserPanelCertificateError = ({ url, error }) => {
|
||||
if (error !== 'net::ERR_CERT_AUTHORITY_INVALID') return false;
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.protocol === 'https:' && LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { shouldAllowBrowserPanelCertificateError } from './browser-panel-security.mjs';
|
||||
|
||||
test('allows untrusted certificate authorities for loopback HTTPS pages', () => {
|
||||
for (const url of [
|
||||
'https://localhost:58580/',
|
||||
'https://127.0.0.1:58580/',
|
||||
'https://[::1]:58580/',
|
||||
]) {
|
||||
assert.equal(shouldAllowBrowserPanelCertificateError({
|
||||
url,
|
||||
error: 'net::ERR_CERT_AUTHORITY_INVALID',
|
||||
}), true);
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps certificate validation for non-loopback pages', () => {
|
||||
for (const url of [
|
||||
'https://example.com/',
|
||||
'https://localhost.example.com/',
|
||||
'https://0.0.0.0:58580/',
|
||||
]) {
|
||||
assert.equal(shouldAllowBrowserPanelCertificateError({
|
||||
url,
|
||||
error: 'net::ERR_CERT_AUTHORITY_INVALID',
|
||||
}), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('does not bypass other certificate failures or malformed URLs', () => {
|
||||
assert.equal(shouldAllowBrowserPanelCertificateError({
|
||||
url: 'https://localhost:58580/',
|
||||
error: 'net::ERR_CERT_DATE_INVALID',
|
||||
}), false);
|
||||
assert.equal(shouldAllowBrowserPanelCertificateError({
|
||||
url: 'not a url',
|
||||
error: 'net::ERR_CERT_AUTHORITY_INVALID',
|
||||
}), false);
|
||||
});
|
||||
@@ -8,7 +8,7 @@ const DEFAULT_XDG_DATA_DIRS = ['/usr/local/share', '/usr/share'];
|
||||
const TARGET_FIELD_CODES = new Set(['f', 'F', 'u', 'U']);
|
||||
const TERMINAL_APP_IDS = new Set(['terminal', 'iterm2', 'ghostty']);
|
||||
|
||||
export const LINUX_CLI_BY_APP_ID = {
|
||||
const LINUX_CLI_BY_APP_ID = {
|
||||
vscode: 'code',
|
||||
cursor: 'cursor',
|
||||
vscodium: 'codium',
|
||||
@@ -43,7 +43,7 @@ const normalizeComparable = (value) => String(value || '')
|
||||
.trim();
|
||||
const normalizeCompactComparable = (value) => normalizeComparable(value).replace(/\s+/g, '');
|
||||
|
||||
export const stripDesktopExecFieldCodes = (execValue) => String(execValue || '')
|
||||
const stripDesktopExecFieldCodes = (execValue) => String(execValue || '')
|
||||
.replace(/%%/g, '\^@')
|
||||
.replace(/%[fFuUdDnNickvm]/g, '')
|
||||
.replace(/%./g, '')
|
||||
@@ -142,9 +142,7 @@ export const readLinuxDesktopEntries = async (options = {}) => {
|
||||
return entries.sort((left, right) => left.name.localeCompare(right.name));
|
||||
};
|
||||
|
||||
export const discoverLinuxDesktopApps = readLinuxDesktopEntries;
|
||||
|
||||
export const desktopEntryMatchesApp = (entry, appName, appId = '') => {
|
||||
const desktopEntryMatchesApp = (entry, appName, appId = '') => {
|
||||
const needles = uniqueStrings([appName, appId]).flatMap((value) => [normalizeComparable(value), normalizeCompactComparable(value)]).filter(Boolean);
|
||||
const haystacks = [entry.name, entry.id, path.basename(entry.filePath || ''), entry.exec]
|
||||
.flatMap((value) => [normalizeComparable(value), normalizeCompactComparable(value)]);
|
||||
@@ -331,7 +329,7 @@ const pathExistsSync = (candidate) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const linuxIconThemeDirs = ({ env = process.env, homeDir = os.homedir() } = {}) => {
|
||||
const linuxIconThemeDirs = ({ env = process.env, homeDir = os.homedir() } = {}) => {
|
||||
const dataHome = typeof env.XDG_DATA_HOME === 'string' && env.XDG_DATA_HOME.trim()
|
||||
? env.XDG_DATA_HOME.trim()
|
||||
: path.join(homeDir || os.homedir(), '.local', 'share');
|
||||
@@ -419,7 +417,7 @@ export const resolveLinuxIconFile = (iconName, options = {}) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const resolveDefaultLinuxFileManagerId = ({ env = process.env, execFileSyncImpl = execFileSync } = {}) => {
|
||||
const resolveDefaultLinuxFileManagerId = ({ env = process.env, execFileSyncImpl = execFileSync } = {}) => {
|
||||
try {
|
||||
const output = String(execFileSyncImpl('xdg-mime', ['query', 'default', 'inode/directory'], {
|
||||
encoding: 'utf8',
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from 'node:path';
|
||||
|
||||
const AUTOSTART_FILE_NAME = 'openchamber.desktop';
|
||||
|
||||
export const resolveLinuxAutostartDirectory = ({
|
||||
const resolveLinuxAutostartDirectory = ({
|
||||
env = process.env,
|
||||
homeDir = os.homedir(),
|
||||
} = {}) => {
|
||||
|
||||
+403
-105
@@ -11,6 +11,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
import updaterPkg from 'electron-updater';
|
||||
import { ElectronSshManager } from './ssh-manager.mjs';
|
||||
import { replaceFileWithRetry } from './windows-file-replace.mjs';
|
||||
import { createTrayController } from './tray.mjs';
|
||||
import { resolveManagedOpenCodeCwd } from './opencode-cwd.mjs';
|
||||
import { resolveStartupUrlProbePlan, shouldIgnoreLoopbackConnectionLimit } from './startup-url-selection.mjs';
|
||||
@@ -31,6 +32,8 @@ import {
|
||||
setLinuxAutostartEnabled,
|
||||
} from './linux-autostart.mjs';
|
||||
import { unsupportedAppSpecificOpenError, validateLocalPath } from './path-open-utils.mjs';
|
||||
import { shouldAllowBrowserPanelCertificateError } from './browser-panel-security.mjs';
|
||||
import { attachRendererRecovery } from './renderer-recovery.mjs';
|
||||
import { mintOutsideFileGrant } from '@openchamber/web/server/lib/fs/routes.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -306,6 +309,9 @@ const readDesktopMinimizeToTrayStatus = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// Close-to-tray gate. The persisted key is still `desktopMinimizeToTrayEnabled`
|
||||
// (settings written by earlier versions), but the behavior it controls is the
|
||||
// window close path only; minimize stays a normal taskbar/dock minimize.
|
||||
const shouldHideMainWindowToTray = (browserWindow) => {
|
||||
if (process.platform !== 'win32' && process.platform !== 'linux') return false;
|
||||
if (!state.trayController) return false;
|
||||
@@ -556,10 +562,15 @@ const writeJsonFile = async (filePath, data) => {
|
||||
// Atomic: write to a temp file then rename. Readers never see a partial
|
||||
// JSON file that could parse-error and get coerced to {}.
|
||||
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
||||
if (process.platform !== 'win32') await fsp.chmod(tmp, 0o600);
|
||||
await fsp.rename(tmp, filePath);
|
||||
if (process.platform !== 'win32') await fsp.chmod(filePath, 0o600);
|
||||
try {
|
||||
await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
||||
if (process.platform !== 'win32') await fsp.chmod(tmp, 0o600);
|
||||
await replaceFileWithRetry(tmp, filePath);
|
||||
if (process.platform !== 'win32') await fsp.chmod(filePath, 0o600);
|
||||
} catch (error) {
|
||||
await fsp.rm(tmp, { force: true }).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const readSettingsRoot = () => {
|
||||
@@ -1129,6 +1140,85 @@ const injectRuntimeConfigIntoHtml = (html) => {
|
||||
return `${initScript}${html}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* The browser panel's own session, kept separate from OpenChamber's.
|
||||
*
|
||||
* Every page the user opens in the panel shares this partition, which is what
|
||||
* lets a dev-server login persist between sessions without touching the app's
|
||||
* own storage.
|
||||
*/
|
||||
const BROWSER_PANEL_PARTITION = 'persist:openchamber-browser';
|
||||
|
||||
/**
|
||||
* Denies device and location access to pages shown in the browser panel.
|
||||
*
|
||||
* Electron grants permission requests by default when no handler is set. The
|
||||
* panel loads whatever address the user types, so that default would hand a
|
||||
* page the camera, the microphone, or the user's location without anything
|
||||
* being asked or shown — a browser people would not tolerate.
|
||||
*
|
||||
* This denies rather than prompts: a prompt is the right end state, but a
|
||||
* silent grant is the one outcome that must not stay. Denials are logged so a
|
||||
* page that legitimately needs something is diagnosable rather than mysterious.
|
||||
*/
|
||||
const MAX_FAVICON_BYTES = 512 * 1024;
|
||||
const FAVICON_MIME_TYPES = new Set([
|
||||
'image/x-icon',
|
||||
'image/vnd.microsoft.icon',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Resolves a web contents id to a browser-panel view, or refuses.
|
||||
*
|
||||
* These commands take an id from the renderer, and an id is guessable. Without
|
||||
* this a compromised renderer could point capture or the debugger at another
|
||||
* window's contents. Membership of the panel's own session is the proof: only
|
||||
* views created with that partition have it, and nothing else in the app does.
|
||||
*/
|
||||
const resolveBrowserPanelContents = (rawId) => {
|
||||
const id = Number.isFinite(rawId) ? Math.trunc(rawId) : null;
|
||||
if (id === null || id < 0) throw new Error('webContentsId is required');
|
||||
const target = webContents.fromId(id);
|
||||
if (!target || target.isDestroyed()) throw new Error('WebContents not found');
|
||||
if (target.session !== session.fromPartition(BROWSER_PANEL_PARTITION)) {
|
||||
throw new Error('That view is not a browser panel page');
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
const hardenBrowserPanelSession = () => {
|
||||
const panelSession = session.fromPartition(BROWSER_PANEL_PARTITION);
|
||||
|
||||
app.on('certificate-error', (event, contents, url, error, _certificate, callback) => {
|
||||
if (contents.session === panelSession && shouldAllowBrowserPanelCertificateError({ url, error })) {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
callback(false);
|
||||
});
|
||||
|
||||
panelSession.setPermissionRequestHandler((_contents, permission, callback, details) => {
|
||||
log.info('[electron] browser panel denied a permission request', {
|
||||
permission,
|
||||
origin: details?.requestingUrl || '',
|
||||
});
|
||||
callback(false);
|
||||
});
|
||||
|
||||
// Asked before some features even request; answering here keeps a page from
|
||||
// reporting a capability it would then be denied.
|
||||
panelSession.setPermissionCheckHandler(() => false);
|
||||
|
||||
// Serial, HID and USB device pickers.
|
||||
panelSession.setDevicePermissionHandler(() => false);
|
||||
};
|
||||
|
||||
const registerPackagedUiProtocol = () => {
|
||||
if (!shouldUsePackagedUi()) return;
|
||||
protocol.handle(UI_PROTOCOL, async (request) => {
|
||||
@@ -1151,7 +1241,15 @@ const registerPackagedUiProtocol = () => {
|
||||
if (filePath.endsWith('.html')) {
|
||||
const html = await fsp.readFile(filePath, 'utf8');
|
||||
const body = injectRuntimeConfigIntoHtml(html);
|
||||
return new Response(body, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
||||
// index.html must never be cached: it names the hashed asset
|
||||
// bundles, and a cached copy keeps a freshly installed build
|
||||
// loading the previous version's UI from the renderer disk cache.
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
});
|
||||
}
|
||||
return electronNet.fetch(pathToFileURL(filePath).toString());
|
||||
}
|
||||
@@ -1272,7 +1370,7 @@ const maybeShowNativeNotification = (rawInput) => {
|
||||
notification.on('click', () => {
|
||||
focusForegroundWindow();
|
||||
if (sessionId) {
|
||||
emitToAllWindows('openchamber:open-session', { sessionId, directory });
|
||||
emitToPrimaryWindow('openchamber:open-session', { sessionId, directory });
|
||||
}
|
||||
release();
|
||||
});
|
||||
@@ -1673,6 +1771,15 @@ const computeBootOutcome = ({ envTargetUrl, probe, config, localAvailable }) =>
|
||||
: probe?.status === 'wrong-service'
|
||||
? 'wrong-service'
|
||||
: 'ok';
|
||||
// A relay-capable host is not a recovery case just because its stored
|
||||
// direct URL failed the http probe — that URL is often the pairing
|
||||
// creator's own loopback (unreachable here, or worse, someone else's
|
||||
// service). The relay leg is activated in the renderer's relay restore,
|
||||
// which cannot run from a recovery screen: boot to main on the local
|
||||
// substrate and let it pick direct-or-relay.
|
||||
if (status !== 'ok' && sanitizeHostRelayForStorage(host.relay)) {
|
||||
return { target: 'remote', status: 'ok', hostId: host.id, url: host.apiUrl || host.url, ...availability };
|
||||
}
|
||||
return { target: 'remote', status, hostId: host.id, url: host.apiUrl || host.url, ...availability };
|
||||
};
|
||||
|
||||
@@ -1900,36 +2007,18 @@ const emitToAllWindows = (event, detail) => {
|
||||
}
|
||||
};
|
||||
|
||||
// macOS vibrancy: the native NSVisualEffectView needs a moment to settle after
|
||||
// the window is shown/restored. Until then the renderer keeps the sidebar solid
|
||||
// to avoid a flash of raw transparency; once ready it switches to the
|
||||
// translucent overlay. We toggle this readiness over the same IPC bridge.
|
||||
// Apply vibrancy to a live, on-screen window. Done after show (not in the
|
||||
// BrowserWindow constructor) because macOS otherwise leaves the material
|
||||
// uncomposited on a cold launch until the window gets a state change.
|
||||
const applyMacVibrancy = (browserWindow) => {
|
||||
if (process.platform !== 'darwin' || !browserWindow || browserWindow.isDestroyed()) return;
|
||||
try {
|
||||
browserWindow.setVibrancy('sidebar');
|
||||
} catch {}
|
||||
// Session navigation must land in ONE window. Broadcasting it makes every
|
||||
// open window adopt the same session, hijacking whatever the other windows
|
||||
// were doing.
|
||||
const emitToPrimaryWindow = (event, detail) => {
|
||||
const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed());
|
||||
if (windows.length === 0) return;
|
||||
const target = (state.mainWindow && !state.mainWindow.isDestroyed())
|
||||
? state.mainWindow
|
||||
: windows.find((window) => window.isFocused()) || windows.find((window) => window.isVisible()) || windows[0];
|
||||
emitToWindow(target, event, detail);
|
||||
};
|
||||
|
||||
const setMacVibrancyReady = (browserWindow, ready) => {
|
||||
if (process.platform !== 'darwin' || !browserWindow || browserWindow.isDestroyed()) return;
|
||||
emitToWindow(browserWindow, 'openchamber:vibrancy-ready', { ready });
|
||||
};
|
||||
|
||||
const scheduleMacVibrancyReady = (browserWindow, delayMs = 160) => {
|
||||
if (process.platform !== 'darwin' || !browserWindow || browserWindow.isDestroyed()) return;
|
||||
setMacVibrancyReady(browserWindow, false);
|
||||
const timer = setTimeout(() => {
|
||||
if (browserWindow.isDestroyed() || browserWindow.isMinimized() || !browserWindow.isVisible()) return;
|
||||
setMacVibrancyReady(browserWindow, true);
|
||||
}, delayMs);
|
||||
if (typeof timer?.unref === 'function') timer.unref();
|
||||
};
|
||||
|
||||
|
||||
const setTaskbarProgress = (value) => {
|
||||
if (process.platform !== 'win32') return;
|
||||
for (const browserWindow of BrowserWindow.getAllWindows()) {
|
||||
@@ -2193,8 +2282,25 @@ const dispatchDeepLink = (link) => {
|
||||
log.warn('[electron] invalid connect deep-link payload');
|
||||
return;
|
||||
}
|
||||
// Sent by the MCP OAuth callback page after it completes authorization in
|
||||
// the system browser. The work is already done server-side; all this has to
|
||||
// do is bring the app back to the front, since the user's attention is in a
|
||||
// browser tab at that moment.
|
||||
if (link.type === 'focus') {
|
||||
const target = state.mainWindow && !state.mainWindow.isDestroyed()
|
||||
? state.mainWindow
|
||||
: BrowserWindow.getAllWindows().find((window) => !window.isDestroyed());
|
||||
if (target) {
|
||||
if (target.isMinimized()) target.restore();
|
||||
target.show();
|
||||
target.focus();
|
||||
}
|
||||
emitToAllWindows('openchamber:deep-link-focus', { reason: link.value || null });
|
||||
return;
|
||||
}
|
||||
|
||||
if (link.type === 'session' && link.value) {
|
||||
emitToAllWindows('openchamber:open-session', { sessionId: link.value });
|
||||
emitToPrimaryWindow('openchamber:open-session', { sessionId: link.value });
|
||||
return;
|
||||
}
|
||||
if (link.type === 'host' && link.value) {
|
||||
@@ -2342,8 +2448,6 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
const desktopMacosMajor = String(macosMajorVersion());
|
||||
const usesFramelessChrome = process.platform === 'win32' || process.platform === 'linux';
|
||||
const usesCustomTitleBar = process.platform === 'darwin' || usesFramelessChrome;
|
||||
// macOS vibrancy, on by default; users can disable it (Appearance settings).
|
||||
const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false;
|
||||
const trayEnabled = process.platform !== 'darwin' || readSettingsRoot().desktopMacMenuBarEnabled !== false;
|
||||
const titleBarOverlayEnabled = false;
|
||||
const autoHidesNativeMenuBar = process.platform !== 'darwin';
|
||||
@@ -2359,11 +2463,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
minHeight: MIN_WINDOW_HEIGHT,
|
||||
icon: windowIconPath,
|
||||
show: false,
|
||||
backgroundColor: useVibrancy ? '#00000000' : '#151313',
|
||||
// Vibrancy is applied after the window is shown (see applyMacVibrancy), not
|
||||
// here: setting it in the constructor leaves the material uncomposited on a
|
||||
// cold launch until a window event. No `transparent: true` either — vibrancy
|
||||
// alone is enough and composites reliably once applied to a live window.
|
||||
backgroundColor: '#151313',
|
||||
frame: usesFramelessChrome ? false : undefined,
|
||||
autoHideMenuBar: autoHidesNativeMenuBar,
|
||||
// Electron's hiddenInset adds its own extra inset, which leaves the controls
|
||||
@@ -2379,7 +2479,6 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
`--openchamber-runtime-headers=${JSON.stringify(desktopRequestHeaders)}`,
|
||||
`--openchamber-home=${desktopHome}`,
|
||||
`--openchamber-macos-major=${desktopMacosMajor}`,
|
||||
`--openchamber-mac-vibrancy=${useVibrancy ? '1' : '0'}`,
|
||||
`--openchamber-tray-enabled=${trayEnabled ? '1' : '0'}`,
|
||||
`--openchamber-boot-outcome=${JSON.stringify(state.bootOutcome || null)}`,
|
||||
`--openchamber-relay-host-id=${rendererRuntimeConfig.relayHostId || ''}`,
|
||||
@@ -2402,6 +2501,9 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
browserWindow.__ocRuntimeConfig = { apiBaseUrl: desktopApiBaseUrl, clientToken: desktopClientToken, requestHeaders: desktopRequestHeaders };
|
||||
browserWindow.__ocInitScript = buildInitScript(desktopLocalOrigin, state.bootOutcome, desktopApiBaseUrl, desktopClientToken, desktopRequestHeaders);
|
||||
browserWindow.__ocTitleBarOverlayEnabled = titleBarOverlayEnabled;
|
||||
browserWindow.on('app-command', (event, command) => {
|
||||
if (command === 'browser-backward') event.preventDefault();
|
||||
});
|
||||
|
||||
if (useSaved && saved.maximized) {
|
||||
browserWindow.maximize();
|
||||
@@ -2430,18 +2532,11 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
};
|
||||
browserWindow.on('minimize', () => {
|
||||
refreshTrafficLights();
|
||||
setMacVibrancyReady(browserWindow, false);
|
||||
});
|
||||
browserWindow.on('restore', () => {
|
||||
refreshTrafficLights();
|
||||
setTimeout(refreshTrafficLights, 250);
|
||||
scheduleMacVibrancyReady(browserWindow, 180);
|
||||
});
|
||||
// Only suppress vibrancy around the minimize/restore cycle (it flashes raw
|
||||
// transparency during the genie animation). A plain show — cold launch from
|
||||
// the dock, un-hide — must NOT suppress, or the sidebar gets stuck solid
|
||||
// when the post-show `ready` re-enable is skipped while the window is still
|
||||
// animating in.
|
||||
browserWindow.on('show', refreshTrafficLights);
|
||||
browserWindow.on('focus', refreshTrafficLights);
|
||||
}
|
||||
@@ -2463,12 +2558,6 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
browserWindow.on('move', () => {
|
||||
debounceWindowStatePersist(browserWindow, false);
|
||||
});
|
||||
browserWindow.on('minimize', (event) => {
|
||||
if (!shouldHideMainWindowToTray(browserWindow)) return;
|
||||
debounceWindowStatePersist(browserWindow, true);
|
||||
event.preventDefault();
|
||||
browserWindow.hide();
|
||||
});
|
||||
browserWindow.on('close', (event) => {
|
||||
if (!state.quitRequested && shouldHideMainWindowToTray(browserWindow)) {
|
||||
debounceWindowStatePersist(browserWindow, true);
|
||||
@@ -2568,6 +2657,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
browserWindow.webContents.on('zoom-changed', () => {
|
||||
browserWindow.webContents.setZoomFactor(1);
|
||||
});
|
||||
attachRendererRecovery(browserWindow, { log, label: 'window' });
|
||||
|
||||
browserWindow.webContents.on('dom-ready', () => {
|
||||
if (browserWindow.__ocLabel === 'main') {
|
||||
@@ -2602,7 +2692,6 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
}
|
||||
browserWindow.show();
|
||||
browserWindow.focus();
|
||||
if (useVibrancy) applyMacVibrancy(browserWindow);
|
||||
});
|
||||
|
||||
if (url) {
|
||||
@@ -2766,8 +2855,6 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj
|
||||
const desktopHome = os.homedir() || '';
|
||||
const desktopMacosMajor = String(macosMajorVersion());
|
||||
const usesFramelessChrome = process.platform === 'win32' || process.platform === 'linux';
|
||||
// macOS vibrancy, on by default; users can disable it (Appearance settings).
|
||||
const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false;
|
||||
const trayEnabled = process.platform !== 'darwin' || readSettingsRoot().desktopMacMenuBarEnabled !== false;
|
||||
const browserWindow = new BrowserWindow({
|
||||
title: 'OpenChamber Mini Chat',
|
||||
@@ -2777,11 +2864,7 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj
|
||||
minHeight: MINI_CHAT_MIN_WINDOW_HEIGHT,
|
||||
icon: getWindowIconPath(),
|
||||
show: false,
|
||||
backgroundColor: useVibrancy ? '#00000000' : '#151313',
|
||||
// Vibrancy is applied after the window is shown (see applyMacVibrancy), not
|
||||
// here: setting it in the constructor leaves the material uncomposited on a
|
||||
// cold launch until a window event. No `transparent: true` either — vibrancy
|
||||
// alone is enough and composites reliably once applied to a live window.
|
||||
backgroundColor: '#151313',
|
||||
frame: usesFramelessChrome ? false : undefined,
|
||||
autoHideMenuBar: process.platform !== 'darwin',
|
||||
titleBarStyle: process.platform === 'darwin' || usesFramelessChrome ? 'hidden' : 'default',
|
||||
@@ -2812,6 +2895,8 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj
|
||||
browserWindow.__ocMiniChatSessionId = sessionWindowKey;
|
||||
browserWindow.__ocPinned = false;
|
||||
|
||||
attachRendererRecovery(browserWindow, { log, label: 'mini chat' });
|
||||
|
||||
if (sessionWindowKey) {
|
||||
state.miniChatWindowsBySession.set(sessionWindowKey, browserWindow);
|
||||
}
|
||||
@@ -2833,17 +2918,13 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj
|
||||
browserWindow.setTrafficLightPosition({ x: 16, y: 17 });
|
||||
} catch {}
|
||||
};
|
||||
// Suppress vibrancy only around minimize/restore, never on a plain show.
|
||||
browserWindow.on('show', refreshTrafficLights);
|
||||
browserWindow.on('focus', refreshTrafficLights);
|
||||
browserWindow.on('minimize', () => setMacVibrancyReady(browserWindow, false));
|
||||
browserWindow.on('restore', () => scheduleMacVibrancyReady(browserWindow, 180));
|
||||
}
|
||||
|
||||
browserWindow.once('ready-to-show', () => {
|
||||
browserWindow.show();
|
||||
browserWindow.focus();
|
||||
if (useVibrancy) applyMacVibrancy(browserWindow);
|
||||
});
|
||||
|
||||
browserWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
@@ -2957,12 +3038,24 @@ const resolveInitialUrl = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const defaultHostRelayCapable = Boolean(
|
||||
config.defaultHostId
|
||||
&& config.defaultHostId !== LOCAL_HOST_ID
|
||||
&& sanitizeHostRelayForStorage(config.hosts.find((entry) => entry.id === config.defaultHostId)?.relay),
|
||||
);
|
||||
if (apiBaseUrl && apiBaseUrl !== localUrl) {
|
||||
remoteProbe = await probeHostWithTimeout(apiBaseUrl, 2_000, clientToken, requestHeaders);
|
||||
if (remoteProbe.status === 'unreachable') {
|
||||
if (remoteProbe.status === 'unreachable' && !defaultHostRelayCapable) {
|
||||
remoteProbe = await probeHostWithTimeout(apiBaseUrl, 10_000, clientToken, requestHeaders);
|
||||
}
|
||||
if (remoteProbe.status === 'unreachable') {
|
||||
// The renderer's relay restore owns transport selection for relay-capable
|
||||
// hosts; any failed direct probe falls back to the local substrate.
|
||||
if (remoteProbe.status !== 'ok' && defaultHostRelayCapable) {
|
||||
apiBaseUrl = localUrl || '';
|
||||
clientToken = localUrl ? readDesktopLocalClientToken() : '';
|
||||
requestHeaders = {};
|
||||
initialUrl = localUiUrl;
|
||||
} else if (remoteProbe.status === 'unreachable') {
|
||||
state.unreachableHosts.add(apiBaseUrl);
|
||||
apiBaseUrl = localUrl || '';
|
||||
clientToken = localUrl ? readDesktopLocalClientToken() : '';
|
||||
@@ -3056,6 +3149,59 @@ const setupAutoUpdater = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// quitAndInstall() reports failures (rejected code signature, a Squirrel
|
||||
// session already disabled by an earlier failure) asynchronously on the
|
||||
// 'error' event, long after the call returns. Give the install that long to
|
||||
// either take the app down or report why it did not.
|
||||
const UPDATE_INSTALL_GRACE_MS = 15_000;
|
||||
|
||||
/**
|
||||
* Hand the downloaded update to the platform installer and keep the IPC call
|
||||
* open until the app quits or the updater reports a failure, so a rejected
|
||||
* install reaches the renderer instead of dying in the log. Restores the
|
||||
* quit/install flags when the install never happens.
|
||||
*/
|
||||
const installDownloadedUpdate = () => new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
|
||||
const rollbackQuitState = () => {
|
||||
state.quitRequested = false;
|
||||
state.installingUpdate = false;
|
||||
};
|
||||
|
||||
const fail = (error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(graceTimer);
|
||||
autoUpdater.off('error', fail);
|
||||
rollbackQuitState();
|
||||
log.error('[electron] update install failed', error);
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
};
|
||||
|
||||
// Still running after the grace period: the install is underway and the app
|
||||
// is shutting down, so release the pending IPC reply.
|
||||
const graceTimer = setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
autoUpdater.off('error', fail);
|
||||
resolve(null);
|
||||
}, UPDATE_INSTALL_GRACE_MS);
|
||||
|
||||
autoUpdater.on('error', fail);
|
||||
|
||||
// Defer so the renderer's invoke channel is idle before the app starts
|
||||
// shutting down.
|
||||
setImmediate(() => {
|
||||
try {
|
||||
killSidecar();
|
||||
autoUpdater.quitAndInstall();
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const parseRelevantChangelogNotes = async (fromVersion, toVersion) => {
|
||||
try {
|
||||
const response = await fetch(CHANGELOG_URL, { signal: AbortSignal.timeout(10_000) });
|
||||
@@ -3690,11 +3836,49 @@ const runSpecChain = (specs, appName) => {
|
||||
throw new Error(`Failed to open in ${appName}: ${failures.join('; ')}`);
|
||||
};
|
||||
|
||||
// The tunnel client lives in the web package (it already has a WebSocket
|
||||
// client) and is loaded only if the user actually previews a remote dev server.
|
||||
let devTunnelClientPromise = null;
|
||||
const getDevTunnelClient = async () => {
|
||||
if (!devTunnelClientPromise) {
|
||||
devTunnelClientPromise = import('@openchamber/web/server/lib/dev-tunnel/client.js')
|
||||
.then(({ createDevTunnelClient }) => createDevTunnelClient({ logger: log }))
|
||||
.catch((error) => {
|
||||
devTunnelClientPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return devTunnelClientPromise;
|
||||
};
|
||||
|
||||
const closeAllDevTunnels = () => {
|
||||
if (!devTunnelClientPromise) return;
|
||||
const pending = devTunnelClientPromise;
|
||||
devTunnelClientPromise = null;
|
||||
pending.then((client) => client.closeAll()).catch(() => {});
|
||||
};
|
||||
|
||||
const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
switch (command) {
|
||||
case 'desktop_start_window_drag':
|
||||
return null;
|
||||
|
||||
// Used after an MCP authorization finishes in the system browser: the app
|
||||
// raises itself rather than relying on the browser to hand control back.
|
||||
// A browser will not follow a custom-protocol link without a user gesture,
|
||||
// and the completion page has none.
|
||||
case 'desktop_focus_window': {
|
||||
const target = browserWindow && !browserWindow.isDestroyed()
|
||||
? browserWindow
|
||||
: (state.mainWindow && !state.mainWindow.isDestroyed() ? state.mainWindow : null);
|
||||
if (!target) return false;
|
||||
if (target.isMinimized()) target.restore();
|
||||
target.show();
|
||||
target.focus();
|
||||
app.focus?.({ steal: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'desktop_is_window_fullscreen':
|
||||
return Boolean(browserWindow?.isFullScreen());
|
||||
|
||||
@@ -3765,11 +3949,131 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
return { supported: true, enabled, active };
|
||||
}
|
||||
|
||||
// Dev-server tunnels: bind a loopback port here and pipe it to a dev server
|
||||
// on the remote OpenChamber host, so the browser panel loads a real origin
|
||||
// instead of a rewritten page. Deliberately absent from
|
||||
// COMMANDS_SAFE_FOR_REMOTE — a remote page must never open local listeners.
|
||||
case 'desktop_dev_tunnel_open': {
|
||||
const baseUrl = typeof args.baseUrl === 'string' ? args.baseUrl.trim() : '';
|
||||
const port = Number.isFinite(args.port) ? Math.trunc(args.port) : 0;
|
||||
if (!baseUrl) throw new Error('baseUrl is required');
|
||||
if (!(port > 0 && port <= 65535)) throw new Error('A valid port is required');
|
||||
|
||||
const headers = {};
|
||||
const requestHeaders = args.requestHeaders && typeof args.requestHeaders === 'object' ? args.requestHeaders : {};
|
||||
for (const [name, value] of Object.entries(requestHeaders)) {
|
||||
if (typeof value === 'string' && value) headers[name] = value;
|
||||
}
|
||||
if (typeof args.clientToken === 'string' && args.clientToken) {
|
||||
headers.Authorization = `Bearer ${args.clientToken}`;
|
||||
}
|
||||
|
||||
const client = await getDevTunnelClient();
|
||||
const result = await client.open({ baseUrl, port, headers });
|
||||
return { localPort: result.localPort, reused: result.reused, url: `http://127.0.0.1:${result.localPort}/` };
|
||||
}
|
||||
|
||||
case 'desktop_dev_tunnel_close': {
|
||||
const baseUrl = typeof args.baseUrl === 'string' ? args.baseUrl.trim() : '';
|
||||
const port = Number.isFinite(args.port) ? Math.trunc(args.port) : 0;
|
||||
if (!baseUrl || !(port > 0)) return { closed: false };
|
||||
const client = await getDevTunnelClient();
|
||||
return { closed: client.close({ baseUrl, port }) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces prefers-color-scheme for one previewed page.
|
||||
*
|
||||
* nativeTheme.themeSource is app-wide and would drag OpenChamber's own
|
||||
* appearance along with it, so this goes through the page's own emulation
|
||||
* instead. The debugger session has to stay attached: emulation is part of
|
||||
* that session and resets the moment it detaches.
|
||||
*/
|
||||
case 'desktop_browser_set_color_scheme': {
|
||||
const scheme = args.scheme === 'light' || args.scheme === 'dark' ? args.scheme : 'system';
|
||||
const target = resolveBrowserPanelContents(args.webContentsId);
|
||||
|
||||
if (!target.debugger.isAttached()) {
|
||||
try {
|
||||
target.debugger.attach('1.3');
|
||||
} catch {
|
||||
// DevTools owns the only debugger session a page can have.
|
||||
throw new Error('Close DevTools for this page before changing its appearance');
|
||||
}
|
||||
}
|
||||
|
||||
await target.debugger.sendCommand('Emulation.setEmulatedMedia', scheme === 'system'
|
||||
? { features: [] }
|
||||
: { features: [{ name: 'prefers-color-scheme', value: scheme }] });
|
||||
|
||||
if (scheme === 'system') {
|
||||
// Nothing left to emulate; give the session back so DevTools can attach.
|
||||
try { target.debugger.detach(); } catch { /* already gone */ }
|
||||
}
|
||||
return { scheme };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a page's favicon for the tab strip.
|
||||
*
|
||||
* Done here, in the panel's own session, rather than by the renderer: the
|
||||
* icon often sits behind the same login as the page, and letting the app's
|
||||
* own origin request it would both fail on those and quietly send traffic
|
||||
* to third-party hosts from OpenChamber itself. The bytes come back as a
|
||||
* data URL so nothing else has to fetch anything.
|
||||
*/
|
||||
case 'desktop_browser_fetch_favicon': {
|
||||
const target = typeof args.url === 'string' ? args.url.trim() : '';
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(target);
|
||||
} catch {
|
||||
throw new Error('A favicon URL is required');
|
||||
}
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
throw new Error('Unsupported favicon URL');
|
||||
}
|
||||
|
||||
const response = await electronNet.fetch(parsed.toString(), {
|
||||
session: session.fromPartition(BROWSER_PANEL_PARTITION),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Favicon request failed (${response.status})`);
|
||||
|
||||
const mime = (response.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
||||
if (!FAVICON_MIME_TYPES.has(mime)) throw new Error('Favicon is not an image');
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
// A tab icon is a few kilobytes; anything of a different order is not one,
|
||||
// and is not worth holding in memory for every tab.
|
||||
if (buffer.length === 0 || buffer.length > MAX_FAVICON_BYTES) {
|
||||
throw new Error('Favicon is not a usable size');
|
||||
}
|
||||
return { dataUrl: `data:${mime};base64,${buffer.toString('base64')}` };
|
||||
}
|
||||
|
||||
// Scoped to the browser panel's own partition, so clearing it can never
|
||||
// touch OpenChamber's session or any other window's storage.
|
||||
case 'desktop_browser_clear_data': {
|
||||
// Exact match, not a prefix: a prefix would also accept a partition that
|
||||
// merely starts with this name, which is not what the comment above
|
||||
// promises and would quietly stop being true if one were ever added.
|
||||
const partition = typeof args.partition === 'string' ? args.partition.trim() : '';
|
||||
if (partition !== BROWSER_PANEL_PARTITION) {
|
||||
throw new Error('Unsupported browser partition');
|
||||
}
|
||||
const storages = [];
|
||||
if (args.cookies === true) storages.push('cookies');
|
||||
if (args.cache === true) storages.push('localstorage', 'indexdb', 'websql', 'serviceworkers', 'cachestorage');
|
||||
if (storages.length === 0) return { cleared: false };
|
||||
|
||||
const browserSession = session.fromPartition(partition);
|
||||
await browserSession.clearStorageData({ storages });
|
||||
if (args.cache === true) await browserSession.clearCache();
|
||||
return { cleared: true };
|
||||
}
|
||||
|
||||
case 'desktop_browser_capture_page': {
|
||||
const wcId = Number.isFinite(args.webContentsId) ? Math.trunc(args.webContentsId) : null;
|
||||
if (wcId === null || wcId < 0) throw new Error('webContentsId is required');
|
||||
const wc = webContents.fromId(wcId);
|
||||
if (!wc || wc.isDestroyed()) throw new Error('WebContents not found');
|
||||
const wc = resolveBrowserPanelContents(args.webContentsId);
|
||||
const image = await wc.capturePage();
|
||||
const buffer = image.toJPEG(82);
|
||||
return {
|
||||
@@ -4178,26 +4482,6 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
case 'desktop_set_vibrancy': {
|
||||
// Vibrancy + transparent backing are window-creation options, so the
|
||||
// change only takes effect on a fresh launch. Persist the preference,
|
||||
// then relaunch the app.
|
||||
const enabled = args.enabled === true;
|
||||
await mutateSettingsRoot((root) => {
|
||||
root.desktopVibrancy = enabled;
|
||||
});
|
||||
setImmediate(() => {
|
||||
try {
|
||||
prepareForQuit();
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
} catch (err) {
|
||||
log.error('[electron] desktop_set_vibrancy relaunch failed', err);
|
||||
}
|
||||
});
|
||||
return { enabled, requiresRestart: true };
|
||||
}
|
||||
|
||||
case 'desktop_check_for_updates': {
|
||||
assertUpdaterCapability({ packaged: app.isPackaged });
|
||||
const currentVersion = APP_VERSION;
|
||||
@@ -4255,9 +4539,20 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
const onError = (error) => finish(reject, error);
|
||||
autoUpdater.on('update-downloaded', onDownloaded);
|
||||
autoUpdater.on('error', onError);
|
||||
Promise.resolve(autoUpdater.downloadUpdate()).catch((error) => finish(reject, error));
|
||||
// downloadUpdate() resolves once the payload is on disk. It stays
|
||||
// the authoritative signal: when the file was already cached the
|
||||
// updater emits no 'update-downloaded', and waiting only for the
|
||||
// event left this promise pending and its listeners attached on
|
||||
// every retry.
|
||||
Promise.resolve(autoUpdater.downloadUpdate())
|
||||
.then(() => finish(resolve, null))
|
||||
.catch((error) => finish(reject, error));
|
||||
});
|
||||
}
|
||||
// The 'update-downloaded' event does not fire for an already cached
|
||||
// payload, so record the payload as ready here too; otherwise restart
|
||||
// would relaunch without installing anything.
|
||||
state.pendingUpdate.downloaded = true;
|
||||
emitToAllWindows('openchamber:update-progress', mapUpdaterProgressEvent({
|
||||
event: 'Finished',
|
||||
data: {},
|
||||
@@ -4294,20 +4589,16 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
return await installDownloadedUpdate();
|
||||
}
|
||||
// Defer so the IPC reply flushes before the app starts shutting down.
|
||||
// Without this, quitAndInstall() can race with the renderer's pending
|
||||
// invoke and the restart appears to do nothing from the UI side.
|
||||
// Without this, relaunch can race with the renderer's pending invoke and
|
||||
// the restart appears to do nothing from the UI side.
|
||||
setImmediate(() => {
|
||||
try {
|
||||
if (applyUpdate) {
|
||||
killSidecar();
|
||||
autoUpdater.quitAndInstall();
|
||||
} else {
|
||||
prepareForQuit();
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
}
|
||||
prepareForQuit();
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
} catch (err) {
|
||||
log.error('[electron] desktop_restart failed', err);
|
||||
}
|
||||
@@ -4446,6 +4737,10 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
}
|
||||
return null;
|
||||
|
||||
// Minimize always goes to the taskbar/dock, even with tray background mode
|
||||
// on: hiding the window here would drop the taskbar entry and make the
|
||||
// in-app minimize button behave differently from the native one. Only
|
||||
// closing hands the window to the tray.
|
||||
case 'desktop_minimize_current_window':
|
||||
if (browserWindow && !browserWindow.isDestroyed()) {
|
||||
browserWindow.minimize();
|
||||
@@ -5124,6 +5419,8 @@ app.on('window-all-closed', () => {
|
||||
|
||||
app.on('before-quit', (event) => {
|
||||
state.quitRequested = true;
|
||||
// Loopback listeners would otherwise outlive the window that needed them.
|
||||
closeAllDevTunnels();
|
||||
|
||||
if (state.installingUpdate) {
|
||||
return;
|
||||
@@ -5197,6 +5494,7 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
nativeTheme.themeSource = readThemeSource();
|
||||
registerPackagedUiProtocol();
|
||||
hardenBrowserPanelSession();
|
||||
setupAutoUpdater();
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { resolveManagedOpenCodeCwd } from './opencode-cwd.mjs';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openchamber/electron",
|
||||
"version": "1.18.0",
|
||||
"version": "1.21.0",
|
||||
"private": true,
|
||||
"description": "Electron desktop runtime for OpenChamber",
|
||||
"author": "OpenChamber",
|
||||
@@ -13,9 +13,10 @@
|
||||
"electron-updater": "^6.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"electron": "^41.2.1",
|
||||
"electron-builder": "^26.0.0"
|
||||
"@electron/rebuild": "^4.2.0",
|
||||
"electron": "^43.3.0",
|
||||
"electron-builder": "^26.0.0",
|
||||
"node-abi": "^4.33.0"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"electron"
|
||||
@@ -39,6 +40,7 @@
|
||||
"bundle:main": "bun ./scripts/bundle-main.mjs",
|
||||
"generate:macos-icon": "node ./scripts/generate-macos-icon-assets.cjs",
|
||||
"rebuild:native": "node ./scripts/rebuild-native.mjs",
|
||||
"test": "node ../../scripts/run-isolated-tests.mjs .",
|
||||
"test:architecture": "node --test ./startup-url-selection.test.mjs ./scripts/target-architecture.test.mjs ./scripts/verify-linux-appimage.test.mjs ./scripts/verify-update-manifest.test.mjs ./scripts/ensure-electron.test.mjs",
|
||||
"test:updater": "node --test ./updater-capability.test.mjs ./updater-channel.test.mjs ./updater-check.test.mjs ./updater-feed.test.mjs ./scripts/finalize-latest-yml.test.mjs ./scripts/updater-e2e-fixture.test.mjs",
|
||||
"test:linux-desktop": "node --test ./linux-autostart.test.mjs && node ./scripts/smoke-linux-app-discovery.mjs && node ./scripts/smoke-path-open-utils.mjs",
|
||||
|
||||
@@ -12,7 +12,7 @@ const accessErrorMessage = (label, targetPath, error) => {
|
||||
return `${label} could not be checked: ${error?.message || String(error)}`;
|
||||
};
|
||||
|
||||
export const normalizeRequiredPath = (rawPath, label = 'Path') => {
|
||||
const normalizeRequiredPath = (rawPath, label = 'Path') => {
|
||||
const targetPath = typeof rawPath === 'string' ? rawPath.trim() : '';
|
||||
if (!targetPath) {
|
||||
throw new Error(`${label} is required`);
|
||||
|
||||
@@ -18,10 +18,6 @@ const runtimeHeadersRaw = readArgValue('--openchamber-runtime-headers');
|
||||
const homeDirectory = readArgValue('--openchamber-home');
|
||||
const macosMajorRaw = readArgValue('--openchamber-macos-major');
|
||||
const macosMajor = Number.parseInt(macosMajorRaw, 10);
|
||||
const macVibrancySupported = process.platform === 'darwin';
|
||||
// Effective state for this window (main process resolves the saved preference
|
||||
// and passes it in). Defaults on when supported unless explicitly '0'.
|
||||
const hasMacVibrancy = macVibrancySupported && readArgValue('--openchamber-mac-vibrancy') !== '0';
|
||||
const trayEnabled = process.platform !== 'darwin' || readArgValue('--openchamber-tray-enabled') !== '0';
|
||||
|
||||
// Preload re-executes on every cross-origin navigation (we run with
|
||||
@@ -97,8 +93,6 @@ if (Number.isFinite(macosMajor) && macosMajor > 0) {
|
||||
contextBridge.exposeInMainWorld('__OPENCHAMBER_ELECTRON__', {
|
||||
runtime: 'electron',
|
||||
arch: process.arch,
|
||||
macVibrancy: hasMacVibrancy,
|
||||
macVibrancySupported,
|
||||
trayEnabled,
|
||||
});
|
||||
|
||||
@@ -148,18 +142,6 @@ const dispatchNativeEvent = (event, detail) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Toggles the frost on/off in response to the main process around the
|
||||
// minimize/restore cycle. The default ("ready") state is set reliably in the
|
||||
// renderer (cssGenerator) — not here — because this preload runs at
|
||||
// document-start when documentElement may not exist yet.
|
||||
const setVibrancyReady = (ready) => {
|
||||
if (!hasMacVibrancy) return;
|
||||
try {
|
||||
document.documentElement.toggleAttribute('data-oc-vibrancy-ready', ready === true);
|
||||
} catch {
|
||||
}
|
||||
};
|
||||
|
||||
// Main-process events are read-only notifications (update progress,
|
||||
// window focus, etc.) — safe to deliver to any page rendered in this
|
||||
// webContents. The events themselves don't grant capability.
|
||||
@@ -173,10 +155,6 @@ ipcRenderer.on('openchamber:emit', (_evt, payload) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === 'openchamber:vibrancy-ready') {
|
||||
setVibrancyReady(payload.detail?.ready === true);
|
||||
}
|
||||
|
||||
dispatchNativeEvent(event, payload.detail);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
const RECOVERY_WINDOW_MS = 60_000;
|
||||
const MAX_RECOVERY_ATTEMPTS = 3;
|
||||
|
||||
const RECOVERABLE_REASONS = new Set([
|
||||
'abnormal-exit',
|
||||
'crashed',
|
||||
'oom',
|
||||
'memory-eviction',
|
||||
]);
|
||||
|
||||
const RELOAD_DELAY_MS = 100;
|
||||
|
||||
export const createRendererRecoveryPolicy = (now = Date.now) => {
|
||||
let windowStartedAt = 0;
|
||||
let attempts = 0;
|
||||
|
||||
return {
|
||||
shouldReload: (reason) => {
|
||||
if (!RECOVERABLE_REASONS.has(reason)) return false;
|
||||
|
||||
const currentTime = now();
|
||||
if (currentTime - windowStartedAt >= RECOVERY_WINDOW_MS) {
|
||||
windowStartedAt = currentTime;
|
||||
attempts = 0;
|
||||
}
|
||||
if (attempts >= MAX_RECOVERY_ATTEMPTS) return false;
|
||||
|
||||
attempts += 1;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Reload a window whose renderer process died, within the recovery budget.
|
||||
* Shared by every BrowserWindow so the desktop shell has one recovery policy.
|
||||
*/
|
||||
export const attachRendererRecovery = (browserWindow, { log, label }) => {
|
||||
const policy = createRendererRecoveryPolicy();
|
||||
browserWindow.webContents.on('render-process-gone', (_event, details) => {
|
||||
if (!policy.shouldReload(details.reason)) return;
|
||||
log.warn('[electron] renderer exited unexpectedly; reloading window', {
|
||||
label: browserWindow.__ocLabel,
|
||||
surface: label,
|
||||
reason: details.reason,
|
||||
exitCode: details.exitCode,
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (!browserWindow.isDestroyed()) {
|
||||
browserWindow.webContents.reload();
|
||||
}
|
||||
}, RELOAD_DELAY_MS);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { attachRendererRecovery, createRendererRecoveryPolicy } from './renderer-recovery.mjs';
|
||||
|
||||
const createFakeWindow = () => {
|
||||
const listeners = new Map();
|
||||
const state = { reloads: 0, destroyed: false };
|
||||
const browserWindow = {
|
||||
__ocLabel: 'main',
|
||||
state,
|
||||
destroy: () => {
|
||||
state.destroyed = true;
|
||||
},
|
||||
emit: (event, details) => listeners.get(event)?.(null, details),
|
||||
isDestroyed: () => state.destroyed,
|
||||
webContents: {
|
||||
on: (event, listener) => listeners.set(event, listener),
|
||||
reload: () => {
|
||||
state.reloads += 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
return browserWindow;
|
||||
};
|
||||
|
||||
const createFakeLog = () => {
|
||||
const warnings = [];
|
||||
return { warnings, warn: (message, payload) => warnings.push({ message, payload }) };
|
||||
};
|
||||
|
||||
test('allows a bounded number of reloads for recoverable renderer failures', () => {
|
||||
const policy = createRendererRecoveryPolicy(() => 1_000);
|
||||
|
||||
assert.equal(policy.shouldReload('crashed'), true);
|
||||
assert.equal(policy.shouldReload('oom'), true);
|
||||
assert.equal(policy.shouldReload('abnormal-exit'), true);
|
||||
assert.equal(policy.shouldReload('crashed'), false);
|
||||
});
|
||||
|
||||
test('reloads after the renderer is evicted for memory', () => {
|
||||
const policy = createRendererRecoveryPolicy(() => 1_000);
|
||||
|
||||
assert.equal(policy.shouldReload('memory-eviction'), true);
|
||||
});
|
||||
|
||||
test('ignores reasons Electron never reports for render-process-gone', () => {
|
||||
const policy = createRendererRecoveryPolicy(() => 1_000);
|
||||
|
||||
assert.equal(policy.shouldReload('made-up-reason'), false);
|
||||
assert.equal(policy.shouldReload('crashed'), true);
|
||||
});
|
||||
|
||||
test('ignores clean and externally killed renderer exits', () => {
|
||||
const policy = createRendererRecoveryPolicy(() => 1_000);
|
||||
|
||||
assert.equal(policy.shouldReload('clean-exit'), false);
|
||||
assert.equal(policy.shouldReload('killed'), false);
|
||||
assert.equal(policy.shouldReload('launch-failed'), false);
|
||||
});
|
||||
|
||||
test('resets the recovery budget after the recovery window', () => {
|
||||
let currentTime = 1_000;
|
||||
const policy = createRendererRecoveryPolicy(() => currentTime);
|
||||
|
||||
assert.equal(policy.shouldReload('crashed'), true);
|
||||
assert.equal(policy.shouldReload('crashed'), true);
|
||||
assert.equal(policy.shouldReload('crashed'), true);
|
||||
assert.equal(policy.shouldReload('crashed'), false);
|
||||
|
||||
currentTime += 60_000;
|
||||
assert.equal(policy.shouldReload('crashed'), true);
|
||||
});
|
||||
|
||||
test('reloads the attached window after a recoverable renderer failure', async () => {
|
||||
const browserWindow = createFakeWindow();
|
||||
const log = createFakeLog();
|
||||
attachRendererRecovery(browserWindow, { log, label: 'mini chat' });
|
||||
|
||||
browserWindow.emit('render-process-gone', { reason: 'crashed', exitCode: 5 });
|
||||
await setTimeout(150);
|
||||
|
||||
assert.equal(browserWindow.state.reloads, 1);
|
||||
assert.equal(log.warnings.length, 1);
|
||||
assert.equal(log.warnings[0].payload.surface, 'mini chat');
|
||||
assert.equal(log.warnings[0].payload.label, 'main');
|
||||
});
|
||||
|
||||
test('skips the reload when the window is gone or the exit is not recoverable', async () => {
|
||||
const browserWindow = createFakeWindow();
|
||||
attachRendererRecovery(browserWindow, { log: createFakeLog(), label: 'window' });
|
||||
|
||||
browserWindow.emit('render-process-gone', { reason: 'clean-exit', exitCode: 0 });
|
||||
browserWindow.emit('render-process-gone', { reason: 'crashed', exitCode: 5 });
|
||||
browserWindow.destroy();
|
||||
await setTimeout(150);
|
||||
|
||||
assert.equal(browserWindow.state.reloads, 0);
|
||||
});
|
||||
@@ -220,7 +220,7 @@ async function main() {
|
||||
});
|
||||
}
|
||||
|
||||
const electron = spawnProcess('npx', ['electron', './main.mjs'], {
|
||||
const electron = spawnProcess('bun', ['x', 'electron', './main.mjs'], {
|
||||
cwd: electronDir,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -5,9 +5,30 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
import { replaceFileWithRetry } from './windows-file-replace.mjs';
|
||||
|
||||
const LOCAL_HOST_ID = 'local';
|
||||
const DEFAULT_CONNECTION_TIMEOUT_SEC = 60;
|
||||
const DEFAULT_LOCAL_BIND_HOST = '127.0.0.1';
|
||||
// Global npm prefixes are root-owned on most distributions, so `npm install -g`
|
||||
// fails with EACCES for a normal SSH user. Everything we install goes to a
|
||||
// prefix inside the user's home instead.
|
||||
const REMOTE_USER_PREFIX = '$HOME/.openchamber/npm-global';
|
||||
const REMOTE_BUN_CANDIDATE = '"${BUN_INSTALL:-$HOME/.bun}/bin/bun"';
|
||||
// The opencode CLI usually installs into the user's home, which an SSH login
|
||||
// shell does not have on PATH. The remote server only looks at OPENCODE_BINARY
|
||||
// and PATH, so resolve the CLI here and hand it over explicitly.
|
||||
const REMOTE_OPENCODE_CANDIDATES = [
|
||||
'"$HOME/.opencode/bin/opencode"',
|
||||
'"${BUN_INSTALL:-$HOME/.bun}/bin/opencode"',
|
||||
'"$HOME/.local/bin/opencode"',
|
||||
'"$HOME/.openchamber/npm-global/bin/opencode"',
|
||||
];
|
||||
const REMOTE_PATH_PREFIX = '$HOME/.opencode/bin:${BUN_INSTALL:-$HOME/.bun}/bin:$HOME/.local/bin:$HOME/.openchamber/npm-global/bin';
|
||||
const REMOTE_BIN_CANDIDATES = [
|
||||
'"$HOME/.openchamber/npm-global/bin/openchamber"',
|
||||
'"${BUN_INSTALL:-$HOME/.bun}/bin/openchamber"',
|
||||
];
|
||||
const DEFAULT_CONTROL_PERSIST_SEC = 300;
|
||||
const DEFAULT_READY_TIMEOUT_SEC = 30;
|
||||
const DEFAULT_RECONNECT_MAX_ATTEMPTS = 5;
|
||||
@@ -77,10 +98,15 @@ const writeJsonRoot = async (settingsFilePath, root) => {
|
||||
await fsp.mkdir(path.dirname(settingsFilePath), { recursive: true });
|
||||
// Atomic write: concurrent readers (main.mjs, web server) would otherwise
|
||||
// see partial JSON and readJsonRoot()'s catch would silently coerce to {},
|
||||
// causing the next read-modify-write to wipe the entire settings file.
|
||||
// causing the next read-modify-write wipe the entire settings file.
|
||||
const tmp = `${settingsFilePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
await fsp.writeFile(tmp, JSON.stringify(root, null, 2));
|
||||
await fsp.rename(tmp, settingsFilePath);
|
||||
try {
|
||||
await fsp.writeFile(tmp, JSON.stringify(root, null, 2));
|
||||
await replaceFileWithRetry(tmp, settingsFilePath);
|
||||
} catch (error) {
|
||||
await fsp.rm(tmp, { force: true }).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultTrue = () => true;
|
||||
@@ -578,6 +604,7 @@ export class ElectronSshManager {
|
||||
localUrl: null,
|
||||
localPort: null,
|
||||
remotePort: null,
|
||||
remoteBinPath: null,
|
||||
startedByUs: false,
|
||||
retryAttempt: 0,
|
||||
requiresUserAction: false,
|
||||
@@ -804,9 +831,10 @@ export class ElectronSshManager {
|
||||
mode: instance?.remoteOpenchamber?.mode === 'external' ? 'external' : 'managed',
|
||||
keepRunning: instance?.remoteOpenchamber?.keepRunning !== false,
|
||||
...(Number.isFinite(instance?.remoteOpenchamber?.preferredPort) ? { preferredPort: Number(instance.remoteOpenchamber.preferredPort) } : {}),
|
||||
installMethod: ['npm', 'bun', 'download_release', 'upload_bundle'].includes(instance?.remoteOpenchamber?.installMethod)
|
||||
installMethod: ['auto', 'npm', 'bun'].includes(instance?.remoteOpenchamber?.installMethod)
|
||||
? instance.remoteOpenchamber.installMethod
|
||||
: 'bun',
|
||||
: 'auto',
|
||||
bindHost: instance?.remoteOpenchamber?.bindHost === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1',
|
||||
uploadBundleOverSsh: Boolean(instance?.remoteOpenchamber?.uploadBundleOverSsh),
|
||||
},
|
||||
localForward: {
|
||||
@@ -981,38 +1009,77 @@ export class ElectronSshManager {
|
||||
return secret?.enabled && typeof secret.value === 'string' && secret.value.trim() ? secret.value.trim() : null;
|
||||
}
|
||||
|
||||
async remoteCommandExists(parsed, controlPath, commandName) {
|
||||
try {
|
||||
const output = await this.runRemoteCommand(parsed, controlPath, `command -v ${commandName} >/dev/null 2>&1 && echo yes || echo no`);
|
||||
return output.trim() === 'yes';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// A login shell over SSH does not source the user's interactive rc files, so
|
||||
// tools installed into a home directory (bun above all) are missing from PATH
|
||||
// even when they exist. Look at their known install locations too.
|
||||
async resolveRemoteTool(parsed, controlPath, commandName, extraCandidates = []) {
|
||||
const candidateList = [...extraCandidates, `"$(command -v ${commandName} 2>/dev/null)"`].join(' ');
|
||||
const script = [
|
||||
`for candidate in ${candidateList}; do`,
|
||||
' [ -n "$candidate" ] || continue;',
|
||||
' [ -x "$candidate" ] || continue;',
|
||||
` printf '%s' "$candidate";`,
|
||||
' exit 0;',
|
||||
'done',
|
||||
].join(' ');
|
||||
|
||||
async currentRemoteOpenChamberVersion(parsed, controlPath) {
|
||||
try {
|
||||
const output = await this.runRemoteCommand(parsed, controlPath, 'openchamber --version 2>/dev/null || true');
|
||||
return parseVersionToken(output);
|
||||
const output = await this.runRemoteCommand(parsed, controlPath, script);
|
||||
return output.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async installOpenChamberManaged(parsed, controlPath, version, preferred) {
|
||||
const hasBun = await this.remoteCommandExists(parsed, controlPath, 'bun');
|
||||
const hasNpm = await this.remoteCommandExists(parsed, controlPath, 'npm');
|
||||
const commands = [];
|
||||
// Every place OpenChamber may live on the remote host, with the version each
|
||||
// one reports. Installs land in the user prefix while an older copy can still
|
||||
// sit on PATH, so the caller picks by version instead of trusting PATH order.
|
||||
async remoteOpenChamberCandidates(parsed, controlPath) {
|
||||
const script = [
|
||||
`for candidate in ${REMOTE_BIN_CANDIDATES.join(' ')} "$(command -v openchamber 2>/dev/null)"; do`,
|
||||
' [ -n "$candidate" ] || continue;',
|
||||
' [ -x "$candidate" ] || continue;',
|
||||
` printf '%s\t%s\n' "$candidate" "$("$candidate" --version 2>/dev/null | head -n 1)";`,
|
||||
'done',
|
||||
].join(' ');
|
||||
|
||||
if (preferred === 'bun') {
|
||||
if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`);
|
||||
if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`);
|
||||
} else if (preferred === 'npm') {
|
||||
if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`);
|
||||
if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`);
|
||||
let output = '';
|
||||
try {
|
||||
output = await this.runRemoteCommand(parsed, controlPath, script);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
for (const line of output.split(/\r?\n/)) {
|
||||
const [binPath, versionRaw] = line.split('\t');
|
||||
const trimmed = (binPath || '').trim();
|
||||
if (!trimmed || seen.has(trimmed)) continue;
|
||||
seen.add(trimmed);
|
||||
candidates.push({ binPath: trimmed, version: parseVersionToken(versionRaw || '') });
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async installOpenChamberManaged(parsed, controlPath, version, preferred) {
|
||||
const bunPath = await this.resolveRemoteTool(parsed, controlPath, 'bun', [REMOTE_BUN_CANDIDATE]);
|
||||
const npmPath = await this.resolveRemoteTool(parsed, controlPath, 'npm');
|
||||
|
||||
// bun's global install already targets ~/.bun; npm is pinned to a prefix in
|
||||
// the user's home so it never touches the root-owned global directory.
|
||||
const bunCommand = bunPath ? `${shellQuote(bunPath)} add -g @openchamber/web@${version}` : null;
|
||||
const npmCommand = npmPath
|
||||
? `mkdir -p "${REMOTE_USER_PREFIX}" && ${shellQuote(npmPath)} install -g --prefix "${REMOTE_USER_PREFIX}" @openchamber/web@${version}`
|
||||
: null;
|
||||
|
||||
const commands = [];
|
||||
if (preferred === 'npm') {
|
||||
if (npmCommand) commands.push(npmCommand);
|
||||
if (bunCommand) commands.push(bunCommand);
|
||||
} else {
|
||||
if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`);
|
||||
if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`);
|
||||
if (bunCommand) commands.push(bunCommand);
|
||||
if (npmCommand) commands.push(npmCommand);
|
||||
}
|
||||
|
||||
if (commands.length === 0) {
|
||||
@@ -1072,24 +1139,35 @@ export class ElectronSshManager {
|
||||
}
|
||||
}
|
||||
|
||||
async startRemoteServerManaged(parsed, controlPath, instance, desiredPort) {
|
||||
let envPrefix = 'OPENCHAMBER_RUNTIME=ssh-remote';
|
||||
async startRemoteServerManaged(parsed, controlPath, instance, desiredPort, binPath) {
|
||||
const opencodePath = await this.resolveRemoteTool(parsed, controlPath, 'opencode', REMOTE_OPENCODE_CANDIDATES);
|
||||
if (!opencodePath) {
|
||||
throw new Error('The opencode CLI is not installed on the remote machine. Install it there, then connect again');
|
||||
}
|
||||
|
||||
const secret = this.configuredOpenChamberPassword(instance);
|
||||
const remoteBindHost = instance.remoteOpenchamber?.bindHost === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1';
|
||||
// Binding the remote server to every interface publishes its UI to the
|
||||
// remote machine's whole network, so it may not run without a password.
|
||||
if (remoteBindHost === '0.0.0.0' && !secret) {
|
||||
throw new Error('Exposing the remote server to its network requires a UI password');
|
||||
}
|
||||
|
||||
let envPrefix = `PATH="${REMOTE_PATH_PREFIX}:$PATH" OPENCODE_BINARY=${shellQuote(opencodePath)} OPENCHAMBER_RUNTIME=ssh-remote`;
|
||||
if (secret) {
|
||||
envPrefix += ` OPENCHAMBER_UI_PASSWORD=${shellQuote(secret)}`;
|
||||
}
|
||||
const output = await this.runRemoteCommand(parsed, controlPath, `${envPrefix} openchamber serve --hostname 127.0.0.1 --port ${desiredPort}`);
|
||||
const output = await this.runRemoteCommand(parsed, controlPath, `${envPrefix} ${shellQuote(binPath)} serve --hostname ${remoteBindHost} --port ${desiredPort}`);
|
||||
const port = output.split(/\s+/).map((token) => Number.parseInt(token, 10)).find((value) => Number.isFinite(value));
|
||||
return port || desiredPort;
|
||||
}
|
||||
|
||||
async stopRemoteServerBestEffort(parsed, controlPath, remotePort) {
|
||||
// `openchamber stop` owns the daemon lifecycle. The HTTP shutdown route sits
|
||||
// behind UI authentication, so it cannot stop a password-protected server.
|
||||
async stopRemoteServerBestEffort(parsed, controlPath, remotePort, remoteBinPath) {
|
||||
if (!remoteBinPath) return;
|
||||
try {
|
||||
await this.runRemoteCommand(
|
||||
parsed,
|
||||
controlPath,
|
||||
`if command -v curl >/dev/null 2>&1; then curl -fsS -X POST http://127.0.0.1:${remotePort}/api/system/shutdown >/dev/null 2>&1 || true; elif command -v wget >/dev/null 2>&1; then wget -qO- --method=POST http://127.0.0.1:${remotePort}/api/system/shutdown >/dev/null 2>&1 || true; fi`,
|
||||
);
|
||||
await this.runRemoteCommand(parsed, controlPath, `${shellQuote(remoteBinPath)} stop --port ${remotePort}`);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -1143,17 +1221,27 @@ export class ElectronSshManager {
|
||||
const port = instance.remoteOpenchamber.preferredPort;
|
||||
this.setStatus(instance.id, 'server_detecting', 'Probing external OpenChamber server', null, null, port, false, 0, false);
|
||||
await this.probeRemoteSystemInfo(parsed, controlPath, port, this.configuredOpenChamberPassword(instance));
|
||||
return { remotePort: port, startedByUs: false };
|
||||
return { remotePort: port, startedByUs: false, remoteBinPath: null };
|
||||
}
|
||||
|
||||
this.setStatus(instance.id, 'remote_probe', 'Checking remote OpenChamber installation');
|
||||
const installedVersion = await this.currentRemoteOpenChamberVersion(parsed, controlPath);
|
||||
if (!installedVersion) {
|
||||
this.setStatus(instance.id, 'installing', 'Installing OpenChamber on remote host');
|
||||
await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod);
|
||||
} else if (installedVersion !== this.appVersion) {
|
||||
this.setStatus(instance.id, 'updating', `Updating remote OpenChamber from ${installedVersion} to ${this.appVersion}`);
|
||||
const installed = await this.remoteOpenChamberCandidates(parsed, controlPath);
|
||||
let binary = installed.find((candidate) => candidate.version === this.appVersion) || null;
|
||||
|
||||
if (!binary) {
|
||||
const existing = installed[0] || null;
|
||||
if (existing) {
|
||||
this.setStatus(instance.id, 'updating', `Updating remote OpenChamber from ${existing.version || 'unknown'} to ${this.appVersion}`);
|
||||
} else {
|
||||
this.setStatus(instance.id, 'installing', 'Installing OpenChamber on remote host');
|
||||
}
|
||||
await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod);
|
||||
|
||||
const afterInstall = await this.remoteOpenChamberCandidates(parsed, controlPath);
|
||||
binary = afterInstall.find((candidate) => candidate.version === this.appVersion) || afterInstall[0] || existing;
|
||||
if (!binary) {
|
||||
throw new Error('OpenChamber was installed on the remote host but no openchamber binary could be found');
|
||||
}
|
||||
}
|
||||
|
||||
this.setStatus(instance.id, 'server_detecting', 'Detecting managed OpenChamber server');
|
||||
@@ -1165,13 +1253,13 @@ export class ElectronSshManager {
|
||||
if (!remotePort) {
|
||||
this.setStatus(instance.id, 'server_starting', 'Starting managed OpenChamber server');
|
||||
const desiredPort = instance.remoteOpenchamber.preferredPort || randomPortCandidate(instance.id);
|
||||
remotePort = await this.startRemoteServerManaged(parsed, controlPath, instance, desiredPort);
|
||||
remotePort = await this.startRemoteServerManaged(parsed, controlPath, instance, desiredPort, binary.binPath);
|
||||
startedByUs = true;
|
||||
}
|
||||
if (!(await this.remoteServerRunning(parsed, controlPath, remotePort, this.configuredOpenChamberPassword(instance)))) {
|
||||
throw new Error('Managed OpenChamber server failed to become reachable');
|
||||
}
|
||||
return { remotePort, startedByUs };
|
||||
return { remotePort, startedByUs, remoteBinPath: binary.binPath };
|
||||
}
|
||||
|
||||
async disconnectInternal(id, reportIdle) {
|
||||
@@ -1186,7 +1274,7 @@ export class ElectronSshManager {
|
||||
|
||||
if (session) {
|
||||
if (session.startedByUs && session.remotePort && session.instance.remoteOpenchamber.mode === 'managed' && !session.instance.remoteOpenchamber.keepRunning) {
|
||||
await this.stopRemoteServerBestEffort(session.parsed, session.controlPath, session.remotePort);
|
||||
await this.stopRemoteServerBestEffort(session.parsed, session.controlPath, session.remotePort, session.remoteBinPath);
|
||||
}
|
||||
await this.stopControlMasterBestEffort(session.parsed, session.controlPath);
|
||||
const auth = this.sshAuth.get(session.parsed);
|
||||
@@ -1262,9 +1350,10 @@ export class ElectronSshManager {
|
||||
throw new Error(`Unsupported remote OS: ${remoteOs}`);
|
||||
}
|
||||
|
||||
const { remotePort, startedByUs } = await this.ensureRemoteServer(instance, parsed, controlPath);
|
||||
const { remotePort, startedByUs, remoteBinPath } = await this.ensureRemoteServer(instance, parsed, controlPath);
|
||||
session.remotePort = remotePort;
|
||||
session.startedByUs = startedByUs;
|
||||
session.remoteBinPath = remoteBinPath;
|
||||
this.setStatus(id, 'forwarding', 'Setting up port forwards', null, null, remotePort, startedByUs, 0, false);
|
||||
|
||||
const bindHost = sanitizeBindHost(instance.localForward?.bindHost);
|
||||
|
||||
@@ -289,4 +289,161 @@ describe('ElectronSshManager', () => {
|
||||
});
|
||||
expect(settings.desktopHosts).toEqual([{ id: 'ssh-1', label: 'SSH Host', url: localUrl, apiUrl: localUrl, clientToken: 'ssh-client-token' }]);
|
||||
});
|
||||
test('installs OpenChamber into a home-owned npm prefix instead of the root-owned global one', async () => {
|
||||
const commands = [];
|
||||
const manager = new ElectronSshManager({
|
||||
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
|
||||
appVersion: '1.2.3',
|
||||
emit: () => undefined,
|
||||
});
|
||||
manager.resolveRemoteTool = async (_parsed, _controlPath, name) => (name === 'npm' ? '/usr/bin/npm' : null);
|
||||
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
|
||||
commands.push(script);
|
||||
return '';
|
||||
};
|
||||
|
||||
await manager.installOpenChamberManaged({ destination: 'user@example.test', args: [] }, '/tmp/control.sock', '1.2.3', 'auto');
|
||||
|
||||
expect(commands).toHaveLength(1);
|
||||
expect(commands[0]).toContain('--prefix "$HOME/.openchamber/npm-global"');
|
||||
expect(commands[0]).not.toMatch(/npm install -g @openchamber/);
|
||||
});
|
||||
|
||||
test('lists every remote OpenChamber binary with its reported version', async () => {
|
||||
const manager = new ElectronSshManager({
|
||||
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
|
||||
appVersion: '1.2.3',
|
||||
emit: () => undefined,
|
||||
});
|
||||
manager.runRemoteCommand = async () => [
|
||||
'/home/pi/.openchamber/npm-global/bin/openchamber\t1.2.3',
|
||||
'/usr/bin/openchamber\t0.9.0',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const candidates = await manager.remoteOpenChamberCandidates({ destination: 'user@example.test', args: [] }, '/tmp/control.sock');
|
||||
|
||||
expect(candidates).toEqual([
|
||||
{ binPath: '/home/pi/.openchamber/npm-global/bin/openchamber', version: '1.2.3' },
|
||||
{ binPath: '/usr/bin/openchamber', version: '0.9.0' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('starts the resolved OpenChamber binary rather than whatever PATH exposes', async () => {
|
||||
let started = '';
|
||||
const manager = new ElectronSshManager({
|
||||
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
|
||||
appVersion: '1.2.3',
|
||||
emit: () => undefined,
|
||||
});
|
||||
manager.resolveRemoteTool = async () => '/home/pi/.opencode/bin/opencode';
|
||||
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
|
||||
started = script;
|
||||
return '4321\n';
|
||||
};
|
||||
|
||||
const instance = { id: 'ssh-1', auth: {}, remoteOpenchamber: { mode: 'managed' } };
|
||||
const port = await manager.startRemoteServerManaged(
|
||||
{ destination: 'user@example.test', args: [] },
|
||||
'/tmp/control.sock',
|
||||
instance,
|
||||
4321,
|
||||
'/home/pi/.openchamber/npm-global/bin/openchamber',
|
||||
);
|
||||
|
||||
expect(port).toBe(4321);
|
||||
expect(started).toContain("'/home/pi/.openchamber/npm-global/bin/openchamber' serve");
|
||||
expect(started).toContain("OPENCODE_BINARY='/home/pi/.opencode/bin/opencode'");
|
||||
expect(started).toContain('$HOME/.opencode/bin:');
|
||||
});
|
||||
|
||||
test('refuses to start when the remote machine has no opencode CLI', async () => {
|
||||
const manager = new ElectronSshManager({
|
||||
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
|
||||
appVersion: '1.2.3',
|
||||
emit: () => undefined,
|
||||
});
|
||||
manager.resolveRemoteTool = async () => null;
|
||||
manager.runRemoteCommand = async () => {
|
||||
throw new Error('should not start the server without a CLI');
|
||||
};
|
||||
|
||||
await expect(manager.startRemoteServerManaged(
|
||||
{ destination: 'user@example.test', args: [] },
|
||||
'/tmp/control.sock',
|
||||
{ id: 'ssh-1', auth: {}, remoteOpenchamber: { mode: 'managed' } },
|
||||
4321,
|
||||
'/home/pi/.bun/bin/openchamber',
|
||||
)).rejects.toThrow(/opencode CLI is not installed/);
|
||||
});
|
||||
test('prefers a bun that only exists in the home directory over npm', async () => {
|
||||
const commands = [];
|
||||
const manager = new ElectronSshManager({
|
||||
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
|
||||
appVersion: '1.2.3',
|
||||
emit: () => undefined,
|
||||
});
|
||||
// A login shell over SSH does not put ~/.bun/bin on PATH.
|
||||
manager.resolveRemoteTool = async (_parsed, _controlPath, name) =>
|
||||
(name === 'bun' ? '/home/pi/.bun/bin/bun' : '/usr/bin/npm');
|
||||
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
|
||||
commands.push(script);
|
||||
return '';
|
||||
};
|
||||
|
||||
await manager.installOpenChamberManaged({ destination: 'user@example.test', args: [] }, '/tmp/control.sock', '1.2.3', 'auto');
|
||||
|
||||
expect(commands).toEqual(["'/home/pi/.bun/bin/bun' add -g @openchamber/web@1.2.3"]);
|
||||
});
|
||||
test('stops a remote server it started through the CLI, not the authenticated HTTP route', async () => {
|
||||
const scripts = [];
|
||||
const manager = new ElectronSshManager({
|
||||
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
|
||||
appVersion: '1.2.3',
|
||||
emit: () => undefined,
|
||||
});
|
||||
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
|
||||
scripts.push(script);
|
||||
return '';
|
||||
};
|
||||
|
||||
await manager.stopRemoteServerBestEffort(
|
||||
{ destination: 'user@example.test', args: [] },
|
||||
'/tmp/control.sock',
|
||||
41777,
|
||||
'/home/pi/.bun/bin/openchamber',
|
||||
);
|
||||
|
||||
expect(scripts).toEqual(["'/home/pi/.bun/bin/openchamber' stop --port 41777"]);
|
||||
});
|
||||
test('publishes the remote server to its network only with a UI password', async () => {
|
||||
const manager = new ElectronSshManager({
|
||||
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
|
||||
appVersion: '1.2.3',
|
||||
emit: () => undefined,
|
||||
});
|
||||
manager.resolveRemoteTool = async () => '/home/pi/.opencode/bin/opencode';
|
||||
let started = '';
|
||||
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
|
||||
started = script;
|
||||
return '4321\n';
|
||||
};
|
||||
|
||||
const parsed = { destination: 'user@example.test', args: [] };
|
||||
const exposed = {
|
||||
id: 'ssh-1',
|
||||
auth: {},
|
||||
remoteOpenchamber: { mode: 'managed', bindHost: '0.0.0.0' },
|
||||
};
|
||||
|
||||
await expect(manager.startRemoteServerManaged(parsed, '/tmp/control.sock', exposed, 4321, '/bin/openchamber'))
|
||||
.rejects.toThrow(/requires a UI password/);
|
||||
|
||||
const secured = {
|
||||
...exposed,
|
||||
auth: { openchamberPassword: { enabled: true, value: 'remote-secret', store: 'settings' } },
|
||||
};
|
||||
await manager.startRemoteServerManaged(parsed, '/tmp/control.sock', secured, 4321, '/bin/openchamber');
|
||||
expect(started).toContain('--hostname 0.0.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const MISSING_UPDATE_FEED_RE =
|
||||
/404|ENOTFOUND|Cannot find (?:channel|latest)|latest-linux(?:-arm64)?\.yml|HttpError:\s*404|status code 404/i;
|
||||
|
||||
export const isMissingUpdateFeedError = (error) => {
|
||||
const isMissingUpdateFeedError = (error) => {
|
||||
const message = error instanceof Error ? error.message : String(error ?? '');
|
||||
return MISSING_UPDATE_FEED_RE.test(message);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import fsp from 'node:fs/promises';
|
||||
|
||||
const WINDOWS_RETRY_DELAYS_MS = [50, 100, 200, 400, 800, 1_000, 1_000];
|
||||
|
||||
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
|
||||
const isTransientWindowsFileError = (error, platform) => {
|
||||
if (platform !== 'win32') return false;
|
||||
const code = error?.code;
|
||||
return code === 'EPERM' || code === 'EACCES' || code === 'EBUSY';
|
||||
};
|
||||
|
||||
export const replaceFileWithRetry = async (source, target, options = {}) => {
|
||||
const platform = options.platform ?? process.platform;
|
||||
const rename = options.rename ?? fsp.rename;
|
||||
const wait = options.wait ?? sleep;
|
||||
|
||||
for (let attempt = 0; ; attempt += 1) {
|
||||
try {
|
||||
await rename(source, target);
|
||||
return;
|
||||
} catch (error) {
|
||||
const delay = WINDOWS_RETRY_DELAYS_MS[attempt];
|
||||
if (delay === undefined || !isTransientWindowsFileError(error, platform)) throw error;
|
||||
await wait(delay);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { replaceFileWithRetry } from './windows-file-replace.mjs';
|
||||
|
||||
const fileError = (code = 'EPERM') => Object.assign(new Error(code), { code });
|
||||
|
||||
test('retries transient Windows rename failures until replacement succeeds', async () => {
|
||||
const delays = [];
|
||||
let attempts = 0;
|
||||
|
||||
await replaceFileWithRetry('settings.tmp', 'settings.json', {
|
||||
platform: 'win32',
|
||||
rename: async () => {
|
||||
attempts += 1;
|
||||
if (attempts < 4) throw fileError();
|
||||
},
|
||||
wait: async (delay) => delays.push(delay),
|
||||
});
|
||||
|
||||
assert.equal(attempts, 4);
|
||||
assert.deepEqual(delays, [50, 100, 200]);
|
||||
});
|
||||
|
||||
test('does not retry rename errors that are not transient Windows locks', async () => {
|
||||
let attempts = 0;
|
||||
const error = fileError('ENOENT');
|
||||
|
||||
await assert.rejects(
|
||||
replaceFileWithRetry('settings.tmp', 'settings.json', {
|
||||
platform: 'win32',
|
||||
rename: async () => {
|
||||
attempts += 1;
|
||||
throw error;
|
||||
},
|
||||
wait: async () => assert.fail('unexpected wait'),
|
||||
}),
|
||||
error,
|
||||
);
|
||||
|
||||
assert.equal(attempts, 1);
|
||||
});
|
||||
|
||||
test('does not retry transient error codes outside Windows', async () => {
|
||||
let attempts = 0;
|
||||
const error = fileError();
|
||||
|
||||
await assert.rejects(
|
||||
replaceFileWithRetry('settings.tmp', 'settings.json', {
|
||||
platform: 'linux',
|
||||
rename: async () => {
|
||||
attempts += 1;
|
||||
throw error;
|
||||
},
|
||||
wait: async () => assert.fail('unexpected wait'),
|
||||
}),
|
||||
error,
|
||||
);
|
||||
|
||||
assert.equal(attempts, 1);
|
||||
});
|
||||
|
||||
test('returns the final Windows lock error after the retry window', async () => {
|
||||
const delays = [];
|
||||
let attempts = 0;
|
||||
|
||||
await assert.rejects(
|
||||
replaceFileWithRetry('settings.tmp', 'settings.json', {
|
||||
platform: 'win32',
|
||||
rename: async () => {
|
||||
attempts += 1;
|
||||
throw fileError();
|
||||
},
|
||||
wait: async (delay) => delays.push(delay),
|
||||
}),
|
||||
{ code: 'EPERM' },
|
||||
);
|
||||
|
||||
assert.equal(attempts, 8);
|
||||
assert.deepEqual(delays, [50, 100, 200, 400, 800, 1_000, 1_000]);
|
||||
});
|
||||
@@ -1,17 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- usesCleartextTraffic: OpenChamber connects to user-hosted servers over
|
||||
plain http:// on the local network (LAN transport). Android blocks all
|
||||
cleartext HTTP by default (targetSdk >= 28), which silently failed every
|
||||
LAN probe and forced Android onto relay-only. This mirrors the iOS ATS
|
||||
exceptions (NSAllowsArbitraryLoadsInWebContent + NSAllowsLocalNetworking). -->
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
+20
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openchamber/ui",
|
||||
"version": "1.18.0",
|
||||
"version": "1.21.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/main.tsx",
|
||||
@@ -8,7 +8,8 @@
|
||||
"dev": "tsc --noEmit --watch",
|
||||
"build": "tsc --noEmit",
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js"
|
||||
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js",
|
||||
"test": "node ../../scripts/run-isolated-tests.mjs src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aparajita/capacitor-secure-storage": "^8.0.0",
|
||||
@@ -18,38 +19,38 @@
|
||||
"@capacitor/keyboard": "^8.0.0",
|
||||
"@capacitor/push-notifications": "^8.1.1",
|
||||
"@capacitor/status-bar": "^8.0.0",
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/autocomplete": "^6.20.3",
|
||||
"@codemirror/commands": "^6.11.0",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-go": "^6.0.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-html": "^6.4.12",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-markdown": "^6.5.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "6.12.2",
|
||||
"@codemirror/lang-yaml": "^6.1.3",
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/view": "6.39.13",
|
||||
"@codemirror/legacy-modes": "^6.5.3",
|
||||
"@codemirror/lint": "^6.9.7",
|
||||
"@codemirror/search": "^6.7.1",
|
||||
"@codemirror/state": "^6.7.1",
|
||||
"@codemirror/view": "6.43.9",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@legendapp/list": "3.3.8",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@opencode-ai/sdk": "1.18.11",
|
||||
"@opencode-ai/sdk": "1.18.25",
|
||||
"@pierre/diffs": "1.3.0-beta.6",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vim": "^6.4.0",
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@tanstack/react-virtual": "3.14.5",
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"@zumer/snapdom": "^2.12.0",
|
||||
"beautiful-mermaid": "^1.1.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -66,6 +67,7 @@
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"katex": "^0.17.0",
|
||||
"marked": "^17.0.3",
|
||||
"marked-linkify-it": "^4.0.2",
|
||||
"morphdom": "^2.7.7",
|
||||
"motion": "^12.23.24",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -103,6 +105,7 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.5.0",
|
||||
"globals": "^16.3.0",
|
||||
"happy-dom": "^18.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tsx": "^4.20.6",
|
||||
|
||||
+59
-36
@@ -1,23 +1,27 @@
|
||||
import React from 'react';
|
||||
import { MainLayout } from '@/components/layout/MainLayout';
|
||||
import { ChatView } from '@/components/views/ChatView';
|
||||
import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog';
|
||||
import { FireworksProvider } from '@/contexts/FireworksContext';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel';
|
||||
import { setStreamPerfEnabled } from '@/stores/utils/streamDebug';
|
||||
import { setRequestsInFlightTrackingEnabled } from '@/stores/utils/requestsInFlight';
|
||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||
// useEventStream removed — replaced by SyncProvider + SyncBridge
|
||||
import { useMenuActions } from '@/hooks/useMenuActions';
|
||||
import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap';
|
||||
import { useTraySync } from '@/hooks/useTraySync';
|
||||
import { useGlobalSessionsPolling } from '@/hooks/useGlobalSessionsPolling';
|
||||
import { useRouter } from '@/hooks/useRouter';
|
||||
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
|
||||
import { useWebNotificationStream } from '@/hooks/useWebNotificationStream';
|
||||
import { useAgentMemorySync } from '@/hooks/useAgentMemorySync';
|
||||
import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt';
|
||||
import { useWindowTitle } from '@/hooks/useWindowTitle';
|
||||
import { useRootScrollLock } from '@/hooks/useRootScrollLock';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { hasModifier } from '@/lib/utils';
|
||||
import { isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp, invokeDesktop } from '@/lib/desktop';
|
||||
import {
|
||||
getInjectedBootOutcome,
|
||||
@@ -32,7 +36,6 @@ import type { RecoveryVariant } from '@/components/onboarding/DesktopConnectionR
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { markSessionViewed } from '@/sync/notification-store';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { getRuntimeKey, subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch';
|
||||
@@ -54,7 +57,11 @@ import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { applyMobileKeyboardMode } from '@/lib/mobileKeyboardMode';
|
||||
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
|
||||
import {
|
||||
EMBEDDED_VISIBILITY_UPDATE,
|
||||
isEmbeddedSessionChat,
|
||||
requestEmbeddedSessionVisibility,
|
||||
} from '@/components/layout/contextPanelEmbeddedChat';
|
||||
import { SyncAppEffects } from '@/apps/AppEffects';
|
||||
import { resetAppForRuntimeEndpointChange } from '@/apps/runtimeEndpointReset';
|
||||
import { useAppFontEffects } from '@/apps/useAppFontEffects';
|
||||
@@ -106,6 +113,7 @@ type EmbeddedSessionChatConfig = {
|
||||
sessionId: string;
|
||||
directory: string | null;
|
||||
readOnly: boolean;
|
||||
allowPromptingSubagentSessions?: boolean;
|
||||
};
|
||||
|
||||
type EmbeddedVisibilityPayload = {
|
||||
@@ -138,6 +146,9 @@ const readEmbeddedSessionChatConfig = (): EmbeddedSessionChatConfig | null => {
|
||||
sessionId,
|
||||
directory,
|
||||
readOnly: params.get('readOnly') === '1' || params.get('readOnly') === 'true',
|
||||
allowPromptingSubagentSessions: params.has('allowPromptingSubagentSessions')
|
||||
? params.get('allowPromptingSubagentSessions') === '1'
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -199,7 +210,16 @@ const EmbeddedSessionChatContent: React.FC<{
|
||||
<>
|
||||
<SyncAppEffects embeddedBackgroundWorkEnabled={embeddedBackgroundWorkEnabled} />
|
||||
<OpenCodeUpdateToast />
|
||||
<ChatView readOnly={embeddedSessionChat.readOnly} />
|
||||
<ChatView
|
||||
active={embeddedBackgroundWorkEnabled}
|
||||
// Always subscribe to message history in the mounted session-chat
|
||||
// iframe. Visibility still gates composer focus and background work so
|
||||
// a boot-inactive / lost-handshake race cannot leave a busy subagent
|
||||
// showing only its status row (#2903 / #2892).
|
||||
messagesEnabled={true}
|
||||
readOnly={embeddedSessionChat.readOnly}
|
||||
initialAllowPromptingSubagentSessions={embeddedSessionChat.allowPromptingSubagentSessions}
|
||||
/>
|
||||
<Toaster />
|
||||
</>
|
||||
);
|
||||
@@ -228,7 +248,10 @@ function App({ apis }: AppProps) {
|
||||
const [showMemoryDebug, setShowMemoryDebug] = React.useState(false);
|
||||
const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
|
||||
const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState<boolean>(() => apis.runtime.isVSCode);
|
||||
const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true);
|
||||
// Embedded chats start inactive until the parent panel identifies the active
|
||||
// tab. Otherwise a newly loaded background tab can focus its composer first
|
||||
// and steal keyboard input from the main chat.
|
||||
const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(false);
|
||||
const [initRetryExhausted, setInitRetryExhausted] = React.useState(false);
|
||||
const [initRetryEpoch, setInitRetryEpoch] = React.useState(0);
|
||||
const [runtimeEndpointEpoch, setRuntimeEndpointEpoch] = React.useState(0);
|
||||
@@ -258,6 +281,13 @@ function App({ apis }: AppProps) {
|
||||
};
|
||||
}, [showMemoryDebug]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setRequestsInFlightTrackingEnabled(showMemoryDebug);
|
||||
return () => {
|
||||
setRequestsInFlightTrackingEnabled(false);
|
||||
};
|
||||
}, [showMemoryDebug]);
|
||||
|
||||
React.useEffect(() => {
|
||||
applyMobileKeyboardMode(mobileKeyboardMode);
|
||||
}, [mobileKeyboardMode]);
|
||||
@@ -527,17 +557,16 @@ function App({ apis }: AppProps) {
|
||||
}
|
||||
|
||||
const applyVisibility = (payload?: EmbeddedVisibilityPayload) => {
|
||||
const nextVisible = payload?.visible === true;
|
||||
setIsEmbeddedVisible(nextVisible);
|
||||
setIsEmbeddedVisible(payload?.visible === true);
|
||||
};
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) {
|
||||
if (event.origin !== window.location.origin || event.source !== window.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.data as { type?: unknown; payload?: EmbeddedVisibilityPayload };
|
||||
if (data?.type !== 'openchamber:embedded-visibility') {
|
||||
if (data?.type !== EMBEDDED_VISIBILITY_UPDATE) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -550,6 +579,7 @@ function App({ apis }: AppProps) {
|
||||
|
||||
scopedWindow.__openchamberSetEmbeddedVisibility = applyVisibility;
|
||||
window.addEventListener('message', handleMessage);
|
||||
requestEmbeddedSessionVisibility();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
@@ -604,7 +634,6 @@ function App({ apis }: AppProps) {
|
||||
const directory = typeof detail?.directory === 'string' && detail.directory.trim().length > 0
|
||||
? detail.directory.trim()
|
||||
: null;
|
||||
useUIStore.getState().setActiveMainTab('chat');
|
||||
void useSessionUIStore.getState().setCurrentSession(sessionId, directory);
|
||||
};
|
||||
|
||||
@@ -618,12 +647,9 @@ function App({ apis }: AppProps) {
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const onOpenMiniChat = () => {
|
||||
const currentDir = useDirectoryStore.getState().currentDirectory;
|
||||
const { activeProjectId, projects } = useProjectsStore.getState();
|
||||
const activeProject = projects.find((p) => p.id === activeProjectId) ?? null;
|
||||
void invokeDesktop('desktop_open_draft_mini_chat_window', {
|
||||
directory: currentDir || activeProject?.path || '',
|
||||
projectId: activeProject?.id ?? null,
|
||||
directory: '',
|
||||
projectId: null,
|
||||
});
|
||||
};
|
||||
window.addEventListener('openchamber:open-mini-chat', onOpenMiniChat);
|
||||
@@ -655,11 +681,12 @@ function App({ apis }: AppProps) {
|
||||
const projectId = typeof detail?.projectId === 'string' && detail.projectId.trim().length > 0
|
||||
? detail.projectId.trim()
|
||||
: null;
|
||||
useUIStore.getState().setActiveMainTab('chat');
|
||||
const hasProjectTarget = Boolean(directory || projectId);
|
||||
useUIStore.getState().setSessionSwitcherOpen(false);
|
||||
useSessionUIStore.getState().openNewSessionDraft({
|
||||
selectedProjectId: projectId,
|
||||
directoryOverride: directory,
|
||||
target: hasProjectTarget ? 'project' : 'chat',
|
||||
selectedProjectId: hasProjectTarget ? projectId : null,
|
||||
directoryOverride: hasProjectTarget ? directory : null,
|
||||
preserveDirectoryOverride: Boolean(directory),
|
||||
});
|
||||
};
|
||||
@@ -683,10 +710,16 @@ function App({ apis }: AppProps) {
|
||||
|
||||
usePushVisibilityBeacon({ enabled: embeddedBackgroundWorkEnabled });
|
||||
useWebNotificationStream({ enabled: embeddedBackgroundWorkEnabled });
|
||||
// Loaded here rather than by the Memory tab: the session index is built from
|
||||
// this snapshot, so leaving it to the panel meant a user who never opened
|
||||
// Project notes sent every message with no memory index at all.
|
||||
useAgentMemorySync(currentDirectory || null);
|
||||
usePwaInstallPrompt();
|
||||
|
||||
useWindowTitle();
|
||||
|
||||
useRootScrollLock();
|
||||
|
||||
useRouter();
|
||||
|
||||
const handleToggleMemoryDebug = React.useCallback(() => {
|
||||
@@ -696,28 +729,16 @@ function App({ apis }: AppProps) {
|
||||
useMenuActions(handleToggleMemoryDebug);
|
||||
|
||||
useTraySync();
|
||||
useGlobalSessionsPolling(!embeddedSessionChat);
|
||||
|
||||
useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled });
|
||||
|
||||
// Palette-only action: the memory debug panel has no keyboard shortcut.
|
||||
React.useEffect(() => {
|
||||
if (embeddedSessionChat) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const isDebugShortcut = hasModifier(e)
|
||||
&& e.shiftKey
|
||||
&& !e.altKey
|
||||
&& (e.code === 'KeyD' || e.key.toLowerCase() === 'd');
|
||||
|
||||
if (isDebugShortcut) {
|
||||
e.preventDefault();
|
||||
setShowMemoryDebug(prev => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown, true);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown, true);
|
||||
if (embeddedSessionChat) return;
|
||||
const handleToggle = () => setShowMemoryDebug((previous) => !previous);
|
||||
window.addEventListener('openchamber:memory-debug-toggle', handleToggle);
|
||||
return () => window.removeEventListener('openchamber:memory-debug-toggle', handleToggle);
|
||||
}, [embeddedSessionChat]);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -883,6 +904,7 @@ function App({ apis }: AppProps) {
|
||||
isVSCodeRuntime={isVSCodeRuntime}
|
||||
embeddedBackgroundWorkEnabled={embeddedBackgroundWorkEnabled}
|
||||
/>
|
||||
<AppLinkConfirmDialog />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</RuntimeAPIProvider>
|
||||
@@ -926,6 +948,7 @@ function App({ apis }: AppProps) {
|
||||
<OpenCodeUpdateToast />
|
||||
<MainLayout />
|
||||
<Toaster />
|
||||
<AppLinkConfirmDialog />
|
||||
{!isBootShell && (
|
||||
<>
|
||||
<ConfigUpdateOverlay />
|
||||
|
||||
@@ -5,8 +5,10 @@ import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { MiniChatLayout } from '@/components/mini-chat/MiniChatLayout';
|
||||
import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog';
|
||||
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
|
||||
import { useWindowTitle } from '@/hooks/useWindowTitle';
|
||||
import { useRootScrollLock } from '@/hooks/useRootScrollLock';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import type { RuntimeAPIs } from '@/lib/api/types';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
worktreeMapsEqual,
|
||||
} from '@/lib/worktrees/worktreeManager';
|
||||
import type { WorktreeMetadata } from '@/types/worktree';
|
||||
import { CHAT_DRAFT_PROJECT_ID } from '@/lib/chatDirectories';
|
||||
|
||||
const MINI_CHAT_PRESENCE_CHANNEL = 'openchamber:mini-chat-presence';
|
||||
|
||||
@@ -153,9 +156,9 @@ const MiniChatBootstrap: React.FC<{ config: MiniChatConfig }> = ({ config }) =>
|
||||
const sessionId = typeof detail?.sessionId === 'string' ? detail.sessionId.trim() : '';
|
||||
if (!sessionId) return;
|
||||
if (useSessionUIStore.getState().currentSessionId === sessionId) return;
|
||||
const directory = typeof detail?.directory === 'string' && detail.directory.trim().length > 0
|
||||
? detail.directory.trim()
|
||||
: (sessions.find((entry) => entry.id === sessionId) as { directory?: string | null } | undefined)?.directory ?? null;
|
||||
const sessionDirectory = (sessions.find((entry) => entry.id === sessionId) as { directory?: string | null } | undefined)?.directory?.trim();
|
||||
const directory = sessionDirectory
|
||||
|| (typeof detail?.directory === 'string' && detail.directory.trim().length > 0 ? detail.directory.trim() : null);
|
||||
void sync.ensureSessionRenderable(sessionId);
|
||||
setCurrentSession(sessionId, directory);
|
||||
sessionBootstrappedRef.current = true;
|
||||
@@ -166,9 +169,11 @@ const MiniChatBootstrap: React.FC<{ config: MiniChatConfig }> = ({ config }) =>
|
||||
|
||||
React.useEffect(() => {
|
||||
if (config.mode !== 'draft' || draftOpen || currentSessionId) return;
|
||||
const hasProjectTarget = Boolean(config.projectId || config.directory);
|
||||
openNewSessionDraft({
|
||||
selectedProjectId: config.projectId,
|
||||
directoryOverride: config.directory,
|
||||
target: hasProjectTarget ? 'project' : 'chat',
|
||||
selectedProjectId: hasProjectTarget ? config.projectId : CHAT_DRAFT_PROJECT_ID,
|
||||
directoryOverride: hasProjectTarget ? config.directory : null,
|
||||
preserveDirectoryOverride: Boolean(config.directory),
|
||||
});
|
||||
}, [config, currentSessionId, draftOpen, openNewSessionDraft]);
|
||||
@@ -278,10 +283,11 @@ const MiniChatPresencePublisher: React.FC = () => {
|
||||
const useSessionUnavailable = (config: MiniChatConfig): boolean => {
|
||||
const sessions = useSessions();
|
||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||
const draftOpen = useSessionUIStore((state) => state.newSessionDraft.open);
|
||||
const [timedOut, setTimedOut] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (config.mode !== 'session' || !config.sessionId || currentSessionId === config.sessionId) {
|
||||
if (draftOpen || config.mode !== 'session' || !config.sessionId || currentSessionId) {
|
||||
setTimedOut(false);
|
||||
return;
|
||||
}
|
||||
@@ -291,7 +297,7 @@ const useSessionUnavailable = (config: MiniChatConfig): boolean => {
|
||||
}
|
||||
const timeout = window.setTimeout(() => setTimedOut(true), 5000);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [config.mode, config.sessionId, currentSessionId, sessions]);
|
||||
}, [config.mode, config.sessionId, currentSessionId, draftOpen, sessions]);
|
||||
|
||||
return timedOut;
|
||||
};
|
||||
@@ -313,6 +319,7 @@ export function ElectronMiniChatApp({ apis }: ElectronMiniChatAppProps) {
|
||||
useMiniChatKeyboardShortcuts();
|
||||
usePushVisibilityBeacon({ enabled: true });
|
||||
useWindowTitle();
|
||||
useRootScrollLock();
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
@@ -321,6 +328,7 @@ export function ElectronMiniChatApp({ apis }: ElectronMiniChatAppProps) {
|
||||
<TooltipProvider delayDuration={300} skipDelayDuration={150}>
|
||||
<div className="h-full text-foreground bg-background">
|
||||
<ElectronMiniChatContent config={config} />
|
||||
<AppLinkConfirmDialog />
|
||||
<Toaster />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -9,8 +9,10 @@ import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
|
||||
import { ChatView } from '@/components/views/ChatView';
|
||||
import { PlanView } from '@/components/views/PlanView';
|
||||
import { SettingsView } from '@/components/views/SettingsView';
|
||||
import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog';
|
||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||
import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
|
||||
import { useAuthSessionStore } from '@/lib/runtime-auth-expiry';
|
||||
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
@@ -20,6 +22,7 @@ import { useUpdatePolling } from '@/hooks/useUpdatePolling';
|
||||
import { useWindowTitle } from '@/hooks/useWindowTitle';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import type { RuntimeAPIs } from '@/lib/api/types';
|
||||
import type { ProjectRef } from '@/lib/projectContextApi';
|
||||
import { readTabletLayout, useOrientation, useTabletLayout } from '@/lib/device';
|
||||
import { useHardwareKeyboard } from '@/lib/hardwareKeyboard';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
@@ -54,7 +57,7 @@ import { MobileSessionsSheet } from './MobileSessionsSheet';
|
||||
import { MobileFullscreenSurface } from './MobileFullscreenSurface';
|
||||
import { MobileWorkspaceDrawer, type MobileWorkspaceTab } from './MobileWorkspaceDrawer';
|
||||
import { DedicatedMobileAppProvider, type MobileAppActions } from './mobileAppContext';
|
||||
import { autoConnectLastInstance, getAutoConnectTargetLabel, reprobeActiveConnection, type AutoConnectOutcome } from './mobileConnections';
|
||||
import { autoConnectLastInstance, getAutoConnectTargetLabel, logMobileConnectEvent, reprobeActiveConnection, type AutoConnectOutcome } from './mobileConnections';
|
||||
import { isCapacitorMobileApp, useNativeAndroidBackButton, useNativeMobileChrome, useNativeMobileLifecycle } from './mobileNativeChrome';
|
||||
import { reconnectAppForTransportSwitch, resetAppForRuntimeEndpointChange } from './runtimeEndpointReset';
|
||||
import { useAppFontEffects } from './useAppFontEffects';
|
||||
@@ -83,6 +86,7 @@ const MOBILE_SETTINGS_PAGES = [
|
||||
'providers',
|
||||
'usage',
|
||||
'voice',
|
||||
'integrations',
|
||||
'about',
|
||||
] as const;
|
||||
|
||||
@@ -108,7 +112,7 @@ const MobileShell: React.FC<{ onActiveConnectionDeleted: () => void }> = ({ onAc
|
||||
const [workspaceTab, setWorkspaceTab] = React.useState<MobileWorkspaceTab>('changes');
|
||||
// A plan opened from the workspace drawer's Notes tab, shown as a fullscreen
|
||||
// layer on top of it (back returns to the notes).
|
||||
const [openPlan, setOpenPlan] = React.useState<{ path: string; title: string } | null>(null);
|
||||
const [openPlan, setOpenPlan] = React.useState<{ id: string; title: string; projectRef: ProjectRef } | null>(null);
|
||||
const [settingsInitialMobileStage, setSettingsInitialMobileStage] = React.useState<'nav' | 'page-content'>('nav');
|
||||
// When set, the Changes surface opens directly into the per-file diff for this path.
|
||||
const [pendingChangesDiff, setPendingChangesDiff] = React.useState<{ path: string; staged: boolean } | null>(null);
|
||||
@@ -539,7 +543,7 @@ const MobileShell: React.FC<{ onActiveConnectionDeleted: () => void }> = ({ onAc
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<PlanView
|
||||
targetPath={openPlan.path}
|
||||
savedProjectPlan={{ projectRef: openPlan.projectRef, planId: openPlan.id }}
|
||||
onNavigatedToChat={() => {
|
||||
closeSurface();
|
||||
closeWorkspace();
|
||||
@@ -660,9 +664,11 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
// saved instance instead of dead-ending on the connect screen until the
|
||||
// user restarts the app. Success fires runtime-endpoint-changed, which
|
||||
// re-bootstraps everything.
|
||||
logMobileConnectEvent('resume:auto-connect', {});
|
||||
void autoConnectLastInstance();
|
||||
return;
|
||||
}
|
||||
logMobileConnectEvent('resume:reprobe', {});
|
||||
|
||||
// Re-probe the active device's transports on resume: the network may have
|
||||
// changed while the app slept, so hot-switch LAN⇄relay if a better transport
|
||||
@@ -675,7 +681,8 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
if (providersCount === 0) void loadProviders({ source: 'mobileApp:nativeResume' });
|
||||
if (agentsCount === 0) void loadAgents({ source: 'mobileApp:nativeResume' });
|
||||
};
|
||||
const disconnect = () => {
|
||||
const disconnect = (reason: string) => {
|
||||
logMobileConnectEvent('resume:disconnect', { reason });
|
||||
switchRuntimeEndpoint({ apiBaseUrl: '', clientToken: null, runtimeKey: 'mobile-disconnected' });
|
||||
setConnectionEpoch((value) => value + 1);
|
||||
};
|
||||
@@ -683,36 +690,50 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
void reprobeActiveConnection().then((outcome) => {
|
||||
if (nativeResumeValidationSeqRef.current !== validationSeq) return;
|
||||
if (outcome === 'no-connection') {
|
||||
disconnect();
|
||||
disconnect('no-connection');
|
||||
return;
|
||||
}
|
||||
if (outcome === 'needs-login') {
|
||||
// Token explicitly rejected (revoked/expired) — tell the user why they
|
||||
// land back on the connect screen instead of silently bouncing them.
|
||||
setAutoConnectNotice({ kind: 'auth-expired', label: getAutoConnectTargetLabel() ?? '' });
|
||||
disconnect();
|
||||
disconnect('needs-login');
|
||||
return;
|
||||
}
|
||||
if (outcome === 'unreachable') {
|
||||
// Right after a resume or Wi-Fi switch the network is often still
|
||||
// settling (on Android without a SIM there is NO connectivity at all for
|
||||
// a few seconds), so a single fast probe races the network coming up.
|
||||
// Retry once after a grace period before tearing the connection down.
|
||||
window.setTimeout(() => {
|
||||
if (nativeResumeValidationSeqRef.current !== validationSeq) return;
|
||||
void reprobeActiveConnection().then((retry) => {
|
||||
// settling (Android without a SIM has NO connectivity for a few
|
||||
// seconds; a WireGuard tunnel re-handshakes; a relay cold start pays
|
||||
// TLS + WS + E2EE before it can answer), so a single fast probe races
|
||||
// the network coming up. Retry on a widening grace ladder before
|
||||
// tearing the connection down — the last attempt runs with the full
|
||||
// connect budget so slow-but-alive transports get a real chance.
|
||||
const retryDelaysMs = [4000, 10000];
|
||||
const retryAt = (attempt: number) => {
|
||||
window.setTimeout(() => {
|
||||
if (nativeResumeValidationSeqRef.current !== validationSeq) return;
|
||||
if (retry === 'switched') return;
|
||||
if (retry === 'unchanged') {
|
||||
refreshInPlace();
|
||||
return;
|
||||
}
|
||||
if (retry === 'needs-login') {
|
||||
setAutoConnectNotice({ kind: 'auth-expired', label: getAutoConnectTargetLabel() ?? '' });
|
||||
}
|
||||
disconnect();
|
||||
});
|
||||
}, 4000);
|
||||
const lastAttempt = attempt === retryDelaysMs.length - 1;
|
||||
void reprobeActiveConnection({ fast: !lastAttempt }).then((retry) => {
|
||||
if (nativeResumeValidationSeqRef.current !== validationSeq) return;
|
||||
if (retry === 'switched') return;
|
||||
if (retry === 'unchanged') {
|
||||
refreshInPlace();
|
||||
return;
|
||||
}
|
||||
if (retry === 'needs-login') {
|
||||
setAutoConnectNotice({ kind: 'auth-expired', label: getAutoConnectTargetLabel() ?? '' });
|
||||
disconnect('retry-needs-login');
|
||||
return;
|
||||
}
|
||||
if (!lastAttempt) {
|
||||
retryAt(attempt + 1);
|
||||
return;
|
||||
}
|
||||
disconnect(`retry-${retry}`);
|
||||
});
|
||||
}, retryDelaysMs[attempt]);
|
||||
};
|
||||
retryAt(0);
|
||||
return;
|
||||
}
|
||||
if (outcome === 'switched') return;
|
||||
@@ -753,6 +774,23 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
};
|
||||
}, [isNativeMobileApp, handleNativeResume]);
|
||||
|
||||
// A confirmed mid-session auth expiry (classified centrally from live 401
|
||||
// traffic) runs the same seq-guarded re-probe the resume path uses: it ends
|
||||
// in needs-login → the native welcome screen with the auth-expired notice.
|
||||
// The shared web banner never renders on native (the session gate is not
|
||||
// mounted here), so this is the only surface reacting to the signal.
|
||||
React.useEffect(() => {
|
||||
if (!isNativeMobileApp) return;
|
||||
return useAuthSessionStore.subscribe((store, previous) => {
|
||||
if (store.state === 'expired' && previous.state !== 'expired') {
|
||||
handleNativeResume();
|
||||
// The probe ladder owns the outcome from here; the shared store goes
|
||||
// back to 'ok' so a later expiry can signal again.
|
||||
useAuthSessionStore.getState().markAuthenticated();
|
||||
}
|
||||
});
|
||||
}, [isNativeMobileApp, handleNativeResume]);
|
||||
|
||||
React.useEffect(() => {
|
||||
registerRuntimeAPIs(apis);
|
||||
return () => registerRuntimeAPIs(null);
|
||||
@@ -764,6 +802,15 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
// stale. The SyncProvider is keyed by runtimeEndpointEpoch so it remounts too.
|
||||
React.useEffect(() => {
|
||||
return subscribeRuntimeEndpointChanged((detail) => {
|
||||
// Catch-all trail entry: EVERY endpoint change lands here regardless of
|
||||
// which code path triggered it, so a "kicked to the connect screen"
|
||||
// report always shows what dropped the runtime even when the trigger
|
||||
// itself is not instrumented.
|
||||
logMobileConnectEvent('endpoint:changed', {
|
||||
runtimeKey: detail.runtimeKey || 'none',
|
||||
previousRuntimeKey: detail.previousRuntimeKey || 'none',
|
||||
connected: Boolean(detail.apiBaseUrl),
|
||||
});
|
||||
// A LAN⇄relay swap for the SAME device keeps the runtime key stable. Treat
|
||||
// that as a transport-only change: rebind the sync layer to the new
|
||||
// transport but keep the user's session/connection state — no reconnecting
|
||||
@@ -800,19 +847,30 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
}
|
||||
let cancelled = false;
|
||||
setAutoConnectPhase('attempting');
|
||||
void autoConnectLastInstance()
|
||||
.catch((): AutoConnectOutcome => ({ status: 'no-candidate' }))
|
||||
.then((outcome) => {
|
||||
if (cancelled) return;
|
||||
// Landing on the connect screen silently reads as data loss — say WHY
|
||||
// the saved instance didn't come back (unreachable vs revoked auth).
|
||||
if (outcome.status === 'unreachable') {
|
||||
setAutoConnectNotice({ kind: 'unreachable', label: outcome.label });
|
||||
} else if (outcome.status === 'needs-login') {
|
||||
setAutoConnectNotice({ kind: 'auth-expired', label: outcome.label });
|
||||
}
|
||||
setAutoConnectPhase('done');
|
||||
});
|
||||
void (async () => {
|
||||
const outcome = await autoConnectLastInstance()
|
||||
.catch((): AutoConnectOutcome => ({ status: 'no-candidate' }));
|
||||
if (cancelled) return;
|
||||
// Landing on the connect screen silently reads as data loss — say WHY
|
||||
// the saved instance didn't come back (unreachable vs revoked auth).
|
||||
if (outcome.status === 'unreachable') {
|
||||
setAutoConnectNotice({ kind: 'unreachable', label: outcome.label });
|
||||
} else if (outcome.status === 'needs-login') {
|
||||
setAutoConnectNotice({ kind: 'auth-expired', label: outcome.label });
|
||||
}
|
||||
// Release the splash on the fast verdict — a dead server must not pin
|
||||
// the logo for the full connect budget. The fast probe races a
|
||||
// just-woken network/relay (WireGuard re-handshake, relay TLS + WS +
|
||||
// E2EE cold start), so a false "unreachable" is common right after
|
||||
// launch: retry once IN THE BACKGROUND with the full budget. A success
|
||||
// switches the runtime and the app moves in from the connect screen on
|
||||
// its own; a manual connect the user started meanwhile wins via
|
||||
// skipIfConnected.
|
||||
setAutoConnectPhase('done');
|
||||
if (outcome.status === 'unreachable') {
|
||||
void autoConnectLastInstance({ fast: false, skipIfConnected: true }).catch(() => null);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
@@ -833,6 +891,7 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
if (!isNativeMobileApp || !getRuntimeApiBaseUrl()) return;
|
||||
let cancelled = false;
|
||||
const dropToConnectScreen = (notice: MobileConnectionNotice | null) => {
|
||||
logMobileConnectEvent('cold-launch:drop', { kind: notice?.kind ?? 'unknown' });
|
||||
if (notice) setAutoConnectNotice(notice);
|
||||
switchRuntimeEndpoint({ apiBaseUrl: '', clientToken: null, runtimeKey: 'mobile-disconnected' });
|
||||
setConnectionEpoch((value) => value + 1);
|
||||
@@ -847,7 +906,14 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
return;
|
||||
}
|
||||
if (outcome === 'unreachable') {
|
||||
// A fast probe racing the just-woken network/relay produces false
|
||||
// "unreachable" verdicts (seen in the field: the same LAN candidate
|
||||
// refuses on launch and answers 200 two minutes later). Show the
|
||||
// connect screen on the fast verdict — no splash hostage — and retry
|
||||
// once in the background with the full budget; a success reconnects
|
||||
// the app from the connect screen on its own.
|
||||
dropToConnectScreen(label ? { kind: 'unreachable', label } : null);
|
||||
void autoConnectLastInstance({ fast: false, skipIfConnected: true }).catch(() => null);
|
||||
return;
|
||||
}
|
||||
// 'no-connection': at cold start the runtime key may not map to a saved
|
||||
@@ -1212,6 +1278,7 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
switchRuntimeEndpoint({ apiBaseUrl: '', clientToken: null, runtimeKey: 'mobile-disconnected' });
|
||||
setConnectionEpoch((value) => value + 1);
|
||||
}} />
|
||||
<AppLinkConfirmDialog />
|
||||
<Toaster position="top-center" offset="calc(var(--oc-safe-area-top, 0px) + 16px)" />
|
||||
{isInitialized ? <ConfigUpdateOverlay /> : null}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
import { formatMobileConnectDebugEntry, getMobileConnectDebugEntries, getMobileConnectDebugText } from './mobileConnectionDebug';
|
||||
|
||||
// Hidden diagnostics surface for device-only connection bugs: renders the
|
||||
// in-memory connection event trail with one-tap copy, so a user on a release
|
||||
// build (no tethered debugger, no Web Inspector) can paste the exact probe
|
||||
// sequence into a bug report. Opened via long-press easter eggs on the connect
|
||||
// screen logo and the instances list — invisible unless you know it's there.
|
||||
export const MobileConnectionDebugPanel: React.FC<{ onClose: () => void }> = ({ onClose }) => {
|
||||
const { t } = useI18n();
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
// Snapshot on open; a live-updating log under the user's finger would fight
|
||||
// the copy button. Reopen to refresh.
|
||||
const entries = React.useMemo(() => getMobileConnectDebugEntries(), []);
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
void copyTextToClipboard(getMobileConnectDebugText()).then((result) => {
|
||||
if (!result.ok) return;
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[70] flex flex-col bg-background pb-[var(--safe-area-inset-bottom,env(safe-area-inset-bottom,0px))] pt-[var(--safe-area-inset-top,env(safe-area-inset-top,0px))] text-foreground">
|
||||
<div className="flex items-center justify-between gap-2 border-b border-border/70 px-4 py-2.5">
|
||||
<h2 className="min-w-0 truncate typography-ui-label text-foreground">{t('mobile.connectionDebug.title')}</h2>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<Button type="button" variant="outline" size="sm" onClick={handleCopy} disabled={entries.length === 0}>
|
||||
<Icon name={copied ? 'check' : 'file-copy'} className="size-4" />
|
||||
{copied ? t('mobile.connectionDebug.copied') : t('mobile.connectionDebug.copy')}
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" size="icon" aria-label={t('mobile.connectionDebug.close')} onClick={onClose}>
|
||||
<Icon name="close" className="size-[18px]" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-4 py-3">
|
||||
{entries.length === 0 ? (
|
||||
<p className="typography-small text-muted-foreground">{t('mobile.connectionDebug.empty')}</p>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap break-words typography-code text-muted-foreground">
|
||||
{entries.map(formatMobileConnectDebugEntry).join('\n')}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,8 @@ import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { connectionDisplayUrl, useMobileConnection } from './mobileConnections';
|
||||
import { useDebugPanelLongPress } from './mobileConnectionDebug';
|
||||
import { MobileConnectionDebugPanel } from './MobileConnectionDebugPanel';
|
||||
import { isQrScanSupported, parseConnectionPayload, scanConnectionQr } from './mobileQrScan';
|
||||
import { mobileConnectionInputClass, mobileInputKeyboardProps } from './mobileConnectionUi';
|
||||
import { MobileQrConnectionLoading, MobileQrScannerOverlay } from './MobileQrScannerOverlay';
|
||||
@@ -37,6 +39,10 @@ export const MobileConnectionWelcome: React.FC<{
|
||||
// Which saved connection is being connected to, for the per-row spinner.
|
||||
const [connectingId, setConnectingId] = React.useState<string | null>(null);
|
||||
const [password, setPassword] = React.useState('');
|
||||
// Hidden diagnostics: long-press the logo to open the connection event log —
|
||||
// reachable even when a user has been bounced back to this screen.
|
||||
const [debugOpen, setDebugOpen] = React.useState(false);
|
||||
const debugLongPress = useDebugPanelLongPress(React.useCallback(() => setDebugOpen(true), []));
|
||||
|
||||
const handleSubmit = React.useCallback((event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -127,10 +133,13 @@ export const MobileConnectionWelcome: React.FC<{
|
||||
<>
|
||||
{isScanning ? <MobileQrScannerOverlay onCancel={() => scanAbortRef.current?.abort()} /> : null}
|
||||
{isCompletingScan ? <MobileQrConnectionLoading /> : null}
|
||||
{debugOpen ? <MobileConnectionDebugPanel onClose={() => setDebugOpen(false)} /> : null}
|
||||
<main className="oc-keyboard-fill-screen flex min-h-dvh flex-col overflow-y-auto bg-background px-6 pb-[calc(var(--safe-area-inset-bottom,env(safe-area-inset-bottom,0px))+28px)] pt-[calc(var(--safe-area-inset-top,env(safe-area-inset-top,0px))+28px)] text-foreground">
|
||||
<div className="m-auto flex w-full max-w-[360px] shrink-0 flex-col items-center gap-9 py-8">
|
||||
<div className="flex flex-col items-center gap-5 text-center">
|
||||
<OpenChamberLogo width={72} height={72} className="size-[72px]" />
|
||||
<span {...debugLongPress} className="select-none" style={{ touchAction: 'manipulation' }}>
|
||||
<OpenChamberLogo width={72} height={72} className="size-[72px]" />
|
||||
</span>
|
||||
<h1 className="typography-h2 text-foreground">{t('mobile.connect.welcome.title')}</h1>
|
||||
</div>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user