Merge remote-tracking branch 'origin/main' into feat/nested-git-repos

# Conflicts:
#	packages/ui/src/components/views/GitView.tsx
#	packages/ui/src/stores/DOCUMENTATION.md
#	packages/ui/src/stores/useGitStore.ts
This commit is contained in:
jaygupta17
2026-08-30 09:48:35 +05:30
640 changed files with 46571 additions and 5636 deletions
+3 -1
View File
@@ -205,12 +205,13 @@ other language mirrors the English files under a locale folder.
| French | `fr/` | `fr` |
| German | `de/` | `de` |
| Japanese | `ja/` | `ja` |
| Turkish | `tr/` | `tr` |
> [!IMPORTANT]
> The **content folder** uses the lowercase locale key (`zh-cn`, `pt-br`); the
> **sidebar `translations`** key uses the BCP-47 language tag (`zh-CN`, `pt-BR`).
> They look similar but are not interchangeable — Starlight resolves them with
> different rules. Everything else (`uk`, `es`, `ko`, `pl`, `fr`, `de`, `ja`, `en`) is identical
> different rules. Everything else (`uk`, `es`, `ko`, `pl`, `fr`, `de`, `ja`, `tr`, `en`) is identical
> in both columns.
This locale set is mirrored in the website at
@@ -232,6 +233,7 @@ content/docs/
ko/install.mdx # Korean
pl/install.mdx # Polish
fr/install.mdx # French
tr/install.mdx # Turkish
ja/install.mdx # Japanese
guides/tunnels.mdx # nested English page
+1 -1
View File
@@ -6,7 +6,7 @@ This package is the source-of-truth for OpenChamber public docs content.
- `content/docs/*.mdx` - English docs pages (source of truth)
- `content/docs/<locale>/*.mdx` - translations, mirroring the English filenames
(e.g. `uk/`, `zh-cn/`, `pt-br/`, `fr/`); see `CONTRIBUTING.md` → Localization
(e.g. `uk/`, `zh-cn/`, `pt-br/`, `fr/`, `tr/`); see `CONTRIBUTING.md` → Localization
- `sidebar.config.json` - docs navigation structure for Starlight sidebar
- `CONTRIBUTING.md` - authoring guide for adding pages, sections, and translations
- `DEPLOYMENT.md` - release/manual packaging and sync trigger model
@@ -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
@@ -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
@@ -21,6 +21,64 @@ Certains prompts ont une partie visible (le message que vous verriez) et une par
Vous avez changé davis ? 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 sexé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 sexé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 longlet PR de la vue git | Vous générez le titre et le corps dune 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 sil existe. |
| Résolution de conflit merge/rebase | Le dialogue de conflits dans la vue git, quand un merge ou un rebase sarrête sur des conflits | Vous choisissez « Resolve in current session » ou « Resolve in new session ». Lagent lit les fichiers en conflit, propose une stratégie par fichier et attend votre confirmation avant de modifier, staging ou poursuivre lopération. |
| Résolution de conflit cherry-pick | La section « Re-integrate commits » dune session en worktree | Le déplacement des commits de la session vers la branche cible rencontre un conflit et vous le confiez à lagent. Lagent résout dans le worktree temporaire, stage les fichiers et poursuit le cherry-pick. |
### GitHub
| Prompt | Où il sexé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 dissue | Le dialogue de nouveau worktree, quand le worktree part dune issue | Le premier message de la nouvelle session relit lissue, 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 aujourdhui. 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 sexé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 quun passage direct à limplémentation. |
| Améliorer un plan | Laction « Improve » sur un plan enregistré dans la vue Plans | Vous envoyez un plan enregistré dans le flux damélioration. Lagent lit dabord 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 | Laction « Implement » sur un plan enregistré | Vous envoyez un plan enregistré dans le flux dimplémentation. Lagent lit le fichier du plan et limplémente de bout en bout sans élargir le périmètre, enregistrant les ajustements dans le fichier quand le plan lui-même savè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 dune nouvelle session.
| Prompt | Où il sexécute | Quand il se déclenche |
| --- | --- | --- |
| Tour du code | `/explore` | Vous demandez une vue densemble 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 à lagent de relire le diff actuel du workspace sous langle intention, correction et sécurité. |
| Planification de fonctionnalité | `/plan-feature` | Vous transformez une idée grossière de fonctionnalité en plan dimplé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 : lagent 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. Lagent compare deux ou trois approches et en recommande une. |
| Fusion | Laction « 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 nont 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 douverture de la session de relecture générée — avec le handoff quand il a été produit, sans sinon. |
| Retour de relecture / réponse dimplé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 limplé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 dexécution et fusion
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,40 @@
---
title: Agent Control Tool
description: Bir agent'ın OpenChamber oturumlarını, worktree'leri ve zamanlanmış görevleri sohbet içinden yönetmesine izin verin.
---
# Agent Control Tool
`openchamber` agent aracını kullanarak uygulamadaki işleri doğrudan sohbetten yönetin. OpenChamber kendi yerel OpenCode sunucusunu çalıştırdığında bu araç varsayılan olarak etkindir; kurmanız gereken ayrı bir araç ya da çalıştırmanız gereken bir shell komutu yoktur.
## Neler isteyebilirsiniz
Agent'a düz dille söyleyin. Örneğin:
- "Bu projede yeni bir OpenChamber oturumu oluştur, `openai/gpt-5.6-sol` modelini kullan ve bu istemi gönder: kimlik doğrulama akışını gözden geçir."
- "Bu görev için ayrı bir worktree içinde yeni bir OpenChamber oturumu oluştur ve giriş akışı için test eklemesini iste."
- "OpenChamber ile en son 10 oturumumu listele ve mevcut durumlarını da ekle."
- "Weekday review adlı bir OpenChamber zamanlanmış görevi oluştur, bunu her iş günü 09:00'da şu istemle gönder: son çalıştırmadan beri yapılan değişiklikleri gözden geçir."
- "Weekday review adlı OpenChamber zamanlanmış görevini şimdi çalıştır."
- "Authentication review adlı OpenChamber oturumunu kontrol et ve son agent yanıtını göster."
Araç projeleri ve model tercihlerini listeleyebilir, oturum oluşturup takip edebilir, bir oturumu fork'layabilir, izole worktree oturumları açabilir ve zamanlanmış görevleri yönetebilir. Bu şekilde başlatılan oturumlar OpenChamber'da diğer oturumlar gibi görünür; açıp işi kendiniz sürdürebilirsiniz.
## Akılda tutun
- Yeni oturum istemleri varsayılan olarak hemen döner. Oturumu OpenChamber'da takip edin ya da agent'tan sonra kontrol etmesini isteyin.
- Ayrı bir worktree yalnızca siz özellikle istediğinizde oluşturulur. Mevcut worktree'nizdeki kaydedilmemiş değişiklikler buna kopyalanmaz.
- Araç oturumları ya da worktree'leri silemez, proje yolları kaydedemez, keyfi shell komutları çalıştıramaz ya da keyfi URL'leri çağırmaz.
## Aracı açma ve kapatma
**Settings → General → OpenChamber Tools** bölümünü açın ve **Agent control tool** ayarını değiştirin. Yönetilen OpenCode sunucusu yeniden başladığında ayar geçerli olur. OpenChamber bunu **Apply & Restart** olarak sunar.
OpenChamber `OPENCODE_HOST` ya da skip-start ile harici bir OpenCode sunucusuna bağlandığında, ya da VS Code eklentisi içinde bu araç kullanılmaz. OpenChamber'ın yönetilen OpenCode sunucusunu kullanan masaüstü ve web kurulumlarında ise otomatik olarak desteklenir.
## İlgili
- [Scheduled Tasks](/scheduled-tasks/)
- [Worktree Sessions](/worktrees/)
- [Session Goals](/session-goals/)
- [Browser Panel](/desktop-browser/) — OpenChamber Web aracı, bir sayfaya bakmak ve onu kontrol etmek için
@@ -0,0 +1,39 @@
---
title: Komutlar ve Snippets
description: Sohbet için yeniden kullanılabilir slash komutları ve metin parçaları oluşturun.
---
# Komutlar ve Snippets
Komutlar ve snippets aynı şeyi yeniden yazmaktan kurtarır. Komutlar, `/` ile çalıştırdığınız tam istemlerdir; snippets ise bir mesaja `#` ile eklediğiniz metin parçalarıdır.
## Komutlar
Komut, `/review` gibi eğik çizgiyle çalıştırdığınız kayıtlı bir istemdir. Bunları **Settings → Commands** altında yönetirsiniz.
1. **Settings → Commands** bölümünü açın ve bir komut oluşturun.
2. Ona bir ad, açıklama ve göndermesi gereken istem metnini verin.
3. İsterseniz belirli bir agent'a ya da modele sabitleyin.
4. Kişisel ya da proje kapsamını seçin.
Sohbette, komutları açmak için mesajın **ilk** karakteri olarak `/` yazın ve ardından birini seçin. Metniniz şu yer tutucuları kullanabilir:
- `$ARGUMENTS` — komuttan sonra yazdığınız her şey
- `@filename` — bir dosyanın içeriğini ekler
- `` !`command` `` — bir shell komutunun çıktısını ekler
Yerleşik `init` ve `review` komutları sıfırlanabilir, ama silinemez.
## Snippets
Snippet, `#signoff` gibi bir etiketle satır içinde çağırdığınız yeniden kullanılabilir metindir. Bunları **Settings → Snippets** altında yönetirsiniz.
1. **Settings → Snippets** bölümünü açın ve bir snippet oluşturun.
2. Ona bir ad ve temsil ettiği metni verin. Birden fazla tetikleyici istiyorsanız takma adlar ekleyin.
3. Kişisel ya da proje kapsamını seçin.
Sohbette `#` yazın ve bir snippet seçin. OpenChamber gönderimden önce tam metni yerine yerleştirir.
## İlgili
- [Skills](/skills/) — daha büyük yönerge kümelerini gerektiğinde yükler
@@ -0,0 +1,68 @@
---
title: Bir Cihaz Bağlayın
description: Tek kullanımlık bir QR kodu ile telefonunuzu, masaüstünüzü veya başka bir tarayıcıyı OpenChamber sunucunuza eşleştirin.
---
# Bir Cihaz Bağlayın
Mobil uygulamayı, masaüstü uygulamasını ya da başka bir makinedeki tarayıcıyı, tek kullanımlık bir QR kodunu tarayarak OpenChamber sunucunuza eşleştirin. Cihaz bağlamak için önerilen yol budur; açılacak port yok, yazılacak adres yok.
## Cihaz eşleştirme
1. OpenChamber'ın çalıştığı makinede **Settings → Remote Instances → Connect to this server** bölümünü açın ve **Add a device** düğmesine basın.
2. Cihaza bir ad verin (ör. *My iPhone*), böylece sonra tanıyabilirsiniz.
3. Cihazı nerede kullanacağınızı seçin:
- **This computer only** — aynı makinede çalışan uygulamalar için
- **Home network only** — doğrudan Wi-Fi üzerinden bağlanır; bu ağın dışındayken çalışmaz
- **Anywhere** — evde de dışarıda da çalışır; dışarıdaki trafik, kuruluma gerek olmadan uçtan uca şifreli bir tünel olan [Private Relay](/private-relay/) üzerinden gider
4. **Create QR code** düğmesine basın.
5. Diğer cihazda kodu tarayın:
- **mobile app** — bağlanma ekranında (ya da instances listesinden) **Scan QR code** düğmesine dokunun
- **desktop app** — bunun yerine bağlantı bağlantısını kopyalayın ve **Settings → Remote Instances → Other OpenChamber servers → Import Link** içine yapıştırın
İletişim kurulduğu anda iletişim kutusu kendiliğinden kapanır ve cihaz canlı durumuyla listede görünür. Hepsi bu kadar, eşleştirme tamam.
## Eşleştirme nasıl güvenli kalır
- **QR kodu tek kullanımlıktır.** Bir cihaz onu kullandığı anda çalışmayı bırakır ve hiç kullanılmazsa kendiliğinden süresi dolar.
- **Her cihaza kendi token'ı verilir.** Bir kodu taramak UI parolanızı açığa çıkarmaz ve bir cihazın token'ı başka bir cihazı taklit etmek için kullanılamaz.
- **Kontrol sizdedir.** Eşleştirilen her cihaz adı, platformu ve bağlantı durumu ile listelenir. İstediğiniz zaman bunlardan herhangi birini iptal edebilirsiniz.
- **Ev dışı trafik uçtan uca şifrelenir.** **Anywhere** ile ağınız dışındaki trafik [Private Relay](/private-relay/) üzerinden akar ve üzerinden geçen şeyi okuyamaz.
## Eşleştirilmiş cihazları yönetme
**Settings → Remote Instances → Connect to this server** bu sunucuya ulaşabilen tüm cihazları listeler. Cihaz çevrimiçiyse yeşil nokta görünür; yerel ağ üzerinden mi yoksa relay üzerinden mi bağlı olduğu da belirtilir.
- **Revoke** bir cihazın bağlantısını hemen keser. Fikriniz değişirse yeni bir QR koduyla yeniden eşleştirin.
- **Clear revoked** listeyi temizler.
Aynı fiziksel cihaz daha sonra yeniden giriş yapsa bile tek bir kayıt olarak kalır. Kopya kayıt birikmez.
## Komut satırından bağlanma
Sunucu başsız çalışıyorsa, yani açık bir UI yoksa, o makinedeki bir terminalden bağlantı bağlantısı oluşturun.
Aynı ağdaki bir cihaz için:
```bash
openchamber connect-url --port 3000 --qr
```
Her yerden bağlanması gereken bir cihaz için. Diyalogda **Anywhere** seçmeye denktir:
```bash
openchamber connect-url --relay --qr
```
`--relay` bağlantısı, diyalogdaki gibi iki yolu da taşır. Cihaz sunucuya erişebildiğinde yerel ağ üzerinden doğrudan bağlanır ve dışarıdayken [Private Relay](/private-relay/) ile devam eder. Relay kendi kendine başlar. Çalışan bir örnek bağlantıyı bir dakika içinde alır, durdurulmuş bir örnek ise sonraki açılışında alır.
> Doğrudan yol yalnızca sunucu gerçekten ağınızda dinliyorsa çalışır. Varsayılan olarak OpenChamber yalnızca makinenin kendisinde dinler. Wi-Fi üzerinden erişilebilir olması için `--lan` ile başlatın. Komut, bağlantının doğrudan yolunun başka cihazlardan kullanılamayacağını anlarsa sizi (`[LAN_UNREACHABLE]`) uyarır. Bu durumda `--relay` bağlantısı yine çalışır, ama her zaman relay üzerinden gider.
Yazdırılan bağlantı ve QR kodu, ayar iletişim kutusundakilerle tam aynıdır. Tek kullanımlık, süresi dolan ve iptal edilebilir.
## İlgili
- [Private Relay](/private-relay/) — "Anywhere" bağlantıları nasıl çalışır ve relay neleri görebilir, neleri göremez
- [Mobile Apps](/mobile/) — iOS ya da Android uygulamasını yükleyin
- [Remote Instances](/remote-instances/) — masaüstü uygulamasını SSH ya da bağlantılar üzerinden sunuculara bağlayın
- [Remote access](/troubleshooting/remote-access/) — bir cihaz bağlanmadığında
+38
View File
@@ -0,0 +1,38 @@
---
title: Context
description: Bir oturumun model belleğinin ne kadarını kullandığını görün.
---
# Context
Her model bir konuşmanın ancak belirli bir kısmını aynı anda tutabilir. Buna context denir. OpenChamber bunun ne kadar dolduğunu gösterir. Böylece bir oturum sınırına yaklaşırken bunu fark eder ve yanıtta eski ayrıntıların düşmeye başlayabileceğini anlarsınız.
## Hızlı gösterge
Sohbet ederken küçük bir gösterge, kullanılan context yüzdesini gösterir. Doluluk arttıkça renk değişir:
- yeşil — bolca yer var
- sarı — doluyor (yaklaşık dörtte üç)
- kırmızı — neredeyse dolu
Tam token sayısını görmek için üzerine gelin ya da mobilde dokunun.
## Tam context paneli
Geçerli oturumun daha geniş görünümü için sağ kenar çubuğundaki **Context** sekmesini açın:
- kullanılan model ve oturumun ne zaman başladığı
- model sınırına karşı toplam token sayısı
- mesaj ve maliyet toplamları
- son yanıtın token dökümü
- context'i neyin kullandığına dair kabaca bir dağılım, sizin mesajlarınız, agent'ın mesajları, araç çıktısı
Bu döküm bir tahmindir, kesin sayı değildir. Faturayı kontrol etmek için değil, pencereyi neyin doldurduğunu görmek için kullanın.
## Dolu olduğunda ne yapmalı
Bir oturumun sonsuza kadar büyümesine izin vermek yerine yeni bir görev için yeni bir oturum açın. Daha kısa context daha hızlıdır ve modelin odağını korur.
## İlgili
- [Projects](/projects/) — oturumlar proje bazında gruplanır
@@ -0,0 +1,59 @@
---
title: Tarayıcı Paneli
description: Uygulama içinde herhangi bir sayfayı açın, üzerine not alın ve agent'ın onu kontrol etmesine izin verin.
---
# Tarayıcı Paneli
Tarayıcı paneli, herhangi bir sayfayı sohbetinizin hemen yanına açar. Uygulama başlığındaki dünya düğmesinden açın.
Masaüstü uygulamasında bu gerçek bir tarayıcıdır. Oturumlarınız kalıcıdır, hot reload çalışır ve geliştirici araçları tek tık uzağındadır. Bir web tarayıcı sekmesinde panel yine de bir sayfa gösterebilir, ama içine bakamaz. Aşağıdaki açıklama araçları yalnızca masaüstünde çalışır. VS Code eklentisinde ise hiç tarayıcı paneli yoktur. Çünkü VS Code zaten yanında bir tarayıcı bulunan bir editördür ve bu paneli değerli yapan şeylerin hepsi masaüstü uygulamasını ister.
Burada açılan sayfalar kameranızı, mikrofonunuzu ya da konumunuzu kullanamaz. Bu istekler reddedilir.
## Araç çubuğu
Adres çubuğu, bu projede daha önce açtığınız sayfaları hatırlar ve siz yazdıkça bunları önerir. Adresin bir kısmıyla ya da sayfa başlığıyla eşleşir. Ok tuşları listede gezinir, Enter vurgulu girdiyi açar ve satırdaki düğme onu listeden siler.
**Reload** düğmesi onun yanındadır. Yanında, bir değişiklik görünmeyi reddettiğinde önbelleği umursamayan bir **hard reload** ve yalnızca sayfayı ölçekleyen yakınlaştırma denetimleri vardır.
**Clear cookies** ve **Clear cached data** yalnızca bu panele uygulanır. OpenChamber oturumunuza ve diğer pencerelere dokunulmaz.
## Bir sayfaya not ekleme
**Annotate** düğmesine basın. Sayfanın üstünde üç araç içeren bir çubuk görünür:
- **Element** — bir öğeye tıklayın. Başka bir öğeye tıklamak seçimi değiştirir. Aynı öğeye tekrar tıklamak seçimi temizler.
- **Region** — bir alanın etrafına kutu çizin. Birden çok öğeyi kapsadığında bunu kullanın.
- **Draw** — sayfanın üzerine serbestçe çizim yapın.
İşaretinizin yanındaki kutuya ne istediğinizi yazın ve **Attach** düğmesine basın. Ya da sadece Enter'a basın. Sohbet mesajınıza, işaretlediğiniz her şeyi, notunuzu ve işaretlerinizin çizildiği görünür sayfanın bir ekran görüntüsünü içeren bir kart eklenir. Böylece konumu tarif etmek yerine "şu düğme, biraz daha yuvarlak" diyebilirsiniz.
Sayfanın kendisi asla değiştirilmez. Not eklemek yalnızca orada olanı işaretler. `Esc` işlemi iptal eder ve araç çubuğunu kapatır.
## Agent'ın kullanmasına izin verin
Agent, tarayıcı panelinin kendisini kullanabilir. Bir sayfa açar, üzerindeki metni okur, tıklar, yazar, kaydırır ve mobile, tablet, desktop düzenleri arasında geçiş yapar. Böylece sizden istemek yerine kendi işini kontrol eder. Bunu panelde olurken görürsünüz.
Agent, sayfanın içinde keyfi kod çalıştıramaz. Tarayıcı gerçek oturumlarınızı tuttuğu için yukarıdaki belirli eylemlerle sınırlıdır.
Ayrıca baktığı şeyin bir resmini proje içindeki `.openchamber/screenshots/` klasörüne kaydedip yanıtında size gösterebilir. Öncesi ve sonrası karşılaştırmasını mümkün kılan şey budur ve dosya sonrasında bir pull request'e eklemek için orada kalır.
Tarayıcı eylemleri **OpenChamber Web tool**'dur. Bunu **Settings → General → OpenChamber Tools** altında ayrı ayrı açıp kapatabilirsiniz.
Bu masaüstü uygulamasını gerektirir. Bir web tarayıcı sekmesinde gösterilen sayfa kontrol edilemez.
## Boyut ve görünüm
Cihaz çubuğunu açmak için telefon düğmesine basın. Bir hazır ayar seçin ya da genişlik ve yükseklik girin. Sayfa o boyutta yerleşir. Panelden büyükse sığacak şekilde küçültülür, ama yine de sizden istediğiniz boyutta ölçüm yapar.
Aynı çubuk sayfayı açık ya da koyu temaya zorlar. Böylece makinenizde hiçbir şeyi değiştirmeden bir tema kontrol edilebilir. DevTools'a dokunmaz. Bir sayfanın yalnızca bir debugger bağlantısı olabilir, bu yüzden açıkse önce DevTools'u kapatın.
## Geliştirici araçları
Sayfa için Chromium'un kendi geliştirici araçlarını açmak üzere araç çubuğundaki terminal düğmesine basın. Console, network, elements, beklediğiniz her şey oradadır.
## İlgili
- [Preview & Dev Servers](/preview/) — çalışan uygulamanızı, uzak bir makinedekiler dahil, açma
- [Agent Control Tool](/agent-control-tool/) — sohbetten oturumlar, worktree'ler ve zamanlanmış görevler
@@ -0,0 +1,41 @@
---
title: Masaüstü Tünelleri
description: Masaüstü uygulamasından Cloudflare veya Ngrok tünelleri oluşturun.
---
# Masaüstü Tünelleri
Masaüstü uygulaması, **Settings → OpenChamber → Tunnel** bölümünden herkese açık bir tünel oluşturabilir. Bu yol için OpenChamber'ı CLI'dan başlatmanız gerekmez.
## Sağlayıcı kurun
OpenChamber sağlayıcının CLI aracını makinenizde başlatır. Önce kullanmak istediğiniz sağlayıcıyı kurun:
```bash
brew install cloudflared
brew install ngrok
```
Cloudflare `cloudflared` kullanır. Ngrok için bir ngrok hesabı ve ngrok panelinden alınmış bir authtoken gerekir:
```bash
ngrok config add-authtoken <your-ngrok-token>
```
## Uygulamadan başlatma
1. **Settings → OpenChamber → Tunnel** bölümünü açın.
2. **Cloudflare** ya da **Ngrok** seçin.
3. Hızlı bir tünel başlatın.
4. Oluşturulan QR kodunu telefonunuzdan tarayın.
Ngrok şu anda hızlı tünelleri destekler. Cloudflare ise hızlı tünelleri ve yönetilen Cloudflare modlarını destekler.
## Erişim koruması
Sağlayıcı URL'si herkese açık olsa bile, OpenChamber erişimi kendi connect token'ı ile korur. Oluşturulan bağlantı bir kerelik token içerir, bir TTL'ye sahiptir ve eski kullanılmamış bağlantılar yeni bir bağlantı oluşturduğunuzda ya da tüneli durdurup yeniden başlattığınızda iptal edilir.
## İlgili
- [Tunnels](/tunnels/) — CLI tünel kullanımı ve yönetilen Cloudflare modları
- [PWA & Mobile](/mobile/) — OpenChamber'a telefonunuzdan erişin
@@ -0,0 +1,138 @@
---
title: Environment Değişkenleri
description: Environment değişkenleriyle OpenChamber ve OpenCode entegrasyonunu yapılandırın.
---
# Environment Değişkenleri
OpenChamber bu environment değişkenlerini başlangıçta okur. Başlangıç servisleri için `openchamber startup enable` varsayılan olarak mevcut environment'ı anlık görüntü olarak kaydeder. Servisin kullanmasını istediğiniz değişkenleri değiştirdikten sonra bunu yeniden çalıştırın.
## OpenChamber sunucusu
### `OPENCHAMBER_HOST`
OpenChamber web sunucusu için bind adresi. Diğer makinelerden erişime izin vermek için `0.0.0.0` kullanın.
### `OPENCHAMBER_UI_PASSWORD`
Tarayıcı arayüzü için parola. Bunu localhost dışına bind ederken, tünel kullanırken ya da reverse proxy arkasında çalıştırırken kullanın.
### `OPENCHAMBER_API_ONLY`
`true` ya da `1` olarak ayarlandığında OpenChamber'ı headless modda başlatır. API rotaları masaüstü ve mobil istemciler için açık kalır, ama tarayıcı arayüzü sunulmaz.
### `OPENCHAMBER_DATA_DIR`
OpenChamber veri dizinini geçersiz kılar. Varsayılan `~/.config/openchamber`.
### `OPENCHAMBER_COMPRESS_API`
API yanıt sıkıştırmasını kontrol eder. `true` ya da `1` ile zorla açın, `false` ya da `0` ile zorla kapatın.
### `OPENCHAMBER_SKIP_API_COMPRESSION`
`true` ya da `1` olarak ayarlandığında API yanıt sıkıştırmasını kapatır. Bu ayar `OPENCHAMBER_COMPRESS_API`'ye göre önceliklidir.
### `OPENCHAMBER_VERBOSE_REQUEST_LOGS`
`true` ya da `1` olarak ayarlandığında ayrıntılı HTTP istek günlüklerini açar.
### `OPENCHAMBER_UPDATE_API_URL`
Güncelleme kontrolü API uç noktasını geçersiz kılar. Çoğu kullanıcı bunu boş bırakmalıdır.
### `OPENCHAMBER_PACKAGE_MANAGER`
Otomatik algılama yanlışsa, güncelleme işlemlerinin kullandığı package manager'ı zorlar.
## OpenCode sunucusu
### `OPENCODE_HOST`
OpenChamber'ı mevcut bir OpenCode sunucusuna bağlar. Değer, açık bir port içeren ve path, query ya da hash içermeyen bir `http` ya da `https` origin'i olmalıdır. `OPENCODE_HOST`, `OPENCODE_PORT`'a göre önceliklidir.
### `OPENCODE_PORT`
OpenCode sunucu portunu ayarlar. Yönetilen OpenCode ile bu, yönetilen portu ister. `OPENCODE_SKIP_START=true` ile harici bir sunucuya o port üzerinden bağlanır.
### `OPENCODE_SKIP_START`
`true` olarak ayarlandığında OpenChamber'ın kendi OpenCode sunucusunu başlatmasını engeller.
### `OPENCHAMBER_OPENCODE_HOSTNAME`
OpenChamber'ın yönettiği OpenCode sunucusu için bind hostname'i. Varsayılan `127.0.0.1`.
### `OPENCODE_BINARY`
OpenChamber'ın çalıştırması gereken `opencode` yürütülebilir dosyasının yolu.
### `OPENCODE_CONFIG`
Belirli bir OpenCode yapılandırma dosyasının yolu.
### `OPENCODE_CONFIG_DIR`
Agent'lar, skills, snippets ve config keşfi için belirli bir OpenCode yapılandırma dizininin yolu.
### `OPENCODE_DATA_DIR`
Yönetilen OpenCode sunucusu için özel veri dizini.
### `OPENCODE_WSL_DISTRO`
Windows'ta OpenCode entegrasyonu için kullanılan WSL dağıtımını seçer.
### `OPENCHAMBER_OPENCODE_WSL_DISTRO`
WSL dağıtımını seçmek için OpenChamber'a özel takma ad. İkisi de ayarlıysa `OPENCODE_WSL_DISTRO` önceliklidir.
### `OPENCODE_JWT_SECRET`
UI kimlik doğrulama token'larını imzalamak için kullanılan gizli değer. Kalıcı servis dağıtımlarında uzun ve rastgele bir değer kullanın.
## Terminal ve Git
### `OPENCHAMBER_TERMINAL_SHELL`
OpenChamber terminal oturumları için kullanılan shell yürütülebilir dosyası.
### `OPENCHAMBER_GIT_BINARY`
OpenChamber Git özelliklerinin kullandığı Git yürütülebilir dosyası.
### `GIT_BINARY`
Alternatif Git yürütülebilir dosyası geçersiz kılma değeri. OpenChamber'a özel yapılandırma için `OPENCHAMBER_GIT_BINARY` tercih edin.
### `OPENCHAMBER_GIT_READ_CACHE_TTL_MS`
Git destekli dosya okumaları için önbelleğin milisaniye cinsinden yaşam süresi. Hata ayıklarken bu önbelleği kapatmak için `0` verin.
## Voice ve tunnels
### `OPENAI_API_KEY`
OpenAI uyumlu servisleri çağıran OpenChamber voice özellikleri tarafından kullanılan API anahtarı.
### `OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS`
`true` ya da `1` olarak ayarlandığında voice özellikleri için uzak OpenAI uyumlu base URL'lere izin verir.
### `NGROK_AUTHTOKEN`
OpenChamber tunnel komutlarının kullandığı ngrok auth token. `ngrok config add-authtoken <token>` ile de yapılandırabilirsiniz.
## Runtime yardımcıları
### `BUN_BINARY`
Daemon süreçleri başlatılırken OpenChamber'ın kullanması gereken Bun yürütülebilir dosyası.
### `BUN_INSTALL`
Bun kurulum kökü. OpenChamber bunu daemon başlangıcı ve güncellemeleri için `bin/bun` bulmakta kullanır.
### `VITE_OPENCODE_URL`
Vite ile derlenen web uygulaması için derleme zamanı API base URL'i. Çoğu kullanıcı bunu normal CLI ya da masaüstü kullanımında ayarlamamalıdır.
@@ -0,0 +1,30 @@
---
title: Git Kimlikleri
description: Her repo için doğru ad ve e-posta ile commit edin.
---
# Git Kimlikleri
Bir git kimliği, commit'lerinizin hangi ad ve e-posta ile imzalandığıdır. Kişisel ve iş repoları arasında çalışıyorsanız, tek bir global ayara güvenmek yerine kimlikleri kaydedip her repo için doğru olanı uygulayabilirsiniz. Bunları **Settings → Git** bölümünden yönetin.
## Bir kimlik ekleyin
1. **Settings → Git** bölümünü açın ve **New** seçin.
2. Commit edeceğiniz **name** ve **email** bilgilerini girin.
3. Uzak depo ile nasıl doğrulanacağını seçin:
- **SSH** — bir SSH anahtarına yönlendirin
- **token** — bir host için kayıtlı bir kimlik bilgisi kullanın
4. İsterseniz kolay seçmek için bir renk ve simge verin.
Sisteminizin global kimliği de salt okunur olarak gösterilir.
## Bir kimliği bir repo'ya uygulayın
Bir kimliği uygulamak, onu o repo'nun **local** git config'ine yazar. Yalnızca o repo'yu etkiler, global ayarınızı etkilemez. SSH kimlikleri ayrıca kullanılacak SSH komutunu anahtarınızı kullanacak şekilde ayarlar. token kimlikleri de host için kimlik bilgisi depolamasını kurar.
OpenChamber'ın mevcut git kimlik bilgilerinizden keşfettiği kimlikleri içe aktarabilir ve token kimlikleri olarak kaydedebilirsiniz.
## İlgili
- [Git & GitHub Workflows](/git/) — ayarladığınız kimlikle commit edin
- [GitHub Issues & PRs](/github/) — PR'ler için bir GitHub hesabı bağlayın
+41
View File
@@ -0,0 +1,41 @@
---
title: Git ve GitHub İş Akışları
description: OpenChamber'dan çıkmadan branch'leri hazırlayın, commit edin ve yönetin.
---
# Git ve GitHub İş Akışları
OpenChamber'da yerleşik bir git görünümü vardır. Böylece terminale geçmeden değişiklikleri gözden geçirebilir, commit edebilir ve branch'leri yönetebilirsiniz. Sağ kenar çubuğundaki **Git** sekmesinden açın.
## İnceleme ve commit
Git görünümü değişikliklerinizi **staged** ve **unstaged** olarak ayırır:
- bir dosyayı stage etmek için yanındaki **+** simgesine, stage'den çıkarmak için **** simgesine tıklayın
- bir gruptaki her şeyi tek seferde stage edin ya da stage'den çıkarın
- diff'ini görmek için bir dosyaya tıklayın
Sonra bir commit mesajı yazıp commit edin. OpenChamber'a staged değişikliklerinizden bir commit mesajı **generate** ettirebilirsiniz. Mevcut oturumun modelini kullanır, bu yüzden açık bir oturum gerekir.
## Branch'ler ve geçmiş
Git görünümü günlük git işlerinin geri kalanını da kapsar:
- branch oluşturma, değiştirme, yeniden adlandırma ve silme
- push, pull ve fetch
- geçmişi ve commit başına diff'leri gezme
- değişiklikleri stash etme ve geri yükleme
## Pull request'ler
GitHub'ı bağlayın. Bkz. [GitHub Issues & PRs](/github/). Sonra **PR** sekmesiyle pull request açabilir, güncelleyebilir, hazır diye işaretleyebilir ya da merge edebilirsiniz. Başlık ve açıklamasını da commit mesajlarında olduğu gibi oluşturur.
## Çakışmaları içeri alma
Bir merge, rebase ya da integrate çakışmaya takılırsa, OpenChamber neyin sıkıştığını gösterir ve bunu çözmenize izin verir. Bunu agent'a devretmek de dahil.
## İlgili
- [GitHub Issues & PRs](/github/) — GitHub'ı bağlayın ve işlere issue'lardan başlayın
- [Worktree Sessions](/worktrees/) — bir branch'i kendi klasöründe izole edin
- [Git Identities](/git-identities/) — her repo için doğru kişi olarak commit edin
+34
View File
@@ -0,0 +1,34 @@
---
title: GitHub Issues & PRs
description: GitHub'ı bağlayın ve issue'lar ile pull request'lerden oturum başlatın.
---
# GitHub Issues & PRs
GitHub hesabınızı bağlayın. OpenChamber issue'ları ve pull request'leri içeri çekebilir, birinden doğrudan oturum başlatabilir ve sizin yerinize PR açıp güncelleyebilir.
## GitHub'ı bağlayın
1. **Settings → Git** bölümünü açın.
2. GitHub altında **Connect** seçin. OpenChamber bir bağlantı ve kısa bir kod gösterir.
3. Bağlantıyı açın, kodu girin ve onaylayın.
Bağlantı kurulunca hesabınız GitHub bölümünde görünür. Birden fazla hesap bağlayabilir, aralarında geçiş yapabilir ya da istediğiniz zaman bağlantıyı kesebilirsiniz.
## Bir issue veya PR'den işe başlayın
GitHub bağlıyken bir [worktree session](/worktrees/) oluşturduğunuzda, **Start from GitHub issue/PR** seçebilirsiniz:
- bir **issue** seçin. OpenChamber branch'i onun adına göre adlandırır ve oturumu, issue ve yorumları ilk mesaj olarak açar
- bir **pull request** seçin. PR'nin branch'ini checkout eder. İsterseniz PR'nin diff'ini de ekleyebilirsiniz, böylece agent değişikliğin tamamını görür
Bu sizi, bağlam zaten yüklenmiş halde doğrudan bir oturuma sokar.
## Pull request açma ve yönetme
[git view](/git/) içindeki **PR** sekmesinden pull request oluşturabilir, güncelleyebilir, taslağı hazır diye işaretleyebilir ya da merge edebilirsiniz. OpenChamber PR başlığını ve açıklamasını değişikliklerinizden oluşturabilir.
## İlgili
- [Git & GitHub Workflows](/git/) — branch'leri commit edin ve yönetin
- [Worktree Sessions](/worktrees/) — issue ve PR oturumlarının başladığı yer
+32
View File
@@ -0,0 +1,32 @@
---
title: OpenChamber Belgeleri
description: OpenChamber için web, masaüstü ve VS Code genel kurulum ve kullanım kılavuzu.
---
# OpenChamber Belgeleri
OpenChamber, terminalinizde çalışan AI kodlama agent'ı OpenCode'un etrafındaki görsel çalışma alanıdır. Komut satırında yaşamaktansa, o işi izleyip yönlendireceğiniz bir ekran verir.
Bu dokümanları şunlar için kullanın:
- çalışma biçiminize uygun uygulamayı kurmak
- güvenli uzaktan kullanım için OpenChamber'ı açmak
- görünümü özelleştirmek ve yaygın sorunları gidermek
## Önce bunu okuyun
- [Install](/install/)
- [Quickstart](/quickstart/)
- [Tunnels](/tunnels/)
- [Troubleshooting](/troubleshooting/)
## Keşfedin
- [Projects](/projects/) ve [Worktree Sessions](/worktrees/) — işinizi düzenleyin ve izole edin
- [Providers, Models & Agents](/providers/) — OpenCode'u bağlayın ve model seçin
- [Git & GitHub Workflows](/git/) — commit edin, inceleyin ve PR açın
- [Security](/security/) ve [Tunnels](/tunnels/) — örneğinizi koruyun ve ona ulaşın
## OpenChamber ne için
OpenChamber, AI kodlamanın bir kontrol odasından fayda gören kısımları içindir. Branch'lenmiş oturumlar, diff inceleme, terminal yönetimi, araç ilerlemesini izleme, project action'ları çalıştırma ve agent çalışırken tüm tahtayı görünür tutma.
+33
View File
@@ -0,0 +1,33 @@
---
title: Kurulum
description: OpenChamber'ı masaüstü, web ya da VS Code için kurun.
---
# Kurulum
OpenChamber'ı çalıştırmanın üç yolu vardır:
- macOS için masaüstü uygulaması
- CLI tarafından barındırılan web uygulaması, bunu telefon uygulaması gibi yükleyebilirsiniz, yani PWA
- VS Code eklentisi
## Gereksinim
Önce [OpenCode](https://opencode.ai) kurun. OpenChamber onun üzerinde çalışır.
## Web + PWA
```bash
curl -fsSL https://raw.githubusercontent.com/openchamber/openchamber/main/scripts/install.sh | bash
openchamber --ui-password be-creative-here
```
CLI'nın yazdırdığı URL'yi açın. Genellikle `http://localhost:3000` olur. OpenChamber oturum listesini görmelisiniz. El altında tutmak için tarayıcınızın adres çubuğundaki "Install" seçeneğini kullanıp uygulama olarak ekleyin.
## Masaüstü
En son masaüstü sürümünü GitHub releases sayfasından ya da OpenChamber indirme sayfasından indirin. Açın ve alışık olduğunuz OpenCode akışına giriş yapın.
## VS Code
VS Code Marketplace'ten yükleyin ve alışık olduğunuz OpenCode akışına giriş yapın. Sonra OpenChamber görünümü kenar çubuğunda açılır.
@@ -0,0 +1,54 @@
---
title: Entegrasyonlar
description: Claude ya da Cursor aboneliğinizi bir provider olarak kullanın.
---
# Entegrasyonlar
Integration, sahip olduğunuz bir aboneliği kullanarak OpenChamber'a bir provider ekleyen küçük bir eklentidir. Bunları **Settings → Integrations** altında yönetirsiniz.
> **Experimental feature.** Provider politikalarına uymaya çalışıyoruz, ama hesap kısıtlamaları ve askıya alma kararları yine her provider'ın kendi kararıdır. Integrations'ı kendi sorumluluğunuzda kullanın.
Kullanılabilir integrations:
- **Claude Code** — Claude Pro ya da Max planınız, API anahtarı gerekmez
- **Cursor** — Cursor planınızın model limitleri
## Bir entegrasyon kurun
1. **Settings → Integrations** bölümünü açın.
2. Integration'ı bulun ve **Install** seçin.
3. İstenince OpenCode'u yeniden başlatın. Provider yeniden başlatmadan sonra görünür.
4. **Set up** seçin ve giriş yapın. Modeller sonra sohbet model seçicisinde görünür.
Integration'lar kullanıcı düzeyinde kurulur, bu yüzden her projede çalışır. Aynı karttan istediğiniz zaman güncelleyebilir ya da kaldırabilirsiniz.
## Claude Code
Claude Code, Claude Pro ya da Max planınızı kullanır. API anahtarı yoktur ve ayrı bir Claude uygulaması gerekmez.
1. Integration'ı kurun. Yukarıya bakın.
2. **Set up** seçin ve giriş yapın. Henüz Claude Code CLI'nız yoksa kurulum önce onu yüklemeyi önerir, sonra giriş yaptırır.
Claude Code, burada provider CLI'sının kurulu ve giriş yapılmış olmasını gerektiren tek integration'dır. Cursor CLI gerektirmez.
**Claude hesabınız nasıl güvende kalır:** Bu integration, Anthropic'in resmi Claude Agent SDK'sını ve kurulu Claude Code CLI'nızı kullanır. OAuth'u kaçırmaz, tarayıcı token'larını çıkarmaz ya da yeniden oynatmaz, desteklenmeyen bir istemciyi taklit etmez ve Anthropic'in kimlik doğrulama akışını atlamaz. Anthropic'in desteklediği erişim yolunda kalır. Bu yüzden token kaçırma ya da yetkisiz kimlik doğrulama kestirmelerinin hesap banı riskini taşımaz.
## Cursor
Cursor, Cursor planınıza dahil modelleri OpenChamber'da kullanılabilir hale getirir.
1. Integration'ı kurun. Yukarıya bakın.
2. **Set up** seçin, bağlantıyı açın ve tarayıcınızda erişimi onaylayın. API anahtarı gerekmez. Giriş yaptıktan sonra model listesi otomatik yüklenir.
## Güncelleme ya da kaldırma
- **Update** eklentinin en son yayımlanmış sürümünü kurar.
- **Remove** eklentiyi OpenCode config'inizden siler. OpenCode yenilendikten sonra provider yüklenmeyi bırakır.
Bir kartta girdilerin elle yönetilmesi gerektiği yazıyorsa, **Manage plugins** seçin ve kopyaları orada temizleyin.
## İlgili
- [Providers, Models & Agents](/providers/) — diğer provider'ları bağlayın ve model seçin
- [Usage & Quotas](/usage/) — ne kadar kullandığınızı izleyin
@@ -0,0 +1,84 @@
---
title: Sihirli İstemler
description: OpenChamber'ın otomatik akışlarının arkasındaki yerleşik istemleri özelleştirin.
---
# Sihirli İstemler
OpenChamber bir şeyi otomatik yaptığında arka planda yerleşik istemler kullanır. Commit mesajı yazmak, PR taslağı hazırlamak, issue incelemek, çakışmayı çözmek, bir oturumu özetlemek gibi. Magic Prompts, bu istemleri okuduğunuz ve yeniden yazdığınız yerdir. **Settings → Magic Prompts** bölümünden açın.
Normal kullanım için bu sayfaya ihtiyacınız yok. Bir akışın farklı davranmasını istediğinizde buraya bakın. Mesela commit mesajlarının belirli bir stilde olmasını istiyorsanız.
## Bir istemi düzenleme
1. **Settings → Magic Prompts** bölümünü açın.
2. Kenar çubuğundaki gruplardan bir istem seçin. Git, GitHub, Planning ve Session.
3. Metni düzenleyin ve kaydedin.
Bazı istemlerin görünen bir kısmı vardır. Bu, sizin göreceğiniz mesajdır. Bir de agent için gizli yönerge kısmı vardır. İstemler `{{placeholders}}` içerebilir. OpenChamber bunları doldurur. Mesela diff ya da issue başlığı gibi. Bunları olduğu gibi bırakın.
## Sıfırlama
Fikrinizi mi değiştirdiniz? Her istemde **reset to default** vardır. Her şeyi baştan başlatmak isterseniz bir de **reset all** bulunur.
## Her istem nerede kullanılır
Aşağıdaki her istem, nerede çalıştığını ve ne zaman tetiklendiğini listeler. Düzenlemeden önce tetikleyiciyi kontrol edin. Böylece hangi akışı değiştirdiğinizi bilirsiniz.
### Git
| Prompt | Where it runs | When it fires |
| --- | --- | --- |
| Commit generation | Git görünümündeki commit kutusunun generate düğmesi ve mobil Changes ekranı | Bir commit mesajı üretirsiniz. Seçili dosyalar ve branch'in son commit subject'leri doldurulur, böylece subject repo'nuzun mevcut stiline uyar. |
| PR generation | Git görünümündeki PR sekmesindeki create-pull-request formu | Bir PR başlığı ve gövdesi üretirsiniz. Base ve head branch'ler, aralarındaki commit'ler ve değişen dosyalar, ek bağlamınız ve varsa repo'nun PR template'i ile doldurulur. |
| Merge/rebase conflict resolution | Git görünümündeki conflicts iletişim kutusu, bir merge ya da rebase çakışmada durduğunda | "Resolve in current session" ya da "Resolve in new session" seçersiniz. Agent çakışan dosyaları okur, dosya başına bir çözüm stratejisi önerir ve düzenleme, stage etme ya da işlemi sürdürmeden önce onayınızı bekler. |
| Cherry-pick conflict resolution | Bir worktree oturumu için "Re-integrate commits" bölümü | Oturumun commit'lerini hedef branch'e taşırken bir çakışma olur ve bunu agent'a verirsiniz. Agent geçici worktree içinde çözer, düzeltilen dosyaları stage eder ve cherry-pick'e devam eder. |
### GitHub
| Prompt | Where it runs | When it fires |
| --- | --- | --- |
| PR review | Composer'daki attach menüsünün "Link GitHub PR" seçicisi ve yeni worktree iletişim kutusu | İki tetikleyici vardır. Bir PR'ı bağlam olarak eklediğinizde yönergeler görünür ve sonraki mesajınızla birlikte gider. Bir PR'dan worktree oturumu başlatmak, prompt'u o oturumun açılış mesajı yapar ve tam PR bağlamı eklenir. |
| Issue review | Worktree'yi bir issue'dan başlattığınızda yeni worktree iletişim kutusu | Yeni oturumun açılış mesajı issue'yu inceler, body'si ve comments'i bağlam olarak eklenir. |
| PR failed checks / PR comments / single PR comment | — | Bugün hiçbir akış tarafından gönderilmez. PR görünümü bunları eskiden tek tıkla review eylemlerinden tetiklerdi. Failed checks ve comments şimdi bunun yerine chat-context draft'ı olarak sabitleniyor. Düzenlenebilir kalırlar, böylece mevcut overrides çalışmaya devam eder. |
### Planning
| Prompt | Where it runs | When it fires |
| --- | --- | --- |
| Todo planning | Proje kenar çubuğundaki Todos paneli | Bir todo'yu bir oturuma ya da yeni bir worktree oturumuna gönderirsiniz. Todo metni görünen mesaj olur. Yönergeler onu doğrudan uygulamaya geçmek yerine soru-cevap odaklı bir planlama diyaloğuna dönüştürür. |
| Improve plan | Plans görünümündeki kayıtlı bir plan üzerinde "Improve" eylemi | Kayıtlı bir planı improve akışına gönderirsiniz. Agent önce plan dosyasını okur, sonra geçerli repo durumuna dayalı değişiklikler önerir ve aynı dosyayı düzenlemeyi teklif eder. |
| Implement plan | Kayıtlı bir plan üzerindeki "Implement" eylemi | Kayıtlı bir planı implement akışına gönderirsiniz. Agent plan dosyasını okur ve kapsamı büyütmeden baştan sona uygular. Planın kendisi yanlış çıkarsa plan düzeltmelerini dosyaya geri kaydeder. |
### Session
Bunların çoğu composer'a yazılan slash komutlarını güçlendirir. Çoğu ayrıca yeni oturum taslağında başlangıç kartı olarak görünür.
| Prompt | Where it runs | When it fires |
| --- | --- | --- |
| Codebase tour | `/explore` | Kod tabanına üst düzey bir giriş istediğinizde. |
| Session summary | `/summary`, isteğe bağlı olarak `/summary <topic>` | Sohbeti şimdiye kadar özetlersiniz. Yeni bir oturuma devretmek için kullanışlıdır. Mevcut bir oturum gerekir. |
| Workspace review | `/workspace-review` | Agent'tan mevcut workspace diff'ini amaç, doğruluk ve güvenlik açısından incelemesini istediğinizde. |
| Feature planning | `/plan-feature` | Belirsiz bir özellik fikrini yönlendirmeli bir soru-cevap diyaloğu ile uygulama planına dönüştürürsünüz. |
| Goal crafting | `/craft-goal`, isteğe bağlı olarak `/craft-goal <idea>` | Bir fikri Goal dialog'u için doğrulanabilir bir Goal hedefine dönüştürürsünüz. |
| Catch up | `/catch-up` | Bir projeye geri dönersiniz ve işlerin nerede kaldığını, sonra neye bakmanız gerektiğini sorarsınız. |
| Debugging | `/debug` | Bir hatayı incelersiniz. Agent hipotez kurar, kök nedeni koddan doğrular ve ancak sonra bir fix önerir. |
| Weigh options | `/weigh` | Ne inşa etmek istediğinizi bilirsiniz ama nasıl olacağını bilmezsiniz. Agent iki ya da üç yaklaşımı karşılaştırır ve birini önerir. |
| Fusion | Çoklu çalıştırma grubundaki "Run fusion" eylemi | Birden fazla çalıştırmanın çıktısını tek yanıtta birleştirirsiniz. Çalıştırma çıktıları yönergelerin sonuna eklenir. |
### Ayar girişi olmayan istemler
Birkaç istem otomatik tetiklenir ve Settings içinde düzenlenebilir bir sayfaları yoktur:
| Prompt | When it fires |
| --- | --- |
| Scheduled task | `/schedule-task`, isteğe bağlı olarak ilk fikirle. Zamanlanmış bir görevi tanımlayan diyaloğu yönlendirir. |
| Review handoff | `/handoff-review` ya da handoff etkinken diff görünümündeki Review düğmesi. Handoff'u çalışan oturumda oluşturur. |
| Review session starter | Üretilen review oturumunun açılış mesajı. Handoff üretilmişse onunla birlikte, değilse onsuz. |
| Review feedback / implementation response | İki oturum arasında shuttle mesajları: reviewer geri bildirimi implementing oturuma gider, implementer'ın yanıtı review oturumuna döner. |
## İlgili
- [Git & GitHub Workflows](/git/) — bu istemlerin çoğu git akışlarını güçlendirir
- [Notes, Todos & Plans](/notes-todos-plans/) — Planning istemlerinin arkasındaki todos ve planlar
- [Multi-run](/multi-run/) — run grupları ve fusion
+30
View File
@@ -0,0 +1,30 @@
---
title: MCP Sunucuları
description: Aracılara ek araçlar vermek için MCP sunucuları ekleyin.
---
# MCP Sunucuları
Bir MCP sunucusu, aracılarınıza veritabanında arama yapmak, bir API çağırmak ya da kullandığınız bir hizmeti okumak gibi ek araçlar verir. Bunları **Ayarlar → MCP** bölümünden ekleyin.
## Bir sunucu ekleyin
1. **Ayarlar → MCP** bölümünü açın.
2. Bir sunucu ekleyin ve türünü seçin:
- **local** — OpenChamber makinenizde bir komut çalıştırır. Çalıştırılacak komutu ve gerekirse ortam değişkenlerini siz verirsiniz.
- **remote** — OpenChamber başkası tarafından barındırılan bir URL'ye bağlanır. URL'yi ve ihtiyaç duyduğu başlıkları verirsiniz, örneğin bir kimlik doğrulama belirteci.
3. Kaydedin. Sunucu varsayılan olarak açıktır. Silmeden kapatabilirsiniz.
## Nerede geçerli olur
Sunucu eklerken kapsamı seçin:
- **personal** — her projede kullanılabilir
- **project** — yalnızca geçerli projede kullanılabilir ve projenin diğer ayarlarıyla birlikte kaydedilir
Sunucu adları küçük harf, rakam, tire ve alt çizgi kullanır.
## İlgili
- [Providers, Models & Agents](/providers/) — önce bir model bağlayın
- [Skills](/skills/) — aracılarınızın yapabileceklerini genişletmenin başka bir yolu
+43
View File
@@ -0,0 +1,43 @@
---
title: Mobil Uygulamalar ve PWA
description: OpenChamber uygulamasını iOS veya Android'e kurun ve sunucunuza bağlayın.
---
# Mobil Uygulamalar ve PWA
OpenChamber'ın iPhone ve Android için yerel uygulamaları var. Böylece oturumları izleyebilir, aracılara yanıt verebilir ve işleri telefonunuzdan yönetebilirsiniz. Evde Wi-Fi üzerinden de, [Private Relay](/private-relay/) ile her yerden de çalışır.
## Uygulamayı yükleyin
- **iPhone/iPad** — [TestFlight beta](https://testflight.apple.com/join/5ek6GU1E) sürümüne katılın
- **Android** — APK'yı [son sürüm](https://github.com/openchamber/openchamber/releases/latest) sayfasından indirin
## Sunucunuza bağlayın
1. OpenChamber'ın çalıştığı bilgisayarda **Settings → Remote Instances → Connect to this server** yolunu açın ve **Add a device** düğmesine basın.
2. **Anywhere** seçin veya telefonu sadece evde kullanacaksanız **Home network only** seçin ve **Create QR code** düğmesine basın.
3. Mobil uygulamada **Scan QR code** seçeneğine dokunun ve kamerayı koda doğrultun.
Uygulama bağlanır ve sunucuyu hatırlar. QR kod tek kullanımlıktır ve her cihazın kendine ait, iptal edilebilir bir belirteci olur. Eşlemenin nasıl güvenli kaldığını [Connect a Device](/connect-devices/) sayfasında görebilirsiniz.
Birkaç sunucuyla eşleşebilir ve instance listesinden aralarında geçiş yapabilirsiniz. Uygulama, her biri için erişilebilir olup olmadığını ve yerel ağ üzerinden mi yoksa relay üzerinden mi bağlı olduğunuzu gösterir.
## PWA (tarayıcı kurulumu)
Hiç uygulama mağazası istemiyor musunuz? Web uygulaması doğrudan tarayıcıdan kurulabilir:
- **desktop browser** — adres çubuğundaki **Install** seçeneğini kullanın
- **iPhone/iPad (Safari)** — Share → **Add to Home Screen**
- **Android (Chrome)** — menü → **Install app** / **Add to Home Screen**
Ağınızın dışından PWA'ya erişmek için bir [tunnel](/tunnels/) ve güçlü bir [UI password](/security/) gerekir. Yerel uygulamalar bunu sizin için relay üzerinden halleder.
## Mobil ayarlar
**Settings → OpenChamber** altında, mobil ve kurulu deneyimi ayarlayan birkaç seçenek vardır. Uygulamanın kurulu adı, ekran yönü ve ekran klavyesinin davranışı bunlara dahildir.
## İlgili
- [Connect a Device](/connect-devices/) — eşleştirme, tek kullanımlık QR kodlar ve cihaz yönetimi
- [Private Relay](/private-relay/) — "Anywhere" erişimi nasıl çalışır
- [Security](/security/) — UI'yı açığa çıkarmadan önce koruyun
@@ -0,0 +1,34 @@
---
title: Çoklu Çalıştırma
description: Aynı promptu birkaç model veya oturumda aynı anda çalıştırın.
---
# Çoklu Çalıştırma
Çoklu çalıştırma, tek bir formdan birkaç oturum başlatır. Aynı işi farklı modellerle denemek ve sonuçları karşılaştırmak için kullanışlıdır. Oturum kenar çubuğunun üstündeki düğmeden açılır.
## Çoklu çalıştırma başlatın
1. Çoklu çalıştırma başlatıcısını açın.
2. Projeyi seçin ve çalıştırma grubuna bir ad verin.
3. Promptu yazın ve hangi modellerle çalıştıracağınızı seçin. Grup başına en fazla beş model seçebilirsiniz.
4. **isolate runs** seçeneğini kullanıp kullanmayacağınıza karar verin.
5. Başlatın.
Her model kendi oturumunu alır ve hepsi promptunuzla başlar.
## İzole çalıştırmalar
Her çalıştırmaya kendi [worktree](/worktrees/) ve branch'ini vermek için **isolate runs** seçeneğini açın. Böylece aynı dosyalara dokunmazlar. Bunun için bir git deposu gerekir. Depo olmayan klasörlerde bu seçenek otomatik olarak kapanır. Çalıştırmaların başlayacağı branch'i seçin.
İzolasyon kapalıyken her çalıştırma proje klasöründe düz bir oturum olur.
## Sonuçları karşılaştırma
Her çalıştırma, açıp okuyabileceğiniz, saklayabileceğiniz ya da silebileceğiniz normal bir oturumdur. Yaklaşımları karşılaştırmak için başlattıysanız, yan yana inceleyin ve en iyi olanı devam ettirin.
Tek bir çalıştırma başlayamazsa diğerleri yine de açılır. Sadece istediğinizden daha az oturum görürsünüz.
## İlgili
- [Worktree Sessions](/worktrees/) — izolasyonun perde arkasında nasıl çalıştığı
@@ -0,0 +1,37 @@
---
title: Proje Notları, Yapılacaklar ve Planlar
description: Her proje için notları, yapılacaklar listesini ve kaydedilmiş planları tutun.
---
# Proje Notları, Yapılacaklar ve Planlar
Her projenin notlar, yapılacaklar listesi ve kaydedilmiş planlar için kendi çalışma alanı vardır. Bunlar bir oturuma değil projeye aittir, bu yüzden oturumlar arasında geçerken yerinde kalırlar. Sağ kenar çubuğundaki **Context** sekmesinde bulabilirsiniz. Mobilde bunun için ayrı bir sekme vardır.
## Notlar
Proje hakkında hatırlamak istediğiniz her şey için serbest biçimli bir not alanı. Yazdıkça kendi kendine kaydeder.
## Yapılacaklar
Basit bir kontrol listesi. Maddeler ekleyin, işaretleyin, sürükleyerek sıralayın ve bitenleri temizleyin.
Her yapılacak öğesinde bir **send** menüsü vardır. Böylece onu aracıya verebilirsiniz:
- mevcut oturuma gönderin
- onunla yeni bir oturum başlatın
- onunla yeni bir [worktree session](/worktrees/) başlatın. Bu yalnızca proje bir git deposuysa mümkündür
## Planlar
Daha uzun planları kayıtlı dosyalar olarak tutmak için bir alan. Şunları yapabilirsiniz:
- bir planı Markdown veya metin dosyasından içe aktarmak
- bir planı açıp yan panelde okumak
- artık gerek olmayan planları silmek
Kaydedilmiş notunuz, işaretlediğiniz yapılacak öğeniz veya listelenen planınızla Context sekmesine geri dönmüş olmanız gerekir. İşlemin gerçekleştiğini böyle anlarsınız.
## İlgili
- [Worktree Sessions](/worktrees/) — bir yapılacak öğesini kendi branch'inde çalıştırın
- [Projects](/projects/) — bunlar etkin projeye aittir
@@ -0,0 +1,34 @@
---
title: Bildirimler
description: Bir oturum size ihtiyaç duyduğunda veya tamamlandığında haber alın.
---
# Bildirimler
Bildirimler, ekranı sürekli izlemenize gerek kalmadan dikkatinizi çekmesi gereken şeyleri söyler. Bir oturum tamamlandıysa, hata verdiyse, soru sorduysa ya da bir şey yapmak için izin gerekiyorsa haber alırsınız. Bunları **Settings → OpenChamber → Notifications** bölümünden ayarlayın.
## Açın
1. **Settings → OpenChamber → Notifications** bölümünü açın.
2. Tarayıcınız veya sisteminiz istediğinde bildirimlere izin verin.
3. Hangi durumların haberini almak istediğinizi seçin:
- bir oturum **finishes**
- bir oturum **error** alır
- bir oturum **asks a question**
- bir oturum **permission** ister
- **subtasks** tamamlanır
## Size nasıl ulaşır
- **desktop** üzerinde yerel sistem bildirimleri alırsınız
- bir **browser or installed app** içinde web push bildirimleri alırsınız. Böylece sekme arka plandayken de gelir
Otomatik kabul edilen oturumlar, izin bildirimleriyle sizi rahatsız etmez.
## Metni özelleştirin
Her bildirim türünün düzenleyebileceğiniz bir başlık ve mesaj şablonu vardır. Aracı adı ve model gibi alanları kullanabilirsiniz. Son mesajın ne kadarının ekleneceği için de bir sınır vardır. Bildirimler böylece kısa kalır.
## İlgili
- [Voice Mode](/voice/) — yanıtları onun yerine sesli dinleyin
@@ -0,0 +1,97 @@
---
title: OpenCode Sunucusu
description: OpenChamber'ı yerel veya uzak bir OpenCode sunucusuna bağlayın.
---
# OpenCode Sunucusu
OpenChamber, bir OpenCode sunucusunun üzerinde çalışır. Varsayılan olarak sizin için bir sunucu başlatır; bu nedenle bir şey yapmanız gerekmez. Bu sayfaya yalnızca OpenChamber'ı zaten çalıştırdığınız bir sunucuya yönlendirmek veya onun başlattığı sunucuyu yönetmek istiyorsanız ihtiyacınız olur.
## OpenChamber sunucuyu nasıl bulur
OpenChamber başladığında sunucuyu şu sırayla arar:
1. daha önce başlattığı sunucuyu yeniden kullanır
2. belirtmişseniz harici bir sunucuya bağlanır, aşağıya bakın
3. varsayılan bağlantı noktasındaki (`4096`) bir sunucuyu otomatik algılar
4. bunların hiçbiri yoksa kendi sunucusunu başlatır ve yönetir
Hiçbir ayar yoksa 4. adım otomatik olarak gerçekleşir ve kullanıma hazır olursunuz.
## Zaten çalıştırdığınız bir sunucuya bağlanın
OpenChamber'ı başlatmadan önce şunları ayarlayın:
```bash
OPENCODE_HOST=http://localhost:4096 OPENCODE_SKIP_START=true openchamber
```
- `OPENCODE_HOST`, bağlantı noktası da dahil olmak üzere OpenCode sunucunuzun tam adresidir, örneğin `http://localhost:4096`. Sonunda bir yol olmamalıdır.
- `OPENCODE_SKIP_START=true`, OpenChamber'a kendi sunucusunu başlatmamasını söyler.
Yalnızca bağlantı noktasını değiştirmek istiyorsanız `OPENCODE_PORT` öğesini `OPENCODE_HOST` yerine ayarlayın.
`OPENCODE_HOST` bağlantı noktasını içermiyorsa veya bir yol içeriyorsa OpenChamber bunu yok sayar ve kendi sunucusunu başlatmaya döner. Beklediğiniz bağlantı kurulmadıysa başlangıç günlüklerindeki `[config]` uyarısına bakın.
## Sunucuyu CLI ile yönetin
```bash
openchamber status
openchamber logs
openchamber restart
openchamber stop
```
Tek başına `openchamber`, sunucuyu arka planda başlatır. Sunucunun terminale bağlı kalması için `--foreground` ekleyin.
## OpenChamber'ı oturum açınca başlatın
Yerel bir kullanıcı hizmeti kurmak için `startup enable` kullanın. OpenChamber macOS'ta `launchd`, Linux'ta `systemd --user`, Windows'ta Task Scheduler kullanır.
```bash
openchamber startup enable
openchamber startup status
openchamber startup disable
```
Arayüzü korumak için hizmeti etkinleştirirken parolayı ayarlayın:
```bash
OPENCHAMBER_UI_PASSWORD='secret' openchamber startup enable
```
Oturum açınca başlayan ve masaüstü veya mobil istemciler için kullanılacak başsız bir sunucu için `--api-only` ile erişilebilir bir host ekleyin:
```bash
openchamber startup enable --port 3000 --api-only --host 0.0.0.0 --ui-password secret
```
`startup enable`, hizmetin aynı kabuktan `openchamber` başlatmaya daha çok benzemesi için geçerli ortamınızı hizmete kaydeder. Böylece sağlayıcı token'ları, `PATH`, SSH agent ayarları ve diğer CLI kimlik doğrulama veya yapılandırma değişkenleri kullanılabilir kalır. Daha az ortam değişkeni olan bir hizmet istiyorsanız `--no-env-snapshot` kullanın.
Başlangıç hizmeti `--port`, `--host`, `--ui-password` ve `--api-only` değerlerini hatırlar. CLI ile yeniden başlatma ve güncelleme sonrası yeniden başlatma, kaydedilmiş bu ayarları yeniden kullanır.
Başka bir OpenChamber uygulaması için bağlantı bağlantısı oluşturmak üzere şunu kullanın:
```bash
openchamber connect-url --port 3000 --server http://your-host:3000 --qr
```
`openchamber connect-url --help` komutunu çalıştırarak `--name`, `--lan`, `--server`, `--api-only`, `--ui-password` ve `--qr` dahil tüm bağlantı seçeneklerini görün.
Çalışan bu hizmetin tünellerini yine de bağımsız olarak yönetebilirsiniz:
```bash
openchamber tunnel start --port 3000
openchamber tunnel stop --port 3000
```
Tüneli durdurmak hizmeti veya uygulamayı yeniden başlatmaz.
## "OpenCode yeniden başlatılıyor"
Sunucu başlarken veya yeniden başlarken OpenChamber "OpenCode yeniden başlatılıyor" durumunu gösterir ve hazır olana kadar istekleri duraklatır. Bu, uygulama açıldıktan veya yeniden başlatıldıktan hemen sonra normaldir. Durum hiç kaybolmazsa [OpenCode bağlantısı](/troubleshooting/opencode-connection/) sayfasına bakın.
## İlgili sayfalar
- [Sağlayıcılar, modeller ve agentlar](/providers/), sunucunun konuşacağı hizmetleri ayarlayın
- [OpenCode bağlantısı](/troubleshooting/opencode-connection/), bağlanmıyorsa
+35
View File
@@ -0,0 +1,35 @@
---
title: Önizleme ve Geliştirme Sunucuları
description: Çalışan bir geliştirme sunucusunu OpenChamber içinde açın.
---
# Önizleme ve Geliştirme Sunucuları
Bir geliştirme sunucusu başlattığınızda, OpenChamber onu ayrı bir tarayıcı sekmesi yerine doğrudan uygulama içinde açabilir. Böylece sitenizi sohbetin yanında görür, öğelere işaret edip onlar hakkında soru sorabilirsiniz.
## Bir geliştirme sunucusu açın
Tarayıcı panelini uygulama başlığındaki küre düğmesinden açın. Bir geliştirme sunucusu zaten çalışıyorsa listede görünür ve tek tıklamayla açılır. OpenChamber bunu makinenizde gerçekten hangi adreslerin dinlediğine bakarak bulur, bu yüzden nasıl başlattığınız önemli değildir.
Geliştirme sunucusu şu durumlarda da otomatik açılır:
- terminalde yerel bir adreste **Open preview** düğmesine bastığınızda
- otomatik açma açık olan bir [project action](/project-actions/) bunu başlattığında
- bir sohbet mesajındaki yerel bağlantıyı takip ettiğinizde
Adresi her zaman kendiniz de yazabilirsiniz. Sadece `localhost:5173` yazarsanız `http://` olarak kabul edilir. Bu yüzden şemayı yazmanız gerekmez.
## Uzak bir OpenChamber ile çalışma
OpenChamber başka bir makinede çalışıyorsa, geliştirme sunucusu o makinededir. Dizüstü bilgisayarınızdaki `localhost` bambaşka bir yeri anlatır. Masaüstü uygulaması bunu sizin için halleder. Uzak geliştirme sunucusuna bağlantıyı taşıyan yerel bir port açar. Böylece sayfa normal şekilde yüklenir, hot reload ve geliştirici araçları çalışır. Siz beklediğiniz adresi yazmaya devam edersiniz, ayrıntılar araya girmez.
Bu, masaüstü uygulamasını gerektirir. Web tarayıcı sekmesinde yalnızca kendi makinenizdeki geliştirme sunucuları açılabilir.
## Sayfayı işaretleyin
Öğelere işaret etmek, sayfanın üzerine çizmek ve bunların hepsini sohbete göndermek için [Browser Panel](/desktop-browser/) sayfasına bakın.
## İlgili
- [Project Actions](/project-actions/) — başlattığınızda bir sunucuyu otomatik açın
- [Browser Panel](/desktop-browser/) — sayfaları işaretleyin ve aracıya kontrol ettirin
@@ -0,0 +1,44 @@
---
title: Özel Relay
description: OpenChamber sunucunuza her yerden uçtan uca şifreli bir relay üzerinden ulaşın. Port yok, tunnel yok, kurulum yok.
---
# Özel Relay
OpenChamber Private Relay, eşleştirilmiş cihazlarınızın sunucunuza her yerden ulaşmasını sağlar. Hücresel ağdan, bir kafe ağından ya da başka bir şehirden bağlanabilirsiniz. Port açmanız, tunnel kurmanız ya da makinenizi internete açmanız gerekmez. Kendi kendini yönetir. [Connect a Device](/connect-devices/) içinde bir cihazı **Anywhere** ile eşleştirmek yeterlidir.
## Nasıl çalışır
Sunucunuz OpenChamber'ın relay altyapısına dışarı giden bir bağlantı açar ve onu açık tutar. Cihazlarınızdan biri ağınızın dışındayken o da relay'e bağlanır ve relay iki taraf arasındaki şifreli trafiği taşır. Makinenizde internetten gelen bağlantıları dinleyen bir şey yoktur.
Doğrudan bağlantı varsa, örneğin aynı Wi-Fi üzerindeyseniz, cihazlarınız onu tercih eder ve relay'i tamamen atlar.
## Relay neyi görür, neyi göremez
Relay kör bir kurye gibidir, aradaki kişi değil:
- **Uçtan uca şifreli.** Cihazınız ve sunucunuz şifreleme anahtarlarını doğrudan birbirleriyle anlaşarak belirler. Relay, anahtarı olmayan kapalı trafiği taşır. Kodunuzu, promptlarınızı veya parolalarınızı okuyamaz.
- **Yalnızca cihazlarınız bağlanabilir.** Bir cihazın, [one-time pairing](/connect-devices/) yoluyla *sizin* sunucunuzdan verilmiş bir belirtece sahip olması gerekir. Kimse relay üzerinden sunucunuzu keşfedemez ya da sizin oluşturduğunuz bir belirteç olmadan bağlanamaz. İstediğiniz anda herhangi bir belirteci iptal edebilirsiniz.
- **Eşleştirme bağlantıları tek kullanımlıktır.** Bir eşleştirme QR kodu tam olarak bir kez çalışır ve kullanılmazsa süresi dolar. Bu yüzden sızan eski bir bağlantı değersizdir.
- **Siz izin vermedikçe hiçbir şey paylaşılmaz.** Relay, siz açana ya da onun üzerinden bir cihaz eşleştirene kadar kapalı kalır. İstediğiniz zaman kapatabilirsiniz. Onun üzerinden bağlı cihazlar anında kesilir.
## Ne zaman çalışır
Relay kendi yaşam döngüsünü yönetir. Hatırlanacak bir anahtar yoktur:
- **İhtiyaç olunca başlar.** Bir **Anywhere** eşleştirmesi oluşturmak relay'i açar. Eşleştirilmiş cihazlardan biri ona ihtiyaç duyduğu sürece yeniden başlatmadan sonra da geri gelir.
- **Kendi kendine durur.** Hiçbir cihaz ya da bekleyen eşleştirme relay'i kullanmıyorsa, örneğin son relay ile eşleştirilmiş cihazı iptal ettikten sonra, otomatik olarak kapanır.
**Settings → Remote Instances → OpenChamber Relay** ekranında canlı durum görünür. Connected, Reconnecting gibi durumlar ve şu anda üzerinden bağlı cihaz sayısı burada gösterilir. Ayrıca oradan **Disable** düğmesine basarak relay erişimini hemen kapatabilirsiniz. Yerel ağınızdaki cihazlar bundan etkilenmez.
## Relay mi tunnel mı?
- **relay**'i kendi eşleştirilmiş cihazlarınızdan kendi sunucunuza ulaşmak için kullanın. Kurulum gerekmez ve hiçbir şey herkese açık olmaz.
- Bir [tunnel](/tunnels/) kullanın. Bu, düz bir **public URL** gerektiğinde işe yarar. Örneğin OpenChamber'ı eşleştiremeyeceğiniz bir makinede sıradan bir tarayıcıda açmak ya da bir [UI password](/security/) arkasından erişim paylaşmak için.
## İlgili
- [Connect a Device](/connect-devices/) — bir cihazı tek kullanımlık QR kodla eşleştirin
- [Mobile Apps](/mobile/) — iOS veya Android uygulamasını yükleyin
- [Security](/security/) — parolalar, passkey'ler ve temel maruz kalma bilgileri
- [Remote access](/troubleshooting/remote-access/) — bağlantı tamamlanmadığında
@@ -0,0 +1,28 @@
---
title: Proje Eylemleri
description: Sık çalıştırdığınız komutları kaydedin ve tek tıklamayla başlatın.
---
# Proje Eylemleri
Proje eylemi, bir kez kaydedip tıklamayla çalıştırdığınız bir shell komutudur. Geliştirme sunucunuz, bir build, bir test çalıştırması olabilir. Her projenin kendi listesi vardır. Bunları **Settings → Projects → Project Actions** bölümünden ayarlayın.
## Bir eylem ekleyin
1. **Settings → Projects** bölümünü açın ve **Project Actions** kısmını bulun.
2. Bir eylem ekleyin, ad verin, bir ikon seçin ve çalıştırılacak komutu yazın.
3. Kaydedin.
Komut yalnızca bir işletim sisteminde anlamlıysa eylemi belirli işletim sistemleriyle sınırlayabilirsiniz.
## Bir eylemi çalıştırın
Eylemler uygulama başlığındaki bir menüde görünür. Birine tıklayın ve OpenChamber onu proje klasörünüzde bir terminalde çalıştırsın. Çıkışı izleyebilmeniz için sizi terminal görünümüne geçirir. Aynı menüden durdurabilirsiniz.
## Bir geliştirme sunucusunu otomatik açın
Sunucu başlatan bir eylem için **auto-open URL** seçeneğini açın. OpenChamber çıktıda yerel bir adres arar ve açmayı önerir. Ayrıntılar için [Preview & Dev Servers](/preview/) sayfasına bakın. Masaüstünde bunu bir SSH port yönlendirmesi üzerinden de geçirebilirsiniz.
## İlgili
- [Preview & Dev Servers](/preview/) — çalışan bir geliştirme sunucusunu OpenChamber içinde açın
@@ -0,0 +1,20 @@
---
title: Proje İkonları
description: Her projeye kolay tanınan bir ikon verin.
---
# Proje İkonları
Bir proje ikonu, projelerinizi ilk bakışta ayırt etmeyi kolaylaştırır. OpenChamber sizin için bir ikon bulmaya çalışır, isterseniz kendi ikonunuzu da ayarlayabilirsiniz. **Settings → Projects** bölümünden yönetilir.
## Otomatik bulma
Bir proje eklediğinizde OpenChamber içinde bir `favicon` dosyası arar ve onu proje ikonu olarak kullanır. Deponuz zaten bir favicon içeriyorsa ikon genellikle kendiliğinden görünür. Yapacak bir şey yoktur.
## Kendi ikonunuzu ayarlayın
**Settings → Projects** bölümünü açın ve bir görsel yükleyin. PNG, JPEG veya SVG olabilir, en fazla 5 MB olmalıdır. Özel görsel, otomatik bulunan görselin önüne geçer. İsterseniz onun yerine bir renk de seçebilirsiniz. Görsele dönmek için resmi kaldırmanız yeterlidir.
## İlgili
- [Projects](/projects/) — projelerinize ad, renk ve düzen verin
@@ -0,0 +1,34 @@
---
title: Projeler
description: İşinizi projelere ayırın ve aralarında geçiş yapın.
---
# Projeler
Proje, OpenChamber'ın takip ettiği bilgisayarınızdaki bir klasördür. Genellikle tek bir kod tabanıdır. Proje değiştirince aracıların çalıştığı klasör değişir. O projeye ait oturumlar ve ayarlar da birlikte gelir.
## Bir proje ekleyin
Bir projeyi birkaç yerden ekleyebilirsiniz:
- komut paletindeki **Add project** girdisi
- oturum kenar çubuğunun üstündeki **+** düğmesi
- bir dizin seçerken klasör tarayıcısı
Klasörü gösterin, OpenChamber onu hatırlar. Ad klasörden gelir. Sonradan değiştirebilirsiniz.
## Projeler arasında geçiş yapın
Etkin yapmak için kenar çubuğundan bir proje seçin. Oturumlar, git, notlar gibi her şey açık olan projeyi izler.
## Projeyi tanınır hale getirin
Özel ad, renk veya ikon vermek için **Settings → Projects** bölümünü açın. OpenChamber bir ikonu otomatik bulmaya çalışır. Ayrıntı için [Project icons](/project-icons/) sayfasına bakın.
> VS Code içinde OpenChamber her zaman açık olan klasörü tek proje olarak kullanır. Bu yüzden ekleyecek ya da geçiş yapacak bir şey yoktur. Projects ayar sayfası orada gizlidir.
## İlgili
- [Project Notes, Todos & Plans](/notes-todos-plans/) — proje başına çalışma notları tutun
- [Project Actions](/project-actions/) — sık çalıştırdığınız komutları kaydedin
- [Context](/context/) — bir oturumun model belleğinin ne kadarını kullandığını görün
@@ -0,0 +1,62 @@
---
title: Sağlayıcılar, Modeller ve Aracılar
description: Yapay zeka sağlayıcıları bağlayın, model seçin ve aracılar kurun.
---
# Sağlayıcılar, Modeller ve Aracılar
OpenChamber'ın bir şey yapabilmesi için önce en az bir yapay zeka sağlayıcısı bağlanmış olmalıdır. Bu sayfa bir sağlayıcı bağlamayı, model seçmeyi ve aracıları ayarlamayı anlatır.
## Bir sağlayıcı bağlayın
1. **Settings → Providers** bölümünü açın.
2. **Add provider** menüsünü açın ve henüz bağlı olmayan bir sağlayıcı seçin ya da OpenAI uyumlu bir uç nokta için **Other / Custom** seçin.
3. Sağlayıcıya göre iki yoldan biriyle oturum açın:
- **API key** — anahtarınızı yapıştırın ve kaydedin.
- **Sign-in (device flow)** — OpenChamber size bir bağlantı ve kısa bir kod gösterir. Bağlantıyı açın, kodu girin ve onaylayın. OpenChamber bağlantıyı kendi başına tamamlar.
### Custom / Other sağlayıcılar
Gateway'ler, kampüs LLM'leri, Ollama, LiteLLM ve benzeri OpenAI uyumlu API'ler için:
1. Sağlayıcı listesinde **Other / Custom** seçin.
2. Bir sağlayıcı kimliği, görünen ad, base URL (`http://` veya `https://`), API anahtarı (`{env:VAR_NAME}` da olabilir) ve en az bir model id/name girin.
3. İsterseniz istek başlıkları ekleyin.
4. Kaydedin. OpenChamber sağlayıcı bloğunu OpenCode config'e yazar ve anahtarı OpenCode auth içinde saklar, düz anahtarlar için ya da bir `{env:VAR}` referansı olarak kaydeder.
5. Mevcut bir custom sağlayıcıyı değiştirmek için onu açın ve **Edit** seçin.
Custom sağlayıcılar, tam bağlı görünmeden önce bir API anahtarı ya da `{env:VAR_NAME}` ister. Kimlik bilgisi yoksa modeller görünse bile sohbet çağrıları başarısız olur.
Bir sağlayıcı bağlı görünürse modelleri sohbette kullanılabilir hale gelir.
Bağlantıyı kesmek için sağlayıcıyı açın ve girişini kaldırmayı seçin.
## Model seçin
Modeli çalıştığınız yerde seçersiniz:
- sohbette, o oturum için sağlayıcı ve modeli ayarlamak üzere mesaj çubuğundaki model seçiciyi kullanın
- aracı başına, aşağıda bir varsayılan model ayarlayın
## Aracıları ayarlayın
Aracı, adı olan bir ayardır. Bir model, bir kişilik ve ne yapmasına izin verildiği birlikte tanımlanır.
1. **Settings → Agents** bölümünü açın.
2. Bir aracı seçin ya da yenisini oluşturun.
3. Şunlardan birini düzenleyin:
- **description** — aracının ne için olduğu
- **model** — varsayılan modeli
- **temperature** — yanıtlarının ne kadar yaratıcı olduğu
- **prompt** — her zaman izlediği kalıcı yönergeler
- **tool rules** — hangi araçları kullanabileceği
## Oturum açmalarınız nerede tutulur
Sağlayıcı oturum açmaları OpenChamber'da değil OpenCode'da saklanır. Bu yüzden OpenCode CLI ile paylaşılır. Aynı sağlayıcıyı birden fazla yerde ayarlarsanız en özel ayar kazanır. Proje düzeyindeki ayar, kişisel ayarın üzerine yazar.
## İlgili
- [Integrations](/integrations/) — Claude veya Cursor aboneliğini sağlayıcı olarak kullanın
- [MCP Servers](/mcp/) — aracılar için ek araçlar ekleyin
- [Usage & Quotas](/usage/) — ne kadar kullandığınızı izleyin
@@ -0,0 +1,26 @@
---
title: Hızlı Başlangıç
description: OpenChamber'ı hızlıca başlatın ve iş için doğru uygulamayı seçin.
---
# Hızlı Başlangıç
## En hızlı yol
1. [OpenCode](https://opencode.ai) yükleyin.
2. OpenChamber CLI'yı yükleyin. Tek satırlık komut için [Install](/install/) sayfasına bakın.
3. `openchamber --ui-password be-creative-here` komutunu çalıştırın.
4. CLI'nın yazdırdığı URL'yi açın. Genellikle `http://localhost:3000` olur.
5. Telefonunuzdan kullanmak için bir [tunnel](/tunnels/) başlatın ve QR kodu tarayın.
Tarayıcınızda OpenChamber oturum listesini görmelisiniz. Yüklendiyse çalışıyordur.
Güçlü bir UI şifresi kullanın, özellikle instance'ı internete açmayı düşünüyorsanız.
Sayfa yüklenmezse [Troubleshooting](/troubleshooting/) bölümüne bakın.
## Hangi uygulamayı kullanmalıyım?
- macOS'ta günlük iş için **desktop** kullanın
- uzaktan erişim ve telefondan inceleme için **web** kullanın
- kodunuzun hemen yanında oturumlar için **VS Code** kullanın
@@ -0,0 +1,48 @@
---
title: Uzak örnekler
description: Masaüstü uygulamasını SSH üzerinden başka bir makinedeki OpenChamber'a bağlayın.
---
# Uzak örnekler
Masaüstü uygulaması, başka bir makinede çalışan OpenChamber'a SSH üzerinden bağlanabilir. Bu bir iş sunucusu, bulut kutusu ya da ev laboratuvarı olabilir. Arayüzünü yerelmiş gibi ekranınıza getirir. Bunu **Ayarlar → Uzak örnekler** bölümünden kurun.
> Uzak örnekler yalnızca **masaüstü** özelliğidir. Web'de ya da VS Code'da, uzak bir sunucuya [OpenCode Server](/opencode-server/) içindeki ortam değişkenleriyle bağlanın.
## Uzak örnek ekleme
1. **Ayarlar → Uzak örnekler** bölümünü açın ve bir tane ekleyin.
2. Makineye ulaşmak için normalde kullandığınız SSH komutunu ve bir takma ad girin.
3. OpenChamber'ın orada nasıl çalışacağını seçin:
- **managed** — OpenChamber kendini uzak makineye kurar ve başlatır
- **external** — zaten çalışan bir örneğe bağlanır
4. Bağlanın.
OpenChamber adımları tek tek ilerletir. Bağlantıyı kontrol eder, uzak ortamı hazırlar, sunucuyu başlatır ve portu yönlendirir. Her aşamada nerede olduğunu gösterir. **ready** noktasına geldiğinde uzak arayüz yerelde açılır.
## Kimlik bilgileri
SSH ve arayüz parolalarını kaydetmeyi ya da her seferinde girmeyi siz seçersiniz. Bağlantı koparsa OpenChamber hangi adımın başarısız olduğunu bildirir, böylece düzeltebilirsiniz. Ayrıntı için [Uzak erişim](/troubleshooting/remote-access/) sayfasına bakın.
## Bağlantı bağlantıları
Uzak makinede OpenChamber zaten çalışıyorsa, masaüstü uygulamasını bağlamanın en kolay yolu eşleştirme bağlantısıdır. Uzak sunucunun arayüzünde **Ayarlar → Uzak örnekler → Bu sunucuya bağlan → Bir cihaz ekle** yolunu açın, bir bağlantı oluşturun ve masaüstünüzde **Ayarlar → Uzak örnekler → Diğer OpenChamber sunucuları → Bağlantıyı içe aktar** bölümünden içe alın. Tam akış için [Cihaz bağlama](/connect-devices/) sayfasına bakın.
**Anywhere** ile oluşturulan bir bağlantı hem doğrudan adres hem de bir [Private Relay](/private-relay/) yolu taşır. Masaüstü, sunucuya erişebildiğinde doğrudan bağlanır. Siz uzaktayken uçtan uca şifrelenmiş relay'e düşer. Kaydedilen her sunucunun yanındaki durum, hangi yolun kullanıldığını gösterir.
Bağlantıyı uzak makinedeki bir terminalden de oluşturabilirsiniz:
```bash
openchamber connect-url --port 3000 --server http://your-host:3000 --qr
```
`connect-url`, o portta hiçbir şey çalışmıyorsa önce sunucuyu başlatır. `--api-only` ile başsız bir sunucu, `--lan` ile başlatırken LAN'a bağlanma, `--ui-password` ile tarayıcı erişimini koruma ve `--name` ile kaydedilen bağlantıya ad verme ekleyebilirsiniz. Yerel ağın dışındayken de çalışan bir bağlantı için `--relay` ekleyin. Cihaz mümkün olduğunda doğrudan bağlantıyı kullanır, erişemezse [Private Relay](/private-relay/) ile devam eder. Örnek relay'i kendi başına ayağa kaldırır.
Oluşturulan bağlantı tek kullanımlık bir eşleştirme sırrı içerir. İçe aktarıldıktan sonra cihaz, tarayıcı arayüz parolasından ayrı kendi istemci belirtecini tutar. Bu belirteç, veren sunucuda iptal edene kadar sunucu yeniden başlasa da kalır.
## İlgili
- [Cihaz bağlama](/connect-devices/) — eşleştirme bağlantıları, QR kodları ve cihaz yönetimi
- [Private Relay](/private-relay/) — "Anywhere" bağlantılarının nasıl çalıştığı
- [OpenCode Server](/opencode-server/) — web'de ya da VS Code'da uzak sunucuya bağlanma
- [Uzak erişim](/troubleshooting/remote-access/) — bağlantı tamamlanmadığında
@@ -0,0 +1,347 @@
---
title: Reverse Proxy
description: OpenChamber'ı Nginx, Nginx Proxy Manager veya başka bir reverse proxy arkasında doğru şekilde ayarlayın.
---
# Reverse Proxy
OpenChamber'ı Nginx, Nginx Proxy Manager, Caddy, Cloudflare ya da başka bir reverse proxy arkasında çalıştırıyorsanız bu sayfayı kullanın.
## Proxy'lemeden önce
1. Önce OpenChamber'ın doğrudan çalıştığını doğrulayın.
2. Aynı ağdan `http://<server-ip>:3000` adresini ya da özel portunuzu açın.
3. Reverse proxy'yi yalnızca doğrudan bağlantı çalıştıktan sonra ekleyin.
## Proxy'nin desteklemesi gerekenler
- Canlı mesaj aktarımı için WebSocket'ler:
- `/api/event/ws`
- `/api/global/event/ws`
- `/api/terminal/ws`
- Buffering olmadan SSE:
- `/api/event`
- `/api/global/event`
- `/api/notifications/stream`
- `/api/openchamber/events`
- `/api/terminal/:sessionId/stream`
- Ek dosyalar ve dosya işlemleri için büyük istek gövdeleri
- Canlı akışlar ve terminal oturumları için uzun süreli okuma zaman aşımı
## Önemli kurallar
- WebSocket proxying'i açın.
- SSE rotalarında buffering'i kapatın.
- OpenChamber yanıtları zaten sıkıştırıyorsa proxy'de gzip'i kapatın.
- Sıkıştırmayı yalnızca bir katmanda açık bırakın.
- `Host`, `X-Forwarded-For` ve `X-Forwarded-Proto` gibi normal proxy başlıklarını iletin.
- Kullanıcılar dosya yüklüyorsa body boyutu sınırlarını artırın.
## Hızlı kontrol listesi
- OpenChamber LAN üzerinden doğrudan erişilebilir
- Proxy'de WebSocket'ler açık
- SSE rotalarında buffering kapalı
- Proxy host'unda `gzip off`, ya da sıkıştırma başka bir yolla kapalı
- `client_max_body_size` ek dosyalar için yeterince büyük
- `proxy_read_timeout` akışlar için yeterince uzun
## Örnek: Nginx
<details>
<summary>Örnek konfigürasyonu göster</summary>
```nginx
client_max_body_size 50M;
client_body_buffer_size 50M;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
gzip off;
location = /api/terminal/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location = /api/global/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location = /api/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location ~ ^/api/terminal/.+/stream$ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
```
</details>
## Örnek: Nginx Proxy Manager
<details>
<summary>Advanced sekmesi örneğini göster</summary>
```nginx
client_max_body_size 50M;
client_body_buffer_size 50M;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
gzip off;
location = /api/terminal/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/global/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/event/ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/event {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/global/event {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/notifications/stream {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location = /api/openchamber/events {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location ~ ^/api/terminal/.+/stream$ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Accept "text/event-stream";
proxy_set_header Cache-Control "no-cache";
proxy_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering "no" always;
add_header Cache-Control "no-cache, no-transform" always;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 30s;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
```
</details>
Bu host için Nginx Proxy Manager'da `Websockets Support` özelliğini de etkinleştirin.
## Yaygın hata belirtileri
### Sayfa yükleniyor ancak mesaj gönderme başarısız oluyor
- Proxy'de WebSocket'ler etkin değildir
- `/api/event/ws` veya `/api/global/event/ws` doğru şekilde iletilmiyordur
### Bildirimler veya canlı durum güncellenmiyor
- SSE rotalarından biri buffer'lanıyor veya önbelleğe alınıyordur
- `X-Accel-Buffering "no"` eksiktir
### Dosya yüklemeleri başarısız oluyor
- `client_max_body_size` çok küçüktür
### Her şey yerelde çalışıyor ama yalnızca proxy arkasında bozuluyor
- Proxy canlı trafiği sıkıştırıyor ve buffer'lıyordur
- Proxy'de WebSocket desteği yoktur
## Örnek: Caddy
<details>
<summary>Örnek konfigürasyonu göster</summary>
```caddy
reverse_proxy 127.0.0.1:3000 {
# WebSocket support is automatic in Caddy
# Flush SSE responses immediately
flush_interval -1
# Pass through Host and proxy headers
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
# Increase timeouts for long-lived streams
transport http {
read_timeout 3600s
write_timeout 3600s
}
}
```
</details>
Caddy, WebSocket yükseltmelerini otomatik olarak işler. Ek yapılandırma gerekmez. `flush_interval -1` yönergesi, SSE parçalarının buffer'lanmadan hemen iletilmesini sağlar.
## CDN ve çift sıkıştırma uyarısı
Reverse proxy'nizin önüne Cloudflare gibi bir CDN koyarsanız çift sıkıştırmaya dikkat edin:
- OpenChamber HTTP yanıtlarını gzip ile sıkıştırır, eşik 1 KB'dir.
- Cloudflare ve diğer CDN'ler de varsayılan olarak yanıtları sıkıştırır.
- Bu, çift sıkıştırılmış yanıtlara veya yanlış `Content-Encoding` başlıklarına yol açabilir.
Bunu önlemek için **bir** katmandaki sıkıştırmayı kapatın:
- **Cloudflare:** Rules → Compression → devre dışı bırakın veya "Passthrough" modunu kullanın.
- **Nginx:** `gzip off`, yukarıdaki örneklerde zaten gösterilmiştir.
- **Caddy:** Üst sunucu zaten sıkıştırılmış içerik gönderiyorsa Caddy varsayılan olarak yeniden sıkıştırmaz.
SSE akış rotaları OpenChamber tarafından sıkıştırmanın dışında tutulur, ancak CDN bunları yine de buffer'layabilir. SSE yollarında buffer'lamayı nasıl kapatacağınızı CDN belgelerinden kontrol edin.
## İlgili sayfalar
- [Tüneller](/tunnels/)
- [Sorun giderme](/troubleshooting/)
@@ -0,0 +1,78 @@
---
title: Zamanlanmış görevler
description: Bir promptu zamanlamaya göre otomatik çalıştırın.
---
# Zamanlanmış görevler
Zamanlanmış bir görev, sizin için bir promptu belirlediğiniz programa göre çalıştırır. Örneğin günlük "dünkü değişiklikleri özetle" ya da haftalık bir temizlik. Çalıştığında OpenChamber yeni bir oturum başlatır ve promptu kendi kendine gönderir. Zamanlayıcıyı oturum kenar çubuğunun üstündeki düğmeden açın.
## Görev oluşturma
1. Oturum kenar çubuğundan zamanlanmış görevler iletişim kutusunu açın.
2. Bir görev ekleyin ve ona bir ad verin.
3. Ne zaman çalışacağını seçin:
- **daily** — her gün bir ya da daha fazla saatte
- **weekly** — seçilen haftanın günlerinde ve saatlerinde
- **once** — tek bir tarih ve saatte
- **cron** — serbest bir cron ifadesi
4. Ne yapacağını ayarlayın. Gönderilecek promptu, kullanılacak sağlayıcıyı, modeli ve agent'ı seçin. Prompt bir slash komutu da olabilir, örneğin `/review`.
5. Kaydedin ve görevin etkin olduğundan emin olun.
İsterseniz herhangi bir görevi hemen **run now** ile çalıştırıp beklediğiniz gibi davrandığını görebilirsiniz.
Çalışmanın tek bir yanıttan sonra durmak yerine promptu tamamlamaya devam etmesini istiyorsanız **Run as goal** seçin. Ayrıntı için [Oturum hedefleri](/session-goals/) sayfasına bakın.
## Döngüler: markdown dosyası olarak zamanlanmış görevler
Bir **loop**, depoya commit edebileceğiniz taşınabilir bir markdown dosyası olarak tanımlanan zamanlanmış görevdir. `.agents/loops/` içine bir dosya koyun ve senkronize etmek için Zamanlanmış Görevler listesini açın. Sunucuyu yeniden başlatmanız gerekmez:
```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.
```
### Dosyalar nerede yaşar
- **Proje kapsamı** — proje dizinindeki ya da git worktree köküne kadar olan herhangi bir üst dizindeki `.agents/loops/*.md` dosyaları.
- **Kullanıcı kapsamı** — `~/.agents/loops/*.md` açık olduğunuz her projeye uygulanır.
Bir proje loop'u ile bir kullanıcı loop'u aynı adı paylaşırsa proje loop'u kazanır.
### Alanlar
| Alan | Anlamı |
|---|---|
| `name` | Görev adı, zorunlu, en fazla 80 karakter. |
| `schedule` | Cron ifadesi, zorunlu. Loop dosyaları yalnızca cron kullanır. |
| `enabled` | Çalışması için `true` yapın. Loop'lar **varsayılan olarak kapalıdır**, bu yüzden bir dosyayı commit etmek tek başına bir görevi başlatmaz. |
| `model` | `provider/model`, zorunlu. Örneğin `anthropic/claude-sonnet-4-5`. |
| `agent` | Kullanılacak agent, isteğe bağlı. |
| `timezone` | IANA saat dilimi, isteğe bağlı, varsayılan olarak sunucu saat dilimi kullanılır. |
| body | Çalıştırma promptu, zorunlu. `/review src/` gibi bir slash komutu olabilir. |
### Loop'lar nasıl davranır
- **Dosya, var olduğu sürece yetkilidir.** **Edit** onu yerleşik dosya düzenleyicide açar, etkinleştirme anahtarı frontmatter'ını günceller ve görevi silmek onaydan sonra markdown dosyasını siler. **Run now** yine kullanılabilir.
- Çalışma zamanı durumu, son çalışma, sonraki çalışma ve durum proje yapılandırmasında tutulur ve markdown dosyasına geri yazılmaz.
- `name` alanını yeniden adlandırmak görevi yerinde yeniden adlandırır. Bir loop dosyası geçici olarak ayrıştırılamaz hale gelirse, örneğin düzenleme sırasında ya da bir merge conflict yüzünden, dosya düzelene kadar görev son iyi tanımla korunur.
- `daily` / `weekly` / `once` zamanlamaları ve hedef ayarları yalnızca arayüzdedir. Loop dosyaları her zaman cron kullanır.
## Başarılı olduğunda ne görünür
Bir çalışmadan sonra görev son ne zaman çalıştığını, başarılı olup olmadığını ve oluşturduğu oturuma bağlantıyı gösterir. Çalışma başarısız olursa hata da burada görünür.
## Aklınızda bulunsun
Görevler yalnızca OpenChamber sunucusu çalışırken tetiklenir. Kapatırsanız, zamanlanan çalıştırmalar sunucu geri gelene kadar duraklar.
## İlgili
- [Komutlar ve parçacıklar](/commands-snippets/) — prompt olarak bir slash komutunu yeniden kullanın
@@ -0,0 +1,44 @@
---
title: Güvenlik
description: Açığa çıkarmadan önce arayüzü bir parola ve passkey ile koruyun.
---
# Güvenlik
OpenChamber makinenize ve kodunuza erişim verir. Bu yüzden sizden başkası ulaşmadan önce kilitleyin. Bu sayfa arayüz parolasını, passkey'leri ve OpenChamber'ı bir ağa açmadan önce bilmeniz gerekenleri anlatır.
## Arayüz parolası ayarlama
OpenChamber'ı bir parola ile başlatın, tarayıcı arayüzü bunu ister:
```bash
openchamber --ui-password be-creative-here
```
Parolayı komut satırına yazmak yerine `OPENCHAMBER_UI_PASSWORD` ortam değişkeniyle de ayarlayabilirsiniz. Oturum açtıktan sonra OpenChamber bir süre cihazı hatırlar, böylece her seferinde sorulmaz.
Örnek başkaları tarafından erişilebilir durumdaysa, özellikle de bir [tunnel](/tunnels/) ya da genel internet üzerinden erişiliyorsa, mutlaka parola ayarlayın.
## Passkey'ler
Parola ayarlandıktan sonra daha hızlı giriş için passkey ekleyebilirsiniz. Bunlar Face ID, Touch ID ya da bir güvenlik anahtarı olabilir. **Ayarlar → OpenChamber → Passkeys** bölümünden ekleyin.
Passkey'ler geçerli parolaya bağlıdır. Parolayı değiştirir ya da kaldırırsanız, kayıtlı passkey'ler silinir ve yeniden eklemeniz gerekir.
## Cihaz belirteçleri
[Cihaz bağlama](/connect-devices/) ile eşleştirilen cihazlar, arayüz parolası yerine kendi cihaza özel belirteçleriyle kimlik doğrular. Eşleştirme bağlantıları tek kullanımlıktır ve kullanılmazsa süresi dolar. Eşleştirilen her cihaz **Ayarlar → Uzak örnekler → Bu sunucuya bağlan** bölümünde listelenir ve istediğiniz zaman herhangi birini iptal edebilirsiniz. Ev dışı bağlantılar [Private Relay](/private-relay/) üzerinden gider. Bu yol uçtan uca şifrelenmiştir ve trafiğinizi okuyamaz.
## Açığa çıkarmadan önce
- Varsayılan olarak OpenChamber yalnızca kendi makinenizde dinler (`127.0.0.1`). Daha geniş dinlemek için bilinçli bir değişiklik gerekir ve önce parola ayarlamalısınız.
- Kendi cihazlarınız için [eşleştirme](/connect-devices/) ile [Private Relay](/private-relay/) kullanmayı tercih edin. Hiçbir şey kamuya açık olmaz.
- Bir genel URL gerekiyorsa, internete bir port açmak yerine bir [tunnel](/tunnels/) ya da özel ağ, örneğin VPN, kullanın.
- OpenChamber'ı kendi HTTPS sunucunuzun arkasına koyuyorsanız [Reverse Proxy](/reverse-proxy/) sayfasına bakın.
## İlgili
- [Cihaz bağlama](/connect-devices/) — tek seferlik eşleştirme ve cihaza özel belirteçler
- [Private Relay](/private-relay/) — her yerden uçtan uca şifreli erişim
- [Tunnels](/tunnels/) — gerektiğinde genel bir URL açma
- [Reverse Proxy](/reverse-proxy/) — OpenChamber'ı kendi sunucunuzun arkasında çalıştırma
@@ -0,0 +1,73 @@
---
title: Oturum hedefleri
description: Bir promptu, agent'ın otomatik olarak doğru hedefe doğru çalıştığı bir hedefe dönüştürün.
---
# Oturum hedefleri
Bir hedef, tek bir promptu bir bitiş çizgisine dönüştürür. Her yanıttan sonra agent'a "devam et" demek yerine hedefi bir kez belirlersiniz. OpenChamber da oturumu buna doğru otomatik olarak çalıştırır ve her turdan sonra bağımsız bir denetçiyle ilerlemeyi kontrol eder. Siz uzaktayken de çalışmaya devam eder.
## Bir hedef başlatma
1. Yazma alanındaki hedef düğmesine basın. Işık yanar. Goal modu hazırdır.
2. Promptunuzu yazın ve gönderin. Bu mesaj hedefin amacı olur.
Bu, hem mevcut bir oturumda hem de yeni oturum taslağında çalışır. Hedefi etkinleştirin, ilk mesajı yazın, gönderin. Yeni oturum hedef zaten açık olarak başlar.
### Hedef başlatmanın başka yolları
- **Bir agent yanıtından**: "Bu yanıttan yeni oturum başlat" iletişim kutusunda **Run as goal** seçin. Yanıt, yeni oturumun tamamlaması gereken bir görev olarak devredilir. İzole bir çalışma için **Create worktree** ile birleştirebilirsiniz.
- **Bir plandan**: Kaydedilmiş bir planı yeni bir oturumda ya da worktree'de uygularsınız, iletişim kutusunda **Run as goal** seçin. Hedef, plan içeriğini amacınız olarak taşır, böylece denetçi ilerlemeyi gerçek plana göre değerlendirir.
- **Bir zamanlamadan**: Tekrarlayan çalıştırmaların promptlarını tamamlamasını istiyorsanız bir [scheduled task](/scheduled-tasks/) üzerinde **Run as goal** seçin.
## Bağımsız bir amaç yazın
İlerleme denetçisi yalnızca amacınızı ve agent'ın en son yanıtını görür. Sohbet geçmişini görmez. Bu yüzden hedef mesajını, geçmişi bilmeyen birinin bitmiş durumun nasıl göründüğünü anlayacağı şekilde yazın.
- İyi: "Export modülü için testler ekle ve tüm test paketini geçerli hale getir."
- Pek iyi değil: "Bunu düzelt" ya da "O fikre devam et."
Küçük ve bağlamlı devamlar için hedefe gerek yok. Normal bir mesaj gönderin.
## Nasıl çalışır
Agent durup oturum bir süre sessiz kaldıktan sonra OpenChamber şunları yapar:
1. Küçük ve ucuz bir modele son turu amaçla karşılaştırmasını söyler. Devam et, bitti ya da takıldı mı?
2. Karar "devam et" ise bir devam promptu gönderir ve agent işi yeniden alır.
3. Amaç doğrulanabilir biçimde sağlandıysa hedef tamamlanır ve bir bildirim alırsınız.
4. Agent gerçekten takıldıysa, yani sizden girdi gerekiyorsa, hedef engellendi olarak durur. Ama bunu yalnızca denetçi üç kez üst üste söylediğinde yapar. Tek seferlik bir aksaklık hedefi bitirmez.
Sert güvenlik durakları da vardır. İsteğe bağlı token bütçesi, otomatik devam sayısı sınırı ve tur hatasında durma. Oturumun bağlamı çalışma sırasında kısaltılırsa hedef devam eder. Bağlam penceresine çarpmak, işin bitmediğinin kanıtıdır.
### Durdurma ve sürdürme
- **Stop** düğmesi çalışan turu iptal eder ve hedefi duraklatır. Sizin açık "dur" komutunuz döngüyü her zaman yener.
- Hedef şeridindeki **Pause** de aynı şeyi öteki yönden yapar. Hedefi duraklatır ve çalışan turu durdurur.
- Duraklatılmış haldeyken normal sohbet edebilirsiniz. Döngü araya girmez.
- **Resume** döngüyü yeniden kurar. Oturan bir oturumda devam dürtüsü hemen gönderilir. Agent o sırada çalışıyorsa döngü bir sonraki duraklamasında sessizce yeniden bağlanır.
## İzleme ve yönetme
- Yazma alanının üstündeki şerit, hedefin son ilerleme notunu, durumunu ve token kullanımını gösterir. İçinde bir duraklatma/sürdürme düğmesi de vardır. Agent durmuşsa ve hedef etkinse şerit dönen bir **Evaluating…** gösterir. Bu sessiz pencere ve denetim çalışıyordur.
- Hedef düğmesi hedef çalışırken mavi, tamamlandığında yeşil, engellendiğinde ya da bütçe bittiğinde kırmızı kalır. Hedef iletişim kutusunu açmak için basın. Amaçı ya da bütçeyi düzenleyin, ya da hedefi kaldırın. Tamamlanmış bir hedef salt okunurdur. Önce kaldırın, sonra yenisini etkinleştirin.
- Oturum kenar çubuğunda, oturum tarihinin yanında küçük bir hedef simgesi görünür. Rengi hedefin durumuna göre değişir.
## Bildirimler
Bir hedef etkinken tur sonu "agent hazır" bildirimleri bastırılır. Bunlar yalnızca hedef döngüsünün kendi devamlarını tekrar eder. Hedef yerleştiğinde, yani tamamlandığında, engellendiğinde ya da bütçeye ulaşıldığında bunun yerine tek bir son bildirim alırsınız. Bu bildirim masaüstünde ve mobil itmede gelir. Aynı "tamamlanınca bildir" ayarına uyar. İzin istemleri, sorular ve hata bildirimleri boyunca normal şekilde çalışmaya devam eder.
## Token bütçesi
**Ayarlar → Chat → Goal** bölümünde yeni hedefler için varsayılan bir token bütçesi ayarlayabilirsiniz. Bir hedef bütçesine ulaşırsa daha fazla harcamak yerine "budget reached" olarak durur. Bütçeyi yükseltip hedef iletişim kutusundan sürdürmeye devam edebilirsiniz.
## Aklınızda bulunsun
- Hedef döngüsü tarayıcı sekmesinde değil, OpenChamber sunucusunda çalışır. Sekmeyi kapatsanız da telefonu kilitleseniz de agent çalışmaya devam eder ve hedef yerleştiğinde bildirim alırsınız. Sunucu, masaüstü uygulaması ya da `openchamber` süreci, açık kalmalıdır.
- Hedefler, denetçi çağrıları dahil, oturumunuzun kendi sağlayıcısını ve modelini kullanır. Kullanmakta olduğunuz sağlayıcıların dışına hiçbir şey çıkmaz.
- Her oturumda aynı anda bir hedef olabilir.
## İlgili
- [Zamanlanmış görevler](/scheduled-tasks/) — bir promptu zamanlamaya göre çalıştırın; orada "Run as goal" açarsanız zamanlı çalıştırma promptu tamamlamaya kadar götürür
- [Bildirimler](/notifications/) — tamamlanmış bir hedefi nasıl duyacağınız
@@ -0,0 +1,27 @@
---
title: Yetenekler kataloğu
description: Hazır yeteneklere göz atın ve kurun.
---
# Yetenekler kataloğu
Yetenekler Kataloğu, başkalarının yayımladığı yetenekleri kurmanızı sağlar. Kendiniz yazmanız gerekmez. **Ayarlar → Skills → Catalog** yolundan açın.
Kendi yeteneklerinizi yazmak için [Skills](/skills/) sayfasına bakın.
## Yetenek kurma
1. Kataloğu açın.
2. Anthropic skills deposu gibi yerleşik kaynaklara göz atın ya da arama yapın.
3. Bir yetenek seçin ve kurun.
4. Nereye kurulacağını seçin. Her yaptığınız şey için mi, yoksa yalnızca geçerli proje için mi.
Aynı adlı bir yetenek zaten varsa OpenChamber ne yapacağını sorar. Atla, üzerine yaz ya da her yetenek için ayrı karar ver.
## Kendi kaynağınızı ekleme
Herhangi bir Git deposunu kaynak olarak, `owner/repo` adıyla ya da tam bir Git URL'iyle ekleyebilirsiniz. Özel depolar için makinenizde erişim kurulmuş olmalı. Bu bir SSH anahtarı ya da kayıtlı kimlik bilgileri olabilir. Bir kaynak kimlik doğrulaması yapamazsa katalog bunu söyler, sessizce başarısız olmaz.
## İlgili
- [Skills](/skills/) — kurulu yetenekleri oluşturun ve yönetin
+30
View File
@@ -0,0 +1,30 @@
---
title: Yetenekler
description: Agent'ların gerektiğinde yüklediği yeniden kullanılabilir talimatlar oluşturun.
---
# Yetenekler
Bir yetenek, bir agent'ın alakalı olduğunda içeri alabileceği yeniden kullanılabilir bir talimat kümesidir. Örneğin "commit mesajlarını nasıl yazdığımız" ya da "API kurallarımız" gibi. Bunları **Ayarlar → Skills** bölümünde yönetirsiniz.
Hazır yetenekleri yazmak yerine kurmak için [Skills Catalog](/skills-catalog/) sayfasına bakın.
## Yetenek oluşturma
1. **Ayarlar → Skills** bölümünü açın.
2. Bir yetenek oluşturun ve ona bir ad ile kısa bir açıklama verin. Açıklama, agent'ın yeteneğin ne zaman uygulanacağını anlamasının yoludur. Bu yüzden net olsun.
3. Talimatları yazın. Yetenek bunlara ihtiyaç duyuyorsa destek dosyaları ekleyin.
4. Nerede yaşayacağını seçin:
- **personal** — her projede kullanılabilir
- **project** — yalnızca geçerli projede kullanılabilir
## Sohbette bir yetenek kullanma
Bir mesajın ortasında `/` yazın, yetenek seçicisi açılır. Sonra birini seçin. Agent o yanıt için bu yeteneğin talimatlarını yükler.
Mesajın en başındaki `/` ise bunun yerine [commands](/commands-snippets/) bölümünü açar.
## İlgili
- [Skills Catalog](/skills-catalog/) — başkalarının yayımladığı yetenekleri kurun
- [Commands & Snippets](/commands-snippets/) — sohbette metni yeniden kullanmanın başka yolları
@@ -0,0 +1,48 @@
---
title: SSH ana bilgisayarları ve proxyleme
description: Kaydedilmiş SSH ana bilgisayarlarını içe aktarın ve masaüstü uygulamasında ek SSH port yönlendirmeleri ekleyin.
---
# SSH ana bilgisayarları ve proxyleme
Kaydedilmiş SSH ana bilgisayarlarını içe aktarmak, uzak bir makineye bağlanmak ve aynı SSH bağlantısı üzerinden ek portları erişilebilir yapmak için masaüstü uygulamasında **Ayarlar → Uzak örnekler** bölümünü kullanın.
> SSH ana bilgisayarları ve SSH proxyleme **yalnızca masaüstü** özellikleridir. Bilgisayarınızdaki SSH istemcisini kullanırlar.
## SSH ana bilgisayarını içe aktarma
OpenChamber, yerel SSH yapılandırmanızdaki host'ları okuyabilir. Bu, `ssh work-server` gibi komutların kullandığı yerdir.
1. **Ayarlar → Uzak örnekler** bölümünü açın.
2. **Saved SSH hosts** altından bir host seçin.
3. Host bir kalıpsa gerçek hedefi girin, örneğin `deploy@app.example.com`.
4. Kaydedin ve bağlanın.
OpenChamber o SSH komutunu kullanarak bir uzak örnek oluşturur. **ready** noktasına geldiğinde uzak OpenChamber arayüzü masaüstü uygulamasında açılır.
## Ek port yönlendirmeleri ekleme
Her uzak örneğin bir **Port Forwards** bölümü vardır. SSH bağlantısının bir tarafındaki bir şeyin diğer taraftaki bir porta ulaşması gerektiğinde kullanın.
OpenChamber üç yönlendirme tipini destekler:
- **Local (-L)** — bilgisayarınızda, uzak makinedeki bir şeye bağlanan bir port açar.
- **Remote (-R)** — uzak makinede, bilgisayarınıza geri bağlanan bir port açar.
- **Dynamic (-D)** — SSH bağlantısı üzerinden yerel bir SOCKS proxy açar.
Uzak makinede çalışan çoğu uygulama önizlemesi ve panel için **Local (-L)** kullanın.
## SOCKS proxy'yi kullanma
Bilgisayarınızdaki diğer araçların uzak makine üzerinden gezmesini istiyorsanız **Dynamic (-D)** seçin. OpenChamber, o SSH bağlantısı için yerel bir SOCKS proxy portu açar.
Bağlantı hazır olduktan sonra yönlendirme satırındaki yerel proxy adresini kopyalayın ya da kullanın. Tarayıcınızı ya da aracınızı SOCKS5 proxy olarak onu kullanacak şekilde ayarlayın.
## Gizli tutun
Yönlendirilen porta ağınızdaki başka cihazlar bilerek ulaşsın istemiyorsanız yerel bağlama adresi olarak `127.0.0.1` ya da `localhost` kullanın.
## İlgili
- [Uzak örnekler](/remote-instances/) — masaüstü uygulamasını başka bir makinedeki OpenChamber'a bağlayın
- [Uzak erişim](/troubleshooting/remote-access/) — SSH ya da uzak erişim bağlanmadığında
+30
View File
@@ -0,0 +1,30 @@
---
title: Temalar
description: OpenChamber'ı yerleşik ve kullanıcı tanımlı temalarla özelleştirin.
---
# Temalar
OpenChamber yerleşik temaları ve özel tema JSON dosyalarını destekler.
## Özel tema ekleme
1. Tema dizinini oluşturun:
```bash
mkdir -p ~/.config/openchamber/themes
```
2. JSON dosyanızı bu dizine ekleyin, örneğin `my-theme.json`.
3. OpenChamber'ı açın, sonra **Settings -> Theme -> Reload themes** yoluna gidin.
4. Temanızı açılır menüden seçin. Hemen uygulanır.
## Tema konumu
- macOS/Linux: `~/.config/openchamber/themes/`
## Tam JSON biçimi başvurusu
Ana depo dokümanlarındaki tam biçim kılavuzunu kullanın:
- [`docs/CUSTOM_THEMES.md`](https://github.com/openchamber/openchamber/blob/main/docs/CUSTOM_THEMES.md)
@@ -0,0 +1,40 @@
---
title: Sorun giderme
description: Hızlı çözümlerle sık görülen kurulum ve çalışma zamanı sorunları.
---
# Sorun giderme
Bir aksilik mi oldu? Aşağıda belirtiyi bulun ve çözümü deneyin.
## OpenChamber komutu kapanıyor ya da başlamıyor
- Node.js sürümünün `>=22` olduğunu doğrulayın
- `openchamber --version` çalıştırın
- Gerekirse en son CLI'yı yeniden kurun
## Web arayüzüne ulaşılamıyor
- Sunucu günlüklerini `openchamber logs` ile kontrol edin
- Aktif portu doğrulayın. Varsayılan `3000`
- Tunnel bağlantılarını test etmeden önce doğrudan `http://localhost:3000` açın
## Uzak/tunnel bağlantısı çalışmıyor
- `openchamber tunnel status --all` çalıştırın
- Tunnel'ı aynı örnek ve porttan yeniden başlatın
- Önceki token zaten kullanıldıysa bağlantı bağlantısını yeniden üretin
Tam kurulum için [Tunnels](/tunnels/) sayfasına, bir reverse proxy kullanıyorsanız da [Reverse Proxy](/reverse-proxy/) sayfasına bakın.
## VS Code uzantısı bağlanmıyor
- OpenChamber sunucusunun çalıştığını doğrulayın
- Uzantının güncel olduğunu kontrol edin
- VS Code penceresini yeniden yükleyin ve bağlantıyı tekrar deneyin
## İlgili
- [Quickstart](/quickstart/)
- [Tunnels](/tunnels/)
- [Reverse Proxy](/reverse-proxy/)
@@ -0,0 +1,34 @@
---
title: OpenCode Bağlantısı
description: OpenChamber'ın OpenCode sunucusuna bağlanmamasını düzeltin.
---
# OpenCode Bağlantısı
OpenChamber açılıyor ama "OpenCode is restarting" aşamasını geçmiyorsa ya da sohbet yanıt vermiyorsa, konuştuğu sunucuya ulaşılamıyordur. Şunları sırayla deneyin.
## "OpenCode is restarting" ekranında takılı kalıyorsa
- başlatmadan hemen sonra biraz bekleyin. Sunucu açılırken bu durum normaldir
- `openchamber status` ile sunucunun canlı olup olmadığını kontrol edin
- `openchamber restart` ile yeniden başlatın
- `openchamber logs` ile başlangıç ayrıntılarını görün
## Kendi sunucunuza bağlanıyorsanız
OpenChamber'ı var olan bir sunucu kullanacak şekilde ayarladıysanız, [OpenCode Server](/opencode-server/) bölümündeki kurulumu yeniden kontrol edin:
- `OPENCODE_HOST`, portu içermeli ve yol içermemeli. Örneğin `http://localhost:4096`
- OpenChamber'ın kendi sunucusunu da başlatmaması için `OPENCODE_SKIP_START=true` ayarlayın
- adres geçersizse OpenChamber onu yok sayar ve onun yerine kendi sunucusunu başlatır. Loglarda bir `[config]` uyarısı arayın
## Hâlâ olmuyorsa
- Node.js sürümünün `20` veya daha yeni olduğunu doğrulayın
- en son CLI'yi yeniden yükleyin
- herhangi bir tunnel veya uzak bağlantıyı denemeden önce `http://localhost:3000` adresini doğrudan açın
## İlgili
- [OpenCode Server](/opencode-server/) — OpenChamber'ın sunucuyu nasıl bulup yönettiği
- [Troubleshooting](/troubleshooting/) — diğer yaygın sorunlar
@@ -0,0 +1,47 @@
---
title: Uzaktan Erişim
description: Tünelleri, uzak instance'ları ve OpenChamber'a başka bir cihazdan erişmeyi düzeltin.
---
# Uzaktan Erişim
OpenChamber'a telefonunuzdan ya da başka bir makineden ulaşamıyorsanız, çözüm bağlantı biçiminize göre değişir.
## Önce temel kontrolleri yapın
- önce aynı bilgisayarda `http://localhost:3000` adresini açın. Bu da başarısız olursa sorun uzak bağlantı değildir. Bkz. [OpenCode connection](/troubleshooting/opencode-connection/)
- sunucunun çalıştığını `openchamber status` ile doğrulayın
## Eşlenmiş cihaz bağlanmıyor
- QR kod / eşleştirme bağlantısı **tek kullanımlıktır**. Daha önce tarandıysa ya da süresi dolduysa, **Add a device** üzerinden yenisini oluşturun
- cihaz **Home network only** ile eşleştirildiyse o ağın dışından bağlanamaz. Tekrar **Anywhere** ile eşleştirin
- **Anywhere** eşleştirmesi için sunucudaki **Settings → Remote Instances → OpenChamber Relay** ayarını kontrol edin. **Connected** yazmalı. Değilse devre dışı bırakıp yeniden etkinleştirin
- bir cihaz **revoked** edildiyse token kalıcı olarak silinmiştir. Yeni bir QR kod ile tekrar eşleştirin
Bu bağlantıların nasıl çalıştığı için [Connect a Device](/connect-devices/) ve [Private Relay](/private-relay/) bölümlerine bakın.
## Tunnel bağlantısı çalışmıyor
- `openchamber tunnel status --all` çalıştırın
- tüneli aynı instance ve porttan yeniden başlatın
- önceki bağlantı zaten kullanıldıysa bağlantı linkini yeniden oluşturun
Tam kurulum için [Tunnels](/tunnels/) bölümüne bakın.
## Uzak instance bağlanmıyor (desktop)
Bir [remote instance](/remote-instances/) takılırsa, OpenChamber hangi adımın başarısız olduğunu söyler:
- **auth** — SSH ya da UI parolanız reddedildi. Tekrar girin
- **install / start** — OpenChamber uzak makinede sunucuyu kuramadı ya da başlatamadı. O makinenin gereksinimlerini kontrol edin
- **forwarding** — bağlantı kuruludur ama port size ulaşmıyordur. Farklı bir local port deneyin
## Kendi sunucunuzun arkasında
OpenChamber'ı bir reverse proxy arkasına koyduysanız ve garip açılıyorsa ya da bağlanmıyorsa, [Reverse Proxy](/reverse-proxy/) bölümüne bakın.
## İlgili
- [Connect a Device](/connect-devices/) · [Private Relay](/private-relay/) · [Tunnels](/tunnels/) · [Remote Instances](/remote-instances/) · [Reverse Proxy](/reverse-proxy/)
- [Security](/security/) — UI'ı dışarı açmadan önce koruyun
@@ -0,0 +1,35 @@
---
title: Worktree'ler ve Git
description: Yaygın worktree ve git sorunlarını düzeltin.
---
# Worktree'ler ve Git
[worktree sessions](/worktrees/) ve [git view](/git/) ile ortaya çıkan sorunlar ve bunları nasıl temizleyeceğiniz.
## Bir worktree'nin bakıma ihtiyacı var
OpenChamber, bir şey yolunda olmadığında worktree'yi işaretler:
- **folder missing** — worktree'nin klasörü OpenChamber dışında silinmiş ya da taşınmış. Oturumu kaldırın ve yeni bir worktree oluşturun
- **detached or unborn branch** — worktree normal bir branch üzerinde değil. Onu bir branch'e checkout edin
- **merge, rebase, or cherry-pick in progress** — bir işlem yarım kalmış. Git görünümünden bitirin ya da iptal edin
## Worktree oluşturulamıyor
- **branch already exists** — başka bir branch adı seçin ya da mevcut branch seçeneğini kullanın
- **name already in use** — başka bir worktree adı seçin
## Commit veya PR üretimi başarısız oluyor
Commit mesajı ya da PR açıklaması üretmek, aktif oturumunuz içinde çalışır. Bu yüzden açık bir oturum ve seçili çalışan bir model gerekir. Bir oturum açın ya da seçin ve tekrar deneyin.
## SSH veya Windows yol sorunları
- repo'nun kullandığı SSH anahtarının, [git identity](/git-identities/) içinde ayarladığınız anahtar olduğundan emin olun
- Windows'ta git Unix tarzı yollar kullanır. Örneğin `/c/Users/...`. OpenChamber bunu ele alır, ama özel SSH anahtarı yolları da aynı biçimi izlemelidir
## İlgili
- [Worktree Sessions](/worktrees/) — worktree'lerin nasıl oluşturulup kaldırıldığı
- [Git Identities](/git-identities/) — her repo için doğru anahtarı ve kimliği ayarlayın
+120
View File
@@ -0,0 +1,120 @@
---
title: Tüneller
description: Uzak ve mobil erişim için OpenChamber'ı güvenli biçimde açığa çıkarın.
---
# Tüneller
Bir tünel, OpenChamber'ınıza giden genel bir bağlantıdır. Böylece başka bir ağdaki normal bir tarayıcıdan erişebilirsiniz. Çalışan bir örnek için bir tane oluşturmak üzere `openchamber tunnel` kullanın.
> **Kendi cihazlarınızı** bağlamak, yani mobil uygulama ya da başka bir masaüstü, genelde tünel gerektirmez. Bunun yerine [eşleştirin](/connect-devices/) ve ev dışı erişimi sıfır kurulumla uçtan uca şifreli [Private Relay](/private-relay/) üzerinden bırakın.
## Ön koşullar
OpenChamber, tünel sağlayıcısının CLI'sını sizin makinenizde başlatır. Önce kullanmak istediğiniz sağlayıcıyı kurun:
```bash
brew install cloudflared
brew install ngrok
```
Cloudflare quick tüneller `cloudflared` ile çalışabilir. Ngrok için bir ngrok hesabı ve ngrok panelinden alınmış bir authtoken gerekir:
```bash
ngrok config add-authtoken <your-ngrok-token>
```
## Hızlı başlangıç
1. OpenChamber'ı başlatın:
```bash
openchamber
```
Bu adımı atlarsanız `openchamber tunnel start` bir CLI sunucusunu otomatik başlatabilir. Otomatik başlatırken `--port`, `--host`, `--lan`, `--ui-password` ve `--api-only` gibi sunucu seçeneklerini verebilirsiniz.
2. Cloudflare tüneli başlatın:
```bash
openchamber tunnel start --provider cloudflare --mode quick
```
Ya da bir Ngrok tüneli başlatın:
```bash
openchamber tunnel start --provider ngrok --mode quick
```
3. Durumu kontrol edin:
```bash
openchamber tunnel status
```
Tünel ayaktayken `status` bir genel URL gösterir. Onu açın ya da QR kodunu tarayın, OpenChamber'a her yerden ulaşın.
Varsayılan olarak OpenChamber, etkileşimli TTY oturumlarında bir QR kodu basar. QR çıktısını zorlamak için `--qr`, kapatmak için `--no-qr` kullanın.
## Sağlayıcılar
- `cloudflare`: quick, managed remote ve managed local modları
- `ngrok`: quick modu
## Managed modlar
### Managed remote
Cloudflare tarafından yönetilen token + hostname kullanın:
```bash
openchamber tunnel start --provider cloudflare --mode managed-remote --token-file ~/.secrets/cf-token --hostname app.example.com
```
### Managed local
Yerel bir `cloudflared` yapılandırması kullanın:
```bash
openchamber tunnel start --provider cloudflare --mode managed-local --config ~/.cloudflared/config.yml
```
## Profiller (managed-remote)
Yeniden kullanılabilir bir profil kaydedin:
```bash
openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token-file ~/.secrets/cf-token
```
Kaydedilmiş profili kullanarak başlatın:
```bash
openchamber tunnel start --profile prod-main
```
## Faydalı komutlar
```bash
openchamber tunnel providers
openchamber tunnel ready --provider cloudflare
openchamber tunnel ready --provider ngrok
openchamber tunnel doctor --provider cloudflare
openchamber tunnel doctor --provider ngrok
openchamber tunnel stop --port 3000
```
## Davranış notları
- OpenChamber örneği başına bir aktif tünel, yani port
- Aynı örnekte yeni bir mod ya da sağlayıcı başlatmak önceki tüneli değiştirir
- Yeni bir bağlantı bağlantısı oluşturmak, kullanılmamış önceki bağlantıyı iptal eder
- Tünel otomatik başlatma, `--ui-password` ve `--api-only` gibi sunucu bayraklarını yeniden başlatma/güncelleme akışlarında kullanılan örnek ayarlarında korur
## İlgili
- [Cihaz bağlama](/connect-devices/) — kendi cihazlarınızı genel URL olmadan eşleştirin
- [Güvenlik](/security/) — açığa çıkarmadan önce arayüzü koruyun
- [Desktop Tunnels](/desktop-tunnels/) — CLI başlatmadan masaüstü uygulaması tünel kurulumu
- [PWA & Mobile](/mobile/) — telefonunuzdan OpenChamber'a ulaşın
- [Sorun giderme](/troubleshooting/) — tünel bağlantısı çalışmıyorsa
+31
View File
@@ -0,0 +1,31 @@
---
title: Güncellemeler
description: OpenChamber'ı masaüstünde, web'de ve VS Code'da güncel tutun.
---
# Güncellemeler
OpenChamber'ı nasıl güncelleyeceğiniz, onu nasıl kurduğunuza bağlıdır. Her durumda geçerli sürümünüzü **Ayarlar → OpenChamber → About** bölümünden görebilirsiniz.
## Masaüstü uygulaması
Masaüstü uygulaması güncellemeleri GitHub sürümlerinden kontrol eder. Bir güncelleme varsa OpenChamber bunu söyler, siz seçince indirir ve bir sonraki yeniden başlatmada kurar. Kontrol sizdedir. Hiçbir şey sizin onayınız olmadan kurulmaz.
## Web / CLI
CLI kurduysanız güncelleme işlemini **About** içindeki kontrol ve güncelle düğmeleriyle ya da terminalden yapabilirsiniz:
```bash
openchamber update
```
OpenChamber nasıl kurulduğunu algılar. npm, pnpm, yarn ya da bun olabilir. Sizin için doğru güncellemeyi çalıştırır.
## OpenCode sunucusu
OpenChamber ve OpenCode ayrı ayrı güncellenir. Yeni bir OpenCode sürümü varsa OpenChamber onu güncellemenizi önerir ve ardından sunucuyu yeniden başlatır. O sunucunun nasıl yönetildiği için [OpenCode Server](/opencode-server/) sayfasına bakın.
## İlgili
- [Install](/install/) — her uygulamanın ilk kurulum biçimi
- [OpenCode Server](/opencode-server/) — alttaki sunucuyu güncelleme
+28
View File
@@ -0,0 +1,28 @@
---
title: Kullanım ve kotalar
description: Sağlayıcı planınızdan ne kadar kullandığınızı takip edin.
---
# Kullanım ve kotalar
Kullanım sayfası, her sağlayıcının planından ne kadar kullandığınızı gösterir. Böylece sınıra ne kadar yaklaştığınızı görürsünüz. **Ayarlar → Usage** bölümünden açın.
## Ne görürsünüz
Bağlı her sağlayıcı için OpenChamber şunları gösterir:
- geçerli pencerede ne kadar kullandığınız, çubuk olarak
- modele göre döküm
- bir tempo göstergesi, böylece kotanın biteceği yolda olup olmadığınızı anlarsınız
Hangi sağlayıcıların görüneceğini seçebilirsiniz. Aynı özet uygulama başlığındaki bir açılır menüden de alınabilir.
## Desteklenen sağlayıcılar
Kullanım, kota yayımlayan sağlayıcılarda çalışır. Bunlar arasında Claude, Codex, GitHub Copilot, Google, OpenRouter, Kimi, NanoGPT, z.ai, Zhipu, MiniMax, Ollama Cloud ve Wafer bulunur.
Bir sağlayıcı yalnızca [Providers](/providers/) sayfasında oturum açtıktan sonra kullanım verisi gösterir. Bazı sağlayıcılar ek bir adım ister. Örneğin Ollama Cloud, ayrı kurduğunuz bir oturum dosyasını okur. Bir sağlayıcı hiç veri göstermiyorsa genelde eksik olan ek kimlik bilgisidir.
## İlgili
- [Providers, Models & Agents](/providers/) — kullanım görünmeden önce oturum açın
+36
View File
@@ -0,0 +1,36 @@
---
title: Ses Modu
description: OpenChamber ile konuşun ve yanıtları sesli dinleyin.
---
# Ses Modu
Ses modu, mesajları dikte etmenizi ve yanıtları size geri okutmanızı sağlar. Bunu **Settings → OpenChamber → Voice** bölümünden açın.
## Yanıtları sesli okutma (text-to-speech)
Yanıtların nasıl seslendirileceğini seçin:
- **browser** — tarayıcınızın yerleşik sesleri, kurulum gerekmez
- **OpenAI** — OpenAI'nın sesleri; API anahtarınızı yapıştırın ve bir ses seçin
- **OpenAI-compatible** — OpenAI biçimini konuşan herhangi bir servis; URL'sini ve gerekiyorsa API anahtarını girin
- **macOS say** — yerleşik `say` komutu, bunu destekleyen Mac'lerde
Açtıktan sonra, mesajların üzerinde onları sesli dinlemek için bir oynat düğmesi görünür.
## Mesaj dikte etme (speech-to-text)
Konuşmanızın nasıl metne dönüştürüleceğini seçin:
- **browser** — tarayıcınızın yerleşik tanıma özelliği, kurulum gerekmez
- **server** — OpenAI uyumlu bir transkripsiyon servisi; URL'sini ve gerekirse API anahtarını girin
- **on-device** — tarayıcınızda çalışan ve ilk kullanımda indirilen bir konuşma modeli
## Telefonlar için not
Bir telefonda yanıtları sesli okutmak için en güvenilir seçenekler OpenAI veya OpenAI-compatible olanlardır. Mobil tarayıcılar yerleşik sesleri sınırlar.
## İlgili
- [Notifications](/notifications/) — dinlemek yerine bildirim alın
- [Providers, Models & Agents](/providers/) — OpenAI anahtarınızın zaten olabileceği yer
@@ -0,0 +1,85 @@
---
title: Değişiklik İncelemesi
description: Bir diff'i alfabetik sıraya göre değil, anlamlı olduğu sırayla okuyun.
---
# Değişiklik İncelemesi
Diff dosya yoluna göre sıralanır, ama değişikliğin anlamlı olduğu sıra neredeyse hiç öyle değildir. Walkthrough bunu yeniden sıralar. İlgili düzenlemeler **duraklar** halinde gruplanır. Her durak, kodun artık neyi farklı yaptığını açıklar ve duraklar da her biri bir öncekine dayanacak şekilde dizilir.
Açıklar ve sıralar. Kodunuzu yargılamaz, karar vermez. Bunun için [Review](/git/) vardır.
Bunu sağ kenar şeridindeki **Walkthrough** simgesinden ya da Changes ve Pull Request panellerindeki **AI walkthrough** düğmesinden açın. İkisi de yalnızca paneli açar. **Generate walkthrough** düğmesine basana kadar hiçbir şey üretilmez.
## Bir durak nasıl işaretlenir
Her durak neyle ilgili olduğunu söyler, bunu bir iki cümleyle açıklar ve sonra anlattığı kodu doğrudan gösterir. Bazı durakların başlığının yanında küçük bir etiket olur:
| Etiket | Anlamı |
| --- | --- |
| **Key change** | Bu durak değişikliğin geri kalanını sürükler ya da riskin çoğunu taşır. Yakından okuyun ve önce bunu okuyun. |
| **Context** | Geri kalan kısmın anlaşılması için eklenmiş destekleyici bir değişiklik. Hızla göz atılabilir. |
| *(etiket yok)* | Okuma sırasındaki sıradan bir adım. |
Etiket, **dikkatinizi nereye vereceğinizi** söyler. Kodun kalitesiyle ilgili değildir. Bir durak, içinde bir şey yanlış bulunduğu için işaretlenmez. Walkthrough hiçbir bulgu, şiddet seviyesi ya da karar vermez. Kodu yargılatmak istiyorsanız, bu [Git & GitHub](/git/) içindeki **Review** işlemidir.
Problem bildiren tek işaretler **Outdated** ve **Not covered**dır. İkisi de kodunuzdan değil, walkthrough'un kendisinin eski kalmasından söz eder. Aşağıya bakın.
## Neleri inceleyebilir
| Kapsam | Neleri kapsar |
| --- | --- |
| All uncommitted | Henüz commit edilmemiş her şey. Stage edilmiş, stage edilmemiş ve yeni dosyalar. |
| Staged | Şu anda commit'e girecek olanlar. |
| Unstaged | Working tree ve yeni dosyalar. |
| This branch | Bu branch'in base'inde olmayan tüm commit'leri. |
| Pull request | GitHub'daki haliyle değişiklik. |
**This branch**, "push edilmemiş commit'ler" demek değildir. Branch'in base'ine eklediği her şeydir, push edilmiş olsun olmasın. Bu yüzden commit ettikten ama push etmeden önce, bu görünüm ile pull request bilerek farklı olur. Biri yaptığınızı, diğeri inceleyenlerin şu anda gördüğünü gösterir.
Her kapsam ayrı saklanır. Aralarında geçiş yapmak hiçbir şeyi kaybettirmez.
## Model seçimi
Walkthrough'lar varsayılan olarak küçük modelinizi kullanır. Başka bir model seçmek için **Settings → Sessions → Changes Walkthrough Model** bölümüne gidin ya da tek bir inceleme için panel başlığındaki seçiciyi kullanın. Değişiklik yeterince riskliyse daha güçlü bir model kullanmak işe yarar.
Seçici yalnızca yapılandırılmış çıktı verebilen modelleri gösterir. Walkthrough bunun olmadan kurulamaz. Model diff için fazla küçükse, üretim sessizce kısaltılmak yerine açıklamayla reddedilir. Diff'in yarısına bakarak yazılmış bir walkthrough kendinden emin görünür ama yanlıştır.
Paneli yeniden açtığınızda, gördüğünüz çıktıyı üreten model görünür. Bu yüzden **Regenerate**, siz değiştirmedikçe aynı modelle tekrar eder.
## Dil seçimi
Walkthrough'lar varsayılan olarak arayüz dilinizde yazılır. Panel başlığındaki dil seçici buradan başlar. OpenChamber'ın çevrildiği başka bir dili tek bir inceleme için seçebilirsiniz. Rehberli açıklama, rahat okuduğunuz bir dildeyse faydalıdır.
Yalnızca düz metin çevrilir. Tanımlayıcılar, dosya yolları ve API adları kodda göründükleri gibi kalır. Böylece bir durak neyi adlandırıyorsa onu yine arayıp bulabilirsiniz.
Seçtiğiniz dilde henüz bir şey üretilmemişse, panel elindeki walkthrough'u göstermeyi sürdürür ve bunu söyler. Yeni dilde bir tane almak için **Generate walkthrough** düğmesine basın.
## Maliyet ve önbellek
Kendi kendine hiçbir şey üretilmez. Üretim yalnızca siz istediğinizde başlar. Yeniden üretim de elle yapılır.
Sonuçlar, diff'in tam içeriğine göre önbelleğe alınır. Working tree'yi eski bir duruma döndürün, eski walkthrough model çağrısı olmadan geri gelir. Dil ve model de bu anahtarın parçasıdır. Her birleşim ayrı tutulur. Bir diff'in iki dilde walkthrough'u varsa, aralarında geçiş anlık olur ve maliyeti yoktur.
Üretim tarayıcı sekmenizde değil, OpenChamber sunucusunda çalışır. Sayfayı yenileyin ya da paneli kapatın, işlem sürer. Geri döndüğünüzde sonuç hazırdır. Onu durduran tek şey **Cancel** düğmesidir.
## Eskime durumunu dürüstçe gösterme
Her durak, anlattığı kodun tam içeriğine bağlıdır. Bu yüzden panel, kod o zamandan beri değiştiyse size bunu söyleyebilir:
- **Outdated steps** — bir durakta anlatılan kod değişmiş ya da silinmiştir. Walkthrough yine gösterilir, ama işaretlenir. Böylece yeniden üretip üretmemeye siz karar verirsiniz.
- **Not covered** — mevcut diff'te olup da hiçbir durak tarafından anlatılmayan değişiklikler. Buna, üretimden sonra yapılan düzenlemeler, walkthrough'un sıradan saydığı değişiklikler ve model girdisinden bilerek çıkarılan lockfile'lar ile diğer üretilmiş dosyalar dahildir. Hiçbir şey sessizce kaybolmasın diye bunların hepsi akışın sonunda listelenir.
Yeniden üretmek, yamalamak yerine yeniden yazmak demektir. Önceki walkthrough bağlam olarak modele verilir, böylece doğru parçalar korunur ve her şey güncel koda yeniden bağlanır.
## Notlar
- Walkthrough'daki herhangi bir satıra diff görünümündekiyle aynı şekilde yorum yapın. Yorumlar chat composer'a eklenir.
- Desktop ve tablet genişliklerinde kullanılabilir. VS Code eklentisinde ya da mobil uygulamada sunulmaz.
- Pull request incelemesi için bağlı bir GitHub hesabı gerekir. Bkz. [GitHub Issues & PRs](/github/).
## İlgili
- [Git & GitHub](/git/) — bunu okuyan Changes paneli ve kodu yargılayan Review işlemi
- [GitHub Issues & PRs](/github/) — pull request'leri incelemek için GitHub'ı bağlayın
- [Providers, Models & Agents](/providers/) — küçük modelin geldiği yer
@@ -0,0 +1,36 @@
---
title: Worktree Oturumları
description: Bir oturuma ayrı branch ve klasör verin, çalışma izole kalsın.
---
# Worktree Oturumları
Bir worktree oturumu, repo'nuzun kendi checkout edilmiş kopyasında ve kendi branch'inde çalışır. Buna git worktree denir. Böylece paralel oturumlar birbirinin dosyalarına çarpmaz. Biri refactor yaparken diğeri bir bug'ı düzeltebilir, geçiş yapıp durmadan.
## Oluşturun
1. Oturum kenar çubuğunun üstündeki düğmeden yeni worktree iletişim kutusunu açın.
2. Bir başlangıç noktası seçin:
- **new branch** — branch'e ad verin ve hangi branch'ten başlanacağını seçin
- **existing branch** — zaten sahip olduğunuz bir branch'i checkout edin
3. Worktree klasörünü onaylayın. OpenChamber klasör adını branch isminden önerir.
4. Oluşturun.
OpenChamber branch'i oluşturur, klasörü hazırlar ve içinde bir oturum başlatır. Bunu bir [todo](/notes-todos-plans/) ya da [GitHub issue veya PR](/github/) üzerinden de başlatabilirsiniz.
## Çalışmayı geri alın
İş hazır olduğunda, Git görünümündeki **Integrate** seçeneğini kullanarak worktree'nin commit'lerini başka bir branch'e, örneğin `main`'e taşıyın. Bir değişiklik çakışırsa, çakışmayı çözmesi için işi agent'a verebilirsiniz.
## Temizleme
Oturumu silmek ya da arşivlemek worktree'yi kaldırabilir. Branch'i de silmek isteyip istemediğinizi siz seçersiniz. Yerelse yerel, varsa uzak depodaki branch de silinir. Siz istemeden hiçbir şey silinmez.
## Bir şey ters görünüyorsa
Klasörü kaybolduysa, branch'i detached durumdaysa ya da bir merge veya rebase yarım kaldıysa worktree'nin bakıma ihtiyacı olabilir. OpenChamber bunları işaretler, böylece düzeltebilirsiniz. Bkz. [Worktrees & Git](/troubleshooting/worktrees-git/).
## İlgili
- [Multi-run](/multi-run/) — birçok worktree oturumunu aynı anda başlatın
- [Git & GitHub Workflows](/git/) — OpenChamber içinden commit edin ve integrate edin
@@ -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
@@ -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
+106 -53
View File
@@ -11,7 +11,8 @@
"pl": "Zacznij tutaj",
"fr": "Commencer ici",
"ja": "ここから開始",
"de": "Hier starten"
"de": "Hier starten",
"tr": "Buradan başlayın"
},
"items": [
{
@@ -26,7 +27,8 @@
"pl": "Przegląd",
"fr": "Vue densemble",
"ja": "概要",
"de": "Übersicht"
"de": "Übersicht",
"tr": "Genel bakış"
}
},
{
@@ -41,7 +43,8 @@
"pl": "Instalacja",
"fr": "Installation",
"ja": "インストール",
"de": "Installation"
"de": "Installation",
"tr": "Kurulum"
}
},
{
@@ -56,7 +59,8 @@
"pl": "Szybki start",
"fr": "Démarrage rapide",
"ja": "クイックスタート",
"de": "Schnellstart"
"de": "Schnellstart",
"tr": "Hızlı başlangıç"
}
},
{
@@ -71,7 +75,8 @@
"pl": "Serwer OpenCode",
"fr": "Serveur OpenCode",
"ja": "OpenCode サーバー",
"de": "OpenCode-Server"
"de": "OpenCode-Server",
"tr": "OpenCode Sunucusu"
}
},
{
@@ -86,7 +91,8 @@
"pl": "Zmienne środowiskowe",
"fr": "Variables denvironnement",
"ja": "環境変数",
"de": "Umgebungsvariablen"
"de": "Umgebungsvariablen",
"tr": "Ortam değişkenleri"
}
}
]
@@ -102,7 +108,8 @@
"pl": "Przepływy pracy",
"fr": "Workflows",
"ja": "ワークフロー",
"de": "Abläufe"
"de": "Abläufe",
"tr": "İş akışları"
},
"items": [
{
@@ -117,7 +124,8 @@
"pl": "Projekty",
"fr": "Projets",
"ja": "プロジェクト",
"de": "Projekte"
"de": "Projekte",
"tr": "Projeler"
}
},
{
@@ -132,7 +140,8 @@
"pl": "Kontekst",
"fr": "Contexte",
"ja": "コンテキスト",
"de": "Kontext"
"de": "Kontext",
"tr": "Bağlam"
}
},
{
@@ -147,7 +156,8 @@
"pl": "Notatki, zadania i plany",
"fr": "Notes, todos et plans",
"ja": "メモ、Todo、計画",
"de": "Notizen, Aufgaben und Pläne"
"de": "Notizen, Aufgaben und Pläne",
"tr": "Notlar, yapılacaklar ve planlar"
}
},
{
@@ -162,7 +172,8 @@
"pl": "Zaplanowane zadania",
"fr": "Tâches planifiées",
"ja": "スケジュールタスク",
"de": "Geplante Aufgaben"
"de": "Geplante Aufgaben",
"tr": "Zamanlanmış görevler"
}
},
{
@@ -177,7 +188,8 @@
"pl": "Narzędzie sterowania dla agentów",
"fr": "Outil de contrôle pour les agents",
"ja": "エージェント制御ツール",
"de": "Agenten-Steuerungstool"
"de": "Agenten-Steuerungstool",
"tr": "Agent kontrol aracı"
}
},
{
@@ -192,7 +204,8 @@
"pl": "Cele sesji",
"fr": "Objectifs de session",
"ja": "セッションゴール",
"de": "Sitzungsziele"
"de": "Sitzungsziele",
"tr": "Oturum hedefleri"
}
},
{
@@ -207,7 +220,8 @@
"pl": "Akcje projektu",
"fr": "Actions de projet",
"ja": "プロジェクトアクション",
"de": "Projektaktionen"
"de": "Projektaktionen",
"tr": "Proje işlemleri"
}
},
{
@@ -222,7 +236,8 @@
"pl": "Podgląd i serwery deweloperskie",
"fr": "Aperçu et serveurs de dev",
"ja": "プレビューと開発サーバー",
"de": "Vorschau und Entwicklungsserver"
"de": "Vorschau und Entwicklungsserver",
"tr": "Önizleme ve geliştirme sunucuları"
}
},
{
@@ -237,7 +252,8 @@
"pl": "Sesje worktree",
"fr": "Sessions worktree",
"ja": "Worktree セッション",
"de": "Worktree-Sitzungen"
"de": "Worktree-Sitzungen",
"tr": "Worktree oturumları"
}
},
{
@@ -246,7 +262,8 @@
"translations": {
"fr": "Multi-run",
"ja": "Multi-run",
"de": "Mehrfachausführung"
"de": "Mehrfachausführung",
"tr": "Çoklu çalıştırma"
}
},
{
@@ -255,7 +272,8 @@
"translations": {
"fr": "Git et GitHub",
"ja": "Git と GitHub",
"de": "Git und GitHub"
"de": "Git und GitHub",
"tr": "Git ve GitHub"
}
},
{
@@ -270,7 +288,8 @@
"pl": "Przewodnik po zmianach",
"fr": "Parcours des modifications",
"ja": "変更のウォークスルー",
"de": "Änderungsrundgang"
"de": "Änderungsrundgang",
"tr": "Değişiklik incelemesi"
}
},
{
@@ -285,7 +304,8 @@
"pl": "Zgłoszenia i PR-y GitHub",
"fr": "Issues et PR GitHub",
"ja": "GitHub Issues と PR",
"de": "GitHub-Issues und PRs"
"de": "GitHub-Issues und PRs",
"tr": "GitHub issue'ları ve PR'lar"
}
},
{
@@ -300,7 +320,8 @@
"pl": "Magiczne prompty",
"fr": "Magic Prompts",
"ja": "マジックプロンプト",
"de": "Magische Prompts"
"de": "Magische Prompts",
"tr": "Sihirli istemler"
}
},
{
@@ -315,7 +336,8 @@
"pl": "Tożsamości Git",
"fr": "Identités Git",
"ja": "Git ID",
"de": "Git-Identitäten"
"de": "Git-Identitäten",
"tr": "Git kimlikleri"
}
}
]
@@ -331,7 +353,8 @@
"pl": "Konfiguracja OpenCode",
"fr": "Configuration OpenCode",
"ja": "OpenCode 設定",
"de": "OpenCode-Einrichtung"
"de": "OpenCode-Einrichtung",
"tr": "OpenCode kurulumu"
},
"items": [
{
@@ -346,7 +369,8 @@
"pl": "Dostawcy, modele i agenci",
"fr": "Fournisseurs, modèles et agents",
"ja": "プロバイダー、モデル、エージェント",
"de": "Anbieter, Modelle und Agenten"
"de": "Anbieter, Modelle und Agenten",
"tr": "Sağlayıcılar, modeller ve agentlar"
}
},
{
@@ -361,7 +385,8 @@
"pl": "Integracje",
"fr": "Intégrations",
"ja": "統合機能",
"de": "Integrationen"
"de": "Integrationen",
"tr": "Entegrasyonlar"
}
},
{
@@ -376,7 +401,8 @@
"pl": "Serwery MCP",
"fr": "Serveurs MCP",
"ja": "MCP サーバー",
"de": "MCP-Server"
"de": "MCP-Server",
"tr": "MCP sunucuları"
}
},
{
@@ -391,7 +417,8 @@
"pl": "Umiejętności",
"fr": "Skills",
"ja": "スキル",
"de": "Fähigkeiten"
"de": "Fähigkeiten",
"tr": "Beceriler"
}
},
{
@@ -406,7 +433,8 @@
"pl": "Katalog umiejętności",
"fr": "Catalogue de skills",
"ja": "スキルカタログ",
"de": "Fähigkeitenkatalog"
"de": "Fähigkeitenkatalog",
"tr": "Beceri kataloğu"
}
},
{
@@ -421,7 +449,8 @@
"pl": "Polecenia i fragmenty",
"fr": "Commandes et snippets",
"ja": "コマンドとスニペット",
"de": "Befehle und Snippets"
"de": "Befehle und Snippets",
"tr": "Komutlar ve parçacıklar"
}
},
{
@@ -436,7 +465,8 @@
"pl": "Zużycie i limity",
"fr": "Utilisation et quotas",
"ja": "使用量とクォータ",
"de": "Nutzung und Kontingente"
"de": "Nutzung und Kontingente",
"tr": "Kullanım ve kotalar"
}
}
]
@@ -452,7 +482,8 @@
"pl": "Dostęp zdalny",
"fr": "Accès distant",
"ja": "リモートアクセス",
"de": "Zugriff von außen"
"de": "Zugriff von außen",
"tr": "Uzaktan erişim"
},
"items": [
{
@@ -467,7 +498,8 @@
"pl": "Podłączanie urządzenia",
"fr": "Connecter un appareil",
"ja": "デバイスを接続",
"de": "Gerät verbinden"
"de": "Gerät verbinden",
"tr": "Cihaz bağlama"
}
},
{
@@ -482,7 +514,8 @@
"pl": "Prywatny relay",
"fr": "Relay privé",
"ja": "プライベートリレー",
"de": "Privates Relay"
"de": "Privates Relay",
"tr": "Özel relay"
}
},
{
@@ -497,7 +530,8 @@
"pl": "Tunele",
"fr": "Tunnels",
"ja": "トンネル",
"de": "Tunnel"
"de": "Tunnel",
"tr": "Tüneller"
}
},
{
@@ -512,7 +546,8 @@
"pl": "Reverse proxy",
"fr": "Reverse proxy",
"ja": "リバースプロキシ",
"de": "Reverse Proxy"
"de": "Reverse Proxy",
"tr": "Ters proxy"
}
},
{
@@ -527,7 +562,8 @@
"pl": "Aplikacje mobilne i PWA",
"fr": "Apps mobiles et PWA",
"ja": "モバイルアプリと PWA",
"de": "Mobile Apps und PWA"
"de": "Mobile Apps und PWA",
"tr": "Mobil uygulamalar ve PWA"
}
},
{
@@ -542,7 +578,8 @@
"pl": "Bezpieczeństwo",
"fr": "Sécurité",
"ja": "セキュリティ",
"de": "Sicherheit"
"de": "Sicherheit",
"tr": "Güvenlik"
}
}
]
@@ -558,7 +595,8 @@
"pl": "Dostosuj",
"fr": "Personnaliser",
"ja": "カスタマイズ",
"de": "Anpassen"
"de": "Anpassen",
"tr": "Özelleştirme"
},
"items": [
{
@@ -573,7 +611,8 @@
"pl": "Motywy",
"fr": "Thèmes",
"ja": "テーマ",
"de": "Designs"
"de": "Designs",
"tr": "Temalar"
}
},
{
@@ -588,7 +627,8 @@
"pl": "Powiadomienia",
"fr": "Notifications",
"ja": "通知",
"de": "Benachrichtigungen"
"de": "Benachrichtigungen",
"tr": "Bildirimler"
}
},
{
@@ -603,7 +643,8 @@
"pl": "Tryb głosowy",
"fr": "Mode vocal",
"ja": "音声モード",
"de": "Sprachmodus"
"de": "Sprachmodus",
"tr": "Ses modu"
}
},
{
@@ -618,7 +659,8 @@
"pl": "Ikony projektów",
"fr": "Icônes de projet",
"ja": "プロジェクトアイコン",
"de": "Projektsymbole"
"de": "Projektsymbole",
"tr": "Proje simgeleri"
}
}
]
@@ -634,7 +676,8 @@
"pl": "Pulpit",
"fr": "Desktop",
"ja": "デスクトップ",
"de": "Desktop"
"de": "Desktop",
"tr": "Masaüstü"
},
"items": [
{
@@ -649,7 +692,8 @@
"pl": "Zdalne instancje",
"fr": "Instances distantes",
"ja": "リモートインスタンス",
"de": "Entfernte Instanzen"
"de": "Entfernte Instanzen",
"tr": "Uzak örnekler"
}
},
{
@@ -664,7 +708,8 @@
"pl": "Panel przeglądarki",
"fr": "Panneau navigateur",
"ja": "ブラウザパネル",
"de": "Browser-Panel"
"de": "Browser-Panel",
"tr": "Tarayıcı paneli"
}
},
{
@@ -679,7 +724,8 @@
"pl": "Tunele w aplikacji desktopowej",
"fr": "Tunnels desktop",
"ja": "デスクトップトンネル",
"de": "Desktop-Tunnel"
"de": "Desktop-Tunnel",
"tr": "Masaüstü tünelleri"
}
},
{
@@ -694,7 +740,8 @@
"pl": "Hosty SSH i proxy",
"fr": "Hosts SSH et proxy",
"ja": "SSH ホストとプロキシ",
"de": "SSH-Hosts und Proxying"
"de": "SSH-Hosts und Proxying",
"tr": "SSH hostları ve proxy"
}
},
{
@@ -709,7 +756,8 @@
"pl": "Aktualizacje",
"fr": "Mises à jour",
"ja": "更新",
"de": "Aktualisierungen"
"de": "Aktualisierungen",
"tr": "Güncellemeler"
}
}
]
@@ -725,7 +773,8 @@
"pl": "Pomoc",
"fr": "Aide",
"ja": "ヘルプ",
"de": "Hilfe"
"de": "Hilfe",
"tr": "Yardım"
},
"items": [
{
@@ -740,7 +789,8 @@
"pl": "Rozwiązywanie problemów",
"fr": "Dépannage",
"ja": "トラブルシューティング",
"de": "Fehlerbehebung"
"de": "Fehlerbehebung",
"tr": "Sorun giderme"
}
},
{
@@ -755,7 +805,8 @@
"pl": "Połączenie z OpenCode",
"fr": "Connexion à OpenCode",
"ja": "OpenCode 接続",
"de": "OpenCode-Verbindung"
"de": "OpenCode-Verbindung",
"tr": "OpenCode bağlantısı"
}
},
{
@@ -770,7 +821,8 @@
"pl": "Worktree i Git",
"fr": "Worktrees et Git",
"ja": "Worktrees と Git",
"de": "Worktrees und Git"
"de": "Worktrees und Git",
"tr": "Worktree'ler ve Git"
}
},
{
@@ -785,7 +837,8 @@
"pl": "Dostęp zdalny",
"fr": "Accès distant",
"ja": "リモートアクセス",
"de": "Externer Zugriff"
"de": "Externer Zugriff",
"tr": "Uzaktan erişim"
}
}
]
+2
View File
@@ -98,6 +98,8 @@ 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.
+112 -15
View File
@@ -33,6 +33,7 @@ import {
} 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);
@@ -1369,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();
});
@@ -1770,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 };
};
@@ -1997,6 +2007,18 @@ const emitToAllWindows = (event, detail) => {
}
};
// 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 setTaskbarProgress = (value) => {
if (process.platform !== 'win32') return;
for (const browserWindow of BrowserWindow.getAllWindows()) {
@@ -2278,7 +2300,7 @@ const dispatchDeepLink = (link) => {
}
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) {
@@ -2635,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') {
@@ -2872,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);
}
@@ -3013,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() : '';
@@ -3112,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) });
@@ -4449,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: {},
@@ -4488,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);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@openchamber/electron",
"version": "1.20.0",
"version": "1.21.1",
"private": true,
"description": "Electron desktop runtime for OpenChamber",
"author": "OpenChamber",
+54
View File
@@ -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);
});
@@ -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>
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@openchamber/ui",
"version": "1.20.0",
"version": "1.21.1",
"private": true,
"type": "module",
"main": "src/main.tsx",
@@ -45,7 +45,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@legendapp/list": "3.3.8",
"@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "1.18.21",
"@opencode-ai/sdk": "1.18.25",
"@pierre/diffs": "1.3.0-beta.6",
"@replit/codemirror-vim": "^6.4.0",
"@simplewebauthn/browser": "13.3.0",
@@ -67,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",
+20 -20
View File
@@ -7,6 +7,7 @@ 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';
@@ -19,8 +20,8 @@ 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,
@@ -48,6 +49,7 @@ import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { useUIStore } from '@/stores/useUIStore';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
import { useLinearAuthStore } from '@/stores/useLinearAuthStore';
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import type { RuntimeAPIs } from '@/lib/api/types';
import { TooltipProvider } from '@/components/ui/tooltip';
@@ -246,6 +248,7 @@ function App({ apis }: AppProps) {
const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory);
const [showMemoryDebug, setShowMemoryDebug] = React.useState(false);
const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
const refreshLinearAuthStatus = useLinearAuthStore((state) => state.refreshStatus);
const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState<boolean>(() => apis.runtime.isVSCode);
// Embedded chats start inactive until the parent panel identifies the active
// tab. Otherwise a newly loaded background tab can focus its composer first
@@ -280,6 +283,13 @@ function App({ apis }: AppProps) {
};
}, [showMemoryDebug]);
React.useEffect(() => {
setRequestsInFlightTrackingEnabled(showMemoryDebug);
return () => {
setRequestsInFlightTrackingEnabled(false);
};
}, [showMemoryDebug]);
React.useEffect(() => {
applyMobileKeyboardMode(mobileKeyboardMode);
}, [mobileKeyboardMode]);
@@ -337,7 +347,8 @@ function App({ apis }: AppProps) {
}
void refreshGitHubAuthStatus(apis.github, { force: true });
}, [apis.github, embeddedSessionChat, refreshGitHubAuthStatus]);
void refreshLinearAuthStatus(apis.linear, { force: true });
}, [apis.github, apis.linear, embeddedSessionChat, refreshGitHubAuthStatus, refreshLinearAuthStatus]);
useAppFontEffects();
@@ -710,6 +721,8 @@ function App({ apis }: AppProps) {
useWindowTitle();
useRootScrollLock();
useRouter();
const handleToggleMemoryDebug = React.useCallback(() => {
@@ -723,25 +736,12 @@ function App({ apis }: AppProps) {
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(() => {
@@ -8,6 +8,7 @@ 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';
@@ -318,6 +319,7 @@ export function ElectronMiniChatApp({ apis }: ElectronMiniChatAppProps) {
useMiniChatKeyboardShortcuts();
usePushVisibilityBeacon({ enabled: true });
useWindowTitle();
useRootScrollLock();
return (
<ErrorBoundary>
+27 -4
View File
@@ -12,6 +12,7 @@ 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';
@@ -21,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';
@@ -33,6 +35,7 @@ import { useConfigStore } from '@/stores/useConfigStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
import { useLinearAuthStore } from '@/stores/useLinearAuthStore';
import { useGitStore } from '@/stores/useGitStore';
import { useMcpConfigStore, type McpDraft } from '@/stores/useMcpConfigStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
@@ -110,7 +113,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<{ id: 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);
@@ -541,7 +544,7 @@ const MobileShell: React.FC<{ onActiveConnectionDeleted: () => void }> = ({ onAc
>
<ErrorBoundary>
<PlanView
projectPlanId={openPlan.id}
savedProjectPlan={{ projectRef: openPlan.projectRef, planId: openPlan.id }}
onNavigatedToChat={() => {
closeSurface();
closeWorkspace();
@@ -628,6 +631,7 @@ export function MobileApp({ apis }: MobileAppProps) {
const clearError = useSessionUIStore((state) => state.clearError);
const setIsMobile = useUIStore((state) => state.setIsMobile);
const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
const refreshLinearAuthStatus = useLinearAuthStore((state) => state.refreshStatus);
const setPlanModeEnabled = useFeatureFlagsStore((state) => state.setPlanModeEnabled);
const projects = useProjectsStore((state) => state.projects);
const [connectionEpoch, setConnectionEpoch] = React.useState(0);
@@ -676,6 +680,7 @@ export function MobileApp({ apis }: MobileAppProps) {
const refreshInPlace = () => {
void initializeApp();
void refreshGitHubAuthStatus(apis.github, { force: true });
void refreshLinearAuthStatus(apis.linear, { force: true });
if (providersCount === 0) void loadProviders({ source: 'mobileApp:nativeResume' });
if (agentsCount === 0) void loadAgents({ source: 'mobileApp:nativeResume' });
};
@@ -744,7 +749,7 @@ export function MobileApp({ apis }: MobileAppProps) {
lastNativeResumeSyncEventAtRef.current = now;
window.dispatchEvent(new Event('openchamber:system-resume'));
}
}, [agentsCount, apis.github, initializeApp, loadAgents, loadProviders, providersCount, refreshGitHubAuthStatus]);
}, [agentsCount, apis.github, apis.linear, initializeApp, loadAgents, loadProviders, providersCount, refreshGitHubAuthStatus, refreshLinearAuthStatus]);
useNativeMobileChrome();
useNativeMobileLifecycle(handleNativeResume);
@@ -772,6 +777,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);
@@ -1012,7 +1034,8 @@ export function MobileApp({ apis }: MobileAppProps) {
React.useEffect(() => {
if (!isConnected) return;
void refreshGitHubAuthStatus(apis.github, { force: true });
}, [apis.github, isConnected, refreshGitHubAuthStatus]);
void refreshLinearAuthStatus(apis.linear, { force: true });
}, [apis.github, apis.linear, isConnected, refreshGitHubAuthStatus, refreshLinearAuthStatus]);
// Discover all worktrees for every known project so the draft session's
// worktree/branch dropdown can list every available branch — not only the
+82 -10
View File
@@ -41,6 +41,8 @@ import { ScrollShadow } from '@/components/ui/ScrollShadow';
import { toast } from '@/components/ui';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { getProjectLabel, normalizePath } from './mobilePaths';
import { CHAT_DRAFT_PROJECT_ID, isChatDirectoryPath } from '@/lib/chatDirectories';
import { partitionSidebarSessions } from '@/components/session/sidebar/list/sessionCollection';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useI18n } from '@/lib/i18n';
import { matchesRankQuery, rankByQuery } from '@/lib/search/fuzzySearch';
@@ -1022,6 +1024,27 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
return merged.filter((session) => !session.time?.archived);
}, [globalActiveSessions, liveSessions]);
// Managed Chats (sessions under ~/.config/openchamber/chats) are not owned
// by any registered project; they get their own section above the project
// tree, the same split the desktop sidebar makes. Temporary /btw forks are
// dropped here as well.
const { projectSessions, chatSessions } = React.useMemo(
() => partitionSidebarSessions(sessions, false),
[sessions],
);
const chatsBucket = React.useMemo<WorktreeBucket>(() => ({
key: CHAT_DRAFT_PROJECT_ID,
label: '',
path: '',
worktree: null,
sessions: orderSessionsByLifecycleScopes(chatSessions, pinnedSessionIds, sessionOrderRanks),
}), [chatSessions, pinnedSessionIds, sessionOrderRanks]);
const chatsBucketKey = `${CHAT_DRAFT_PROJECT_ID}::${CHAT_DRAFT_PROJECT_ID}`;
const chatRootCount = React.useMemo(
() => chatSessions.filter((session) => !getParentId(session)).length,
[chatSessions],
);
const normalizedQuery = query.trim().toLowerCase();
// On open, bring the current session (or at least its project) into view —
@@ -1070,7 +1093,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
for (const worktree of node.project.worktrees) ensureBucket(node, worktree.path, worktree);
}
for (const session of sessions) {
for (const session of projectSessions) {
const directory = getSessionDirectory(session);
if (!directory) continue;
const normalizedDirectory = normalizePath(directory);
@@ -1093,7 +1116,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
}
return nodes;
}, [activeProjectId, pinnedSessionIds, projectsMeta, sessionOrderRanks, sessions]);
}, [activeProjectId, pinnedSessionIds, projectSessions, projectsMeta, sessionOrderRanks]);
const normalizedDirectory = normalizePath(currentDirectory);
@@ -1149,8 +1172,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
// Paginated, tree-aware list of a bucket's sessions: top-level sessions paginate,
// and a parent with subsessions can be expanded to reveal its children (nested,
// recursively). Pagination counts only top-level sessions.
const renderBucketSessions = (node: ProjectNode, bucket: WorktreeBucket, indent: number) => {
const bucketKey = `${node.project.id}::${bucket.key}`;
const renderBucketSessions = (bucketKey: string, bucket: WorktreeBucket, indent: number) => {
// Group children by parent within this bucket, and treat sessions whose parent
// is not in this bucket as top-level so nothing is hidden.
@@ -1336,13 +1358,14 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
const buildSessionContextLabel = React.useCallback(
(session: Session): string => {
const directory = getSessionDirectory(session);
if (isChatDirectoryPath(directory)) return t('mobile.sessions.section.chats');
const project = findExactProjectMatch(projectsMeta, directory);
if (!project) return getProjectLabel(directory) || directory;
const matchedWorktree = findExactWorktreeMatch(project, normalizePath(directory));
if (matchedWorktree?.branch) return `${project.label} · ${matchedWorktree.branch}`;
return project.label;
},
[projectsMeta],
[projectsMeta, t],
);
const handleSelectProject = (project: ProjectMeta) => {
@@ -1481,7 +1504,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
) : null}
</div>
</div>
{projectsMeta.length === 0 ? (
{projectsMeta.length === 0 && chatSessions.length === 0 ? (
<MobileSessionsEmpty
title={t('mobile.sessions.empty.noProjectsTitle')}
description={t('mobile.sessions.empty.noProjectsDescription')}
@@ -1601,7 +1624,56 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
</div>
) : (
<div className="flex flex-col">
{orderedNodes.map((node, nodeIndex) => {
{(() => {
const chatsExpanded = projectExpandedMap[CHAT_DRAFT_PROJECT_ID] ?? true;
const chatsLabel = t('mobile.sessions.section.chats');
return (
<section>
<div className="flex min-h-12 w-full items-center">
<button
type="button"
className="flex min-h-12 min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset"
onClick={() => {
if (revealedRowId) {
handleRowKeyRevealedChange(revealedRowId, false);
return;
}
toggleProject(CHAT_DRAFT_PROJECT_ID, chatsExpanded);
}}
aria-expanded={chatsExpanded}
aria-label={
chatsExpanded
? t('sessions.sidebar.group.collapseAria', { label: chatsLabel })
: t('sessions.sidebar.group.expandAria', { label: chatsLabel })
}
style={{ touchAction: 'manipulation' }}
>
<span className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-[var(--surface-muted)] text-muted-foreground">
<Icon name="chat-4" className="size-4" />
</span>
<span className="block min-w-0 flex-1 truncate typography-ui-label font-semibold text-foreground">
{chatsLabel}
</span>
<span className="shrink-0 typography-micro text-muted-foreground tabular-nums">
{chatRootCount}
</span>
</button>
</div>
{chatsExpanded ? (
<div className="pb-2">
{chatsBucket.sessions.length > 0 ? (
renderBucketSessions(chatsBucketKey, chatsBucket, PROJECT_SESSION_INDENT)
) : (
<p className="px-3 pb-1 typography-micro text-muted-foreground" style={{ paddingLeft: PROJECT_SESSION_INDENT }}>
{t('sessions.sidebar.activity.chatsEmpty')}
</p>
)}
</div>
) : null}
</section>
);
})()}
{orderedNodes.map((node) => {
const projectExpanded = isProjectExpanded(node);
const buckets = normalizedQuery
? node.buckets.filter((bucket) =>
@@ -1614,7 +1686,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
return (
<section
key={node.project.id}
className={cn(nodeIndex > 0 && 'border-t border-border/70')}
className="border-t border-border/70"
>
<MobileSwipeActionsRow
actionsWidth={96}
@@ -1712,7 +1784,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
return (
<>
{rootBucket && rootBucket.sessions.length > 0
? renderBucketSessions(node, rootBucket, PROJECT_SESSION_INDENT)
? renderBucketSessions(`${node.project.id}::${rootBucket.key}`, rootBucket, PROJECT_SESSION_INDENT)
: null}
{worktreeBuckets.map((bucket) => {
const worktreeExpanded = isWorktreeExpanded(node, bucket);
@@ -1787,7 +1859,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
</button>
</MobileSwipeActionsRow>
{worktreeExpanded
? renderBucketSessions(node, bucket, PROJECT_SESSION_INDENT)
? renderBucketSessions(`${node.project.id}::${bucket.key}`, bucket, PROJECT_SESSION_INDENT)
: null}
</div>
);
@@ -9,6 +9,7 @@ import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
import { SortableTabsStrip, type SortableTabsStripItem } from '@/components/ui/sortable-tabs-strip';
import { TerminalView } from '@/components/views/TerminalView';
import { useI18n } from '@/lib/i18n';
import type { ProjectRef } from '@/lib/projectContextApi';
import { cn } from '@/lib/utils';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useMcpConfigStore } from '@/stores/useMcpConfigStore';
@@ -105,7 +106,7 @@ export const MobileWorkspaceDrawer: React.FC<{
/** When set, the Changes tab opens directly into the per-file diff. */
pendingChangesDiff: { path: string; staged: boolean } | null;
/** Notes tab: opens a plan fullscreen (layered above the drawer). */
onOpenPlan: (plan: { id: string; title: string }) => void;
onOpenPlan: (plan: { id: string; title: string; projectRef: ProjectRef }) => void;
/** MCP tab: jump to the MCP settings page pre-seeded with a new server draft. */
onOpenMcpSettings: () => void;
variant?: 'drawer' | 'panel';
+2
View File
@@ -14,6 +14,7 @@ import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
import { useGlobalSessionsPolling } from '@/hooks/useGlobalSessionsPolling';
import { useRouter } from '@/hooks/useRouter';
import { useWindowTitle } from '@/hooks/useWindowTitle';
import { useRootScrollLock } from '@/hooks/useRootScrollLock';
import { opencodeClient } from '@/lib/opencode/client';
import type { RuntimeAPIs } from '@/lib/api/types';
import { runtimeFetch } from '@/lib/runtime-fetch';
@@ -57,6 +58,7 @@ export function VSCodeApp({ apis }: VSCodeAppProps) {
useAppFontEffects();
usePushVisibilityBeacon({ enabled: true });
useWindowTitle();
useRootScrollLock();
useRouter();
useGlobalSessionsPolling(panelType !== 'agentManager');
+1 -1
View File
@@ -15,7 +15,7 @@ import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
import { useTerminalStore } from '@/stores/useTerminalStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { resetStreamingState } from '@/sync/streaming';
import { useGlobalSessionStatusStore, replaceGlobalSessionStatusById } from '@/sync/global-session-status';
import { replaceGlobalSessionStatusById } from '@/sync/global-session-status';
import { resetSessionOrdering } from '@/sync/session-ordering';
import { resetSessionActivityTiming } from '@/sync/session-activity-timing';
import { syncDesktopSettings } from '@/lib/persistence';
@@ -0,0 +1,41 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/icon/Icon';
import { useI18n } from '@/lib/i18n';
import { useAuthSessionStore } from '@/lib/runtime-auth-expiry';
/**
* Non-blocking notice that the OpenChamber session expired mid-work. It never
* takes the screen on its own: work stays visible and interactive, and only
* the explicit "Log in" click hands control to the session gate's full login
* flow (password, passkey, desktop shell all already there).
*/
export const AuthExpiredBanner: React.FC = () => {
const { t } = useI18n();
const authState = useAuthSessionStore((store) => store.state);
const markReauthenticating = useAuthSessionStore((store) => store.markReauthenticating);
if (authState !== 'expired') {
return null;
}
return (
// Below the header on purpose: the header row can be a window-drag region
// on desktop, where nothing under the cursor is clickable.
<div
className="pointer-events-none fixed inset-x-0 z-[200] flex justify-center px-4"
style={{ top: 'calc(var(--oc-header-height, 56px) + 8px)' }}
>
<div
role="alert"
className="oc-glass-popover oc-glass-floating pointer-events-auto flex items-center gap-3 rounded-lg px-3 py-2"
>
<Icon name="lock" className="size-4 flex-shrink-0" style={{ color: 'var(--status-error)' }} />
<span className="typography-ui-label text-foreground">{t('sessionAuth.expired.banner')}</span>
<Button size="xs" variant="outline" onClick={markReauthenticating} className="normal-case">
{t('sessionAuth.expired.loginAction')}
</Button>
</div>
</div>
);
};
@@ -303,6 +303,19 @@ mock.module('@/lib/passkeys', () => ({
registerCurrentDevicePasskey: mock(() => Promise.resolve(null)),
}));
const authSessionStore = {
state: 'ok' as const,
markAuthenticated: mock(() => undefined),
};
mock.module('@/lib/runtime-auth-expiry', () => ({
installAuthSessionFocusWatch: mock(() => undefined),
useAuthSessionStore: Object.assign(
(selector: (store: typeof authSessionStore) => unknown) => selector(authSessionStore),
{ getState: () => authSessionStore },
),
}));
const { SessionAuthGate } = await import('./SessionAuthGate');
const flushEffects = async () => {
@@ -12,6 +12,8 @@ import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { installAuthSessionFocusWatch, useAuthSessionStore } from '@/lib/runtime-auth-expiry';
import { AuthExpiredBanner } from './AuthExpiredBanner';
import { getRuntimeExtraHeadersSync } from '@/lib/runtime-auth';
import { getRuntimeApiBaseUrl, getRuntimeKey, subscribeRuntimeEndpointChanged, switchRuntimeEndpoint } from '@/lib/runtime-switch';
import { desktopHostsGet, desktopHostsSet, getDesktopHostApiUrl, normalizeHostUrl } from '@/lib/desktopHosts';
@@ -351,6 +353,7 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({
const [activePasskeyAction, setActivePasskeyAction] = React.useState<'auth' | 'register' | null>(null);
const passwordInputRef = React.useRef<HTMLInputElement | null>(null);
const hasResyncedRef = React.useRef(skipAuth);
const hasBootstrapResyncedRef = React.useRef(skipAuth);
React.useEffect(() => {
if (typeof window === 'undefined') {
@@ -557,6 +560,27 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({
}
}, [skipAuth, state]);
// Mid-session expiry: the banner asks for a re-login by flipping the shared
// auth store to 'reauthenticating'; the gate answers with its own status
// check, which lands in the full 'locked' flow on a genuine 401. A
// successful login resolves the store back to 'ok'.
const authSessionState = useAuthSessionStore((store) => store.state);
React.useEffect(() => {
if (!skipAuth) installAuthSessionFocusWatch();
}, [skipAuth]);
React.useEffect(() => {
if (skipAuth) return;
if (authSessionState === 'reauthenticating') {
void checkStatusRef.current?.();
}
}, [authSessionState, skipAuth]);
React.useEffect(() => {
if (skipAuth) return;
if (state === 'authenticated' && useAuthSessionStore.getState().state !== 'ok') {
useAuthSessionStore.getState().markAuthenticated();
}
}, [skipAuth, state]);
React.useEffect(() => {
if (state === 'locked' && passwordInputRef.current) {
passwordInputRef.current.focus();
@@ -570,10 +594,18 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({
}
if (state === 'authenticated' && !hasResyncedRef.current) {
hasResyncedRef.current = true;
// First authentication of this page load is bootstrap: adopt the
// persisted workspace pointers. A re-login after mid-session expiry is
// not — this window already has its own workspace, and the shared
// settings document may carry another window's pointers.
const isBootstrapResync = !hasBootstrapResyncedRef.current;
hasBootstrapResyncedRef.current = true;
void (async () => {
await initializeAppearancePreferences();
await syncDesktopSettings();
await applyPersistedDirectoryPreferences();
await syncDesktopSettings({ adoptWorkspace: isBootstrapResync });
if (isBootstrapResync) {
await applyPersistedDirectoryPreferences();
}
})();
}
}, [skipAuth, state]);
@@ -983,5 +1015,10 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({
);
}
return <>{children}</>;
return (
<>
{skipAuth ? null : <AuthExpiredBanner />}
{children}
</>
);
};
@@ -339,6 +339,25 @@ const WebviewBrowser: React.FC<BrowserPaneProps> = ({ initialUrl, directory, tab
}
if (action === 'browser.capture') {
// A user may close the panel after browser.open. Chromium then removes
// the zero-width webview's composited surface and capturePage() fails
// with UnknownVizError. Reveal this existing browser tab again and let
// the layout paint before asking Electron for the image.
useUIStore.getState().openContextBrowser(directory, webview.getURL());
const surfaceDeadline = Date.now() + 1_200;
let previousWidth = 0;
let stableSamples = 0;
while (stableSamples < 2 && Date.now() < surfaceDeadline) {
const width = webview.getBoundingClientRect().width;
stableSamples = width >= 2 && Math.abs(width - previousWidth) < 0.5
? stableSamples + 1
: 0;
previousWidth = width;
await new Promise((resolve) => setTimeout(resolve, 50));
}
await new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
// Wait for a settled page first: a screenshot of a half-painted layout is
// worse than none, because it looks like a finished one.
await waitForIdle();
@@ -450,7 +469,7 @@ const WebviewBrowser: React.FC<BrowserPaneProps> = ({ initialUrl, directory, tab
await waitForIdle();
}
return result;
}, [annotationHost, loadUrl, waitForIdle]);
}, [annotationHost, directory, loadUrl, waitForIdle]);
React.useEffect(
() => registerBrowserController({ run: runControlAction }),
+200 -33
View File
@@ -11,13 +11,27 @@ import { Skeleton } from '@/components/ui/skeleton';
import ChatEmptyState from './ChatEmptyState';
import { useGlobalSyncStore } from '@/sync/global-sync-store';
import MessageList, { type MessageListHandle } from './MessageList';
import { createTimelineRevealGate, TIMELINE_REVEAL_CAP_MS, TimelineRevealGateContext, type TimelineRevealGate } from './timelineRevealGate';
// How long the previous timeline stays on screen while a session that is not
// in memory loads, before the skeleton takes over.
const SESSION_SWITCH_HOLD_MS = 400;
// End inset reserved for the status row that floats over the timeline's
// bottom edge (its tallest resting height plus the mb-2 gap).
const STATUS_OVERLAY_RESERVED_HEIGHT = 40;
// A freshly opened timeline is shown once its content height has held still
// for this many consecutive frames, or after the cap.
const TIMELINE_SETTLE_STABLE_FRAMES = 2;
const TIMELINE_SETTLE_CAP_MS = 300;
import { PermissionCard } from './PermissionCard';
import { QuestionCard } from './QuestionCard';
import { hasActiveQuestionToolInCurrentTurn, recoverPendingQuestionWithRetry } from '@/sync/question-recovery';
import { StatusRowContainer } from './StatusRowContainer';
import { SessionRecapNote } from '@/components/chat/SessionRecapSpacer';
import { SessionErrorNotice } from '@/components/chat/SessionErrorNotice';
import ScrollToBottomButton from './components/ScrollToBottomButton';
import { PromptNavigatorRail } from './components/PromptNavigatorRail';
import { useAuthSessionStore } from '@/lib/runtime-auth-expiry';
import { useScrollShadow } from '@/components/ui/useScrollShadow';
import { useChatTimelineScroll, type TimelineListHandle } from '@/hooks/useChatTimelineScroll';
import { useChatTimelineController } from './hooks/useChatTimelineController';
@@ -55,6 +69,7 @@ import { WorkStatusPanel } from './work-status/WorkStatusPanel';
import { useWorkStatusVisibility } from './work-status/useWorkStatusVisibility';
import { getEmbeddedSessionChatOriginSessionId } from '@/components/layout/contextPanelEmbeddedChat';
import { isFullySyntheticMessage } from '@/lib/messages/synthetic';
import { hasContextParts } from '@/lib/messages/contextParts';
import { normalizeUserDisplayParts } from './message/normalizeUserDisplayParts';
import { findShellCommandForMessage, isUserShellMarkerMessage } from './lib/shellBridge';
import { resolveChatPromptReadOnly } from './chatPromptReadOnly';
@@ -173,9 +188,9 @@ type ChatViewportProps = {
} | null;
scrollToBottom: () => void;
endPinningReleased: boolean;
// One-shot fade for content that replaced the hydration skeleton;
// cached sessions render instantly without it.
revealContent: boolean;
/** The user waited for this session (held or fetched); reveal it with a fade. */
revealWaited: boolean;
revealGate: TimelineRevealGate;
sessionQuestions: QuestionRequest[];
sessionPermissions: PermissionRequest[];
isProgrammaticFollowActive: boolean;
@@ -212,7 +227,8 @@ const ChatViewport = React.memo(({
retryOverlay,
scrollToBottom,
endPinningReleased,
revealContent,
revealWaited,
revealGate,
sessionQuestions,
sessionPermissions,
isProgrammaticFollowActive,
@@ -261,7 +277,10 @@ const ChatViewport = React.memo(({
// Other fully synthetic user messages (loop continuations,
// plan-mode injections) are not prompts the user typed — keep
// them out of the navigator entirely.
if (isFullySyntheticMessage(message.parts)) {
// Attached context (a quoted message, a terminal selection) is
// synthetic transport-wise but is a turn the user sent, so a
// context-only message stays navigable.
if (isFullySyntheticMessage(message.parts) && !hasContextParts(message.parts)) {
continue;
}
let displayParts = normalizedPromptPartsCache.current.get(message.parts);
@@ -357,12 +376,84 @@ const ChatViewport = React.memo(({
</div>
)}
<SessionErrorNotice sessionId={currentSessionId} directory={directory} />
<SessionRecapNote sessionId={currentSessionId} directory={directory} isMobile={isMobile} />
<div className="flex-shrink-0" style={{ height: isMobile ? '40px' : '10vh' }} aria-hidden="true" />
</>
), [currentSessionId, directory, isMobile, sessionPermissions, sessionQuestions]);
// Opening a session paints the timeline as one finished picture: the root
// stays invisible while any renderer holds a provisional first paint, then
// everything appears together. A session the user waited for fades in
// once as a whole; one that was ready at the click shows in the same
// frame.
const timelineRootRef = React.useRef<HTMLDivElement | null>(null);
const endPinningReleasedRef = React.useRef(endPinningReleased);
endPinningReleasedRef.current = endPinningReleased;
React.useLayoutEffect(() => {
const root = timelineRootRef.current;
if (!root) return;
root.setAttribute('data-timeline-reveal', 'pending');
let finished = false;
let timer: number | null = null;
let frame: number | null = null;
// Revealed once the geometry has settled: after the last hold the
// list still lays rows out from its own measurements over a few
// frames, so the timeline stays hidden — pinned to the end on every
// frame — until the content height has held still for two frames,
// then shows already sitting on the end. The settle is bounded so a
// list that keeps growing (images, late tool output) still appears.
const reveal = (fade: boolean) => {
if (finished) return;
finished = true;
if (timer !== null) window.clearTimeout(timer);
const startedAt = performance.now();
let lastHeight = -1;
let stableFrames = 0;
const settle = () => {
frame = null;
const node = scrollRef.current;
let height = -1;
if (node) {
height = node.scrollHeight;
if (!endPinningReleasedRef.current) {
const end = height - node.clientHeight;
if (end - node.scrollTop > 1) node.scrollTop = end;
}
}
stableFrames = height === lastHeight ? stableFrames + 1 : 0;
lastHeight = height;
if (stableFrames < TIMELINE_SETTLE_STABLE_FRAMES && performance.now() - startedAt < TIMELINE_SETTLE_CAP_MS) {
frame = window.requestAnimationFrame(settle);
return;
}
if (fade) root.setAttribute('data-timeline-reveal', 'fading');
else root.removeAttribute('data-timeline-reveal');
};
frame = window.requestAnimationFrame(settle);
};
// Holds are taken in layout effects, including those of rows the list
// mounts in a nested synchronous pass; a microtask runs after all of
// them and still before the browser paints this commit.
queueMicrotask(() => {
if (finished) return;
revealGate.close();
if (revealGate.holds === 0) {
reveal(revealWaited);
return;
}
revealGate.onEmpty = () => reveal(true);
timer = window.setTimeout(() => reveal(true), TIMELINE_REVEAL_CAP_MS);
});
return () => {
finished = true;
if (timer !== null) window.clearTimeout(timer);
if (frame !== null) window.cancelAnimationFrame(frame);
revealGate.onEmpty = null;
};
}, [revealGate, revealWaited, scrollRef]);
const scrollContainerProps = React.useMemo(() => ({
className: 'absolute inset-0 overflow-y-auto overflow-x-hidden z-0 chat-scroll overlay-scrollbar-target',
style: CHAT_SCROLL_STYLE,
@@ -380,11 +471,12 @@ const ChatViewport = React.memo(({
isDesktopExpandedInput
? 'absolute inset-0 opacity-0 pointer-events-none'
: 'flex-1',
revealContent && !isDesktopExpandedInput && 'oc-chat-hydration-reveal',
)}
ref={timelineRootRef}
aria-hidden={isDesktopExpandedInput}
>
<div className="absolute inset-0">
<TimelineRevealGateContext.Provider value={revealGate}>
<MessageList
key={currentSessionKey}
ref={messageListRef}
@@ -412,6 +504,7 @@ const ChatViewport = React.memo(({
listFooter={listFooter}
scrollContainerProps={scrollContainerProps}
/>
</TimelineRevealGateContext.Provider>
<OverlayScrollbar containerRef={scrollRef} disableHorizontal suppressVisibility={isProgrammaticFollowActive} userIntentOnly observeMutations={false} />
{showPromptNavigator && promptTurnIds.length >= 2 ? (
<PromptNavigatorRail
@@ -443,7 +536,8 @@ const ChatViewport = React.memo(({
&& prev.retryOverlay === next.retryOverlay
&& prev.scrollToBottom === next.scrollToBottom
&& prev.endPinningReleased === next.endPinningReleased
&& prev.revealContent === next.revealContent
&& prev.revealWaited === next.revealWaited
&& prev.revealGate === next.revealGate
&& prev.sessionQuestions === next.sessionQuestions
&& prev.sessionPermissions === next.sessionPermissions
&& prev.isProgrammaticFollowActive === next.isProgrammaticFollowActive
@@ -583,10 +677,54 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
}) => {
const messagesEnabled = messagesEnabledProp ?? active;
const { t } = useI18n();
// Session UI state
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
const currentSessionDirectory = useSessionUIStore((s) => s.currentSessionDirectory);
// Session UI state. The selection is published synchronously by the
// sidebar click, but the chat swaps its content on a deferred copy: the
// first commit paints the cheap reactions (active row, URL, tab) while the
// timeline for the new session renders in an interruptible transition
// behind it. Both fields travel as one value so the key, the message
// subscription, and the loader target never mix an old directory with a
// new session id.
const liveSessionId = useSessionUIStore((s) => s.currentSessionId);
const liveSessionDirectory = useSessionUIStore((s) => s.currentSessionDirectory);
const materializedDraftSessionId = useSessionUIStore((s) => s.materializedDraftSessionId);
const liveSelection = React.useMemo(
() => ({ sessionId: liveSessionId, directory: liveSessionDirectory }),
[liveSessionId, liveSessionDirectory],
);
// A session whose messages are not in memory yet keeps the previous
// timeline on screen while they load, instead of flashing a skeleton
// between two conversations. The hold ends when the session becomes
// renderable or after SESSION_SWITCH_HOLD_MS, whichever comes first, and
// never applies when nothing was shown before or when the session was just
// created from a draft.
const liveSessionRenderable = useSessionRenderable(liveSessionId ?? '', liveSessionDirectory ?? undefined);
const shownSelectionRef = React.useRef(liveSelection);
const [expiredHoldSessionId, setExpiredHoldSessionId] = React.useState<string | null>(null);
const holdPreviousTimeline = Boolean(liveSessionId)
&& !liveSessionRenderable
&& liveSessionId !== materializedDraftSessionId
&& shownSelectionRef.current.sessionId !== null
&& shownSelectionRef.current.sessionId !== liveSessionId
&& expiredHoldSessionId !== liveSessionId;
React.useEffect(() => {
if (!holdPreviousTimeline || !liveSessionId) return;
const timer = window.setTimeout(() => setExpiredHoldSessionId(liveSessionId), SESSION_SWITCH_HOLD_MS);
return () => window.clearTimeout(timer);
}, [holdPreviousTimeline, liveSessionId]);
// A session the user waited for (not in memory at the click) fades in; one
// that was ready appears in the same frame. Decided once per selection so
// a later, warm visit to the same session is instant again.
const lastLiveSessionIdRef = React.useRef<string | null | undefined>(undefined);
const waitedSessionIdRef = React.useRef<string | null>(null);
if (liveSessionId !== lastLiveSessionIdRef.current) {
lastLiveSessionIdRef.current = liveSessionId;
waitedSessionIdRef.current = liveSessionId && !liveSessionRenderable ? liveSessionId : null;
}
const targetSelection = holdPreviousTimeline ? shownSelectionRef.current : liveSelection;
const { sessionId: currentSessionId, directory: currentSessionDirectory } = React.useDeferredValue(targetSelection);
shownSelectionRef.current = { sessionId: currentSessionId, directory: currentSessionDirectory };
const revealWaited = Boolean(currentSessionId) && currentSessionId === waitedSessionIdRef.current;
const clearMaterializedDraftSession = useSessionUIStore((s) => s.clearMaterializedDraftSession);
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession);
@@ -599,6 +737,10 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
const currentSessionKey = currentSessionId
? JSON.stringify([getRuntimeKey(), effectiveSessionDirectory, currentSessionId])
: null;
// One gate per opened session; the scroll hook holds it until the
// viewport is pinned to the end so the first visible frame is already
// at the bottom.
const revealGate = React.useMemo(() => createTimelineRevealGate(), [currentSessionKey]);
const ensureSessionRenderable = React.useCallback(
(sessionId: string) => sync.ensureSessionRenderable(sessionId, false, effectiveSessionDirectory),
[effectiveSessionDirectory, sync],
@@ -645,6 +787,8 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
suspendPartUpdatesForMessageId: streamingMessageId,
});
const sessionMessages = currentSessionId ? sessionMessageRecords : EMPTY_MESSAGES;
const authSessionExpired = useAuthSessionStore((store) => store.state !== 'ok');
const wasAuthExpiredRef = React.useRef(false);
const sessionMessageLoadState = useSessionMessageLoadState(
currentSessionId ?? '',
effectiveSessionDirectory,
@@ -817,9 +961,6 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
return () => setWorkStatusPanelVisible(false);
}, [setWorkStatusPanelVisible, showWorkStatusPanel]);
const messageListRef = React.useRef<MessageListHandle | null>(null);
// Session keys that showed the hydration skeleton this app run; their
// content gets a one-shot reveal fade once it replaces the skeleton.
const hydrationRevealKeyRef = React.useRef<string | null>(null);
const currentSession = useSession(currentSessionId, effectiveSessionDirectory);
const parentSession = useParentSession(currentSessionId, effectiveSessionDirectory);
@@ -900,13 +1041,17 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
};
}, []);
// Selection policy reads the live selection, not the deferred one: right
// after a click the deferred id still names the previous session (or
// nothing) for one commit, and acting on that would open a draft over the
// session the user just chose.
React.useEffect(() => {
if (autoOpenDraft && !currentSessionId && !draftOpen) {
if (autoOpenDraft && !liveSessionId && !draftOpen) {
// Programmatic fallback, not user navigation — must not clear the
// persisted last-session pointer the cold-launch restore reads.
openNewSessionDraft({ automatic: true });
}
}, [autoOpenDraft, currentSessionId, draftOpen, openNewSessionDraft]);
}, [autoOpenDraft, liveSessionId, draftOpen, openNewSessionDraft]);
const activeTurnChangeRef = React.useRef<(turnId: string | null) => void>(() => {});
const handleActiveTurnChange = React.useCallback((turnId: string | null) => {
@@ -917,7 +1062,11 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
// OVER the timeline's bottom edge; its measured height keeps the live
// streaming line above it and reserves matching end inset in the list.
const [statusOverlayHeight, setStatusOverlayHeight] = React.useState(0);
const composerOverlayHeight = statusOverlayHeight;
// The reserve is fixed so the timeline's end does not move when the row
// appears a commit after the session opened: a viewport pinned to the end
// would otherwise be left sitting the row's height above it. Measurement
// only extends the reserve for a taller row.
const composerOverlayHeight = Math.max(STATUS_OVERLAY_RESERVED_HEIGHT, statusOverlayHeight);
const statusOverlayObserverRef = React.useRef<ResizeObserver | null>(null);
const onStatusOverlayNode = React.useCallback((node: HTMLDivElement | null) => {
statusOverlayObserverRef.current?.disconnect();
@@ -974,6 +1123,8 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
sessionMessageCount,
composerOverlayHeight,
lastUserMessageId,
sessionIsWorking,
revealGate,
onActiveTurnChange: handleActiveTurnChange,
});
@@ -1156,20 +1307,28 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
const isSessionHydrating =
Boolean(currentSessionId)
&& !hasRenderableSessionSnapshot;
React.useEffect(() => {
if (isSessionHydrating || hydrationRevealKeyRef.current === null) return;
// One-shot: forget the key after the reveal animation has played so a
// later (now cached) visit to the same session opens instantly.
const timer = setTimeout(() => {
hydrationRevealKeyRef.current = null;
}, 400);
return () => clearTimeout(timer);
}, [isSessionHydrating, currentSessionKey]);
const retrySessionLoad = React.useCallback(() => {
if (!messagesEnabled || !currentSessionId) return;
void sync.ensureSessionRenderable(currentSessionId, true, effectiveSessionDirectory);
}, [currentSessionId, effectiveSessionDirectory, messagesEnabled, sync]);
// A load that failed while the session was expired retries itself the
// moment the re-login lands — the error screen should never outlive its
// cause.
React.useEffect(() => {
if (authSessionExpired) {
wasAuthExpiredRef.current = true;
return;
}
if (wasAuthExpiredRef.current) {
wasAuthExpiredRef.current = false;
if (sessionMessageLoadState.status === 'error') {
retrySessionLoad();
}
}
}, [authSessionExpired, retrySessionLoad, sessionMessageLoadState.status]);
React.useEffect(() => {
if (!active || !currentSessionId) return;
if (lastScrolledSessionKeyRef.current === currentSessionKey) return;
@@ -1286,9 +1445,6 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
}
const showHydrationSkeleton = isSessionHydrating && sessionMessages.length === 0 && !sessionIsWorking;
if (showHydrationSkeleton) {
hydrationRevealKeyRef.current = currentSessionKey ?? currentSessionId ?? null;
}
if (showHydrationSkeleton) {
if (sessionMessageLoadState.status === 'error') {
return (
@@ -1298,10 +1454,20 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
<Icon name="error-warning" className="size-4" />
</div>
<p className="typography-ui-label font-medium text-foreground">{t('chat.container.sessionLoadError.title')}</p>
<p className="typography-meta mt-1 text-muted-foreground">{t('chat.container.sessionLoadError.description')}</p>
<Button variant="outline" size="sm" className="mt-4" onClick={retrySessionLoad}>
{t('chat.container.sessionLoadError.retry')}
</Button>
<p className="typography-meta mt-1 text-muted-foreground">
{authSessionExpired
? t('chat.container.sessionLoadError.authDescription')
: t('chat.container.sessionLoadError.description')}
</p>
{authSessionExpired ? (
<Button variant="outline" size="sm" className="mt-4" onClick={() => useAuthSessionStore.getState().markReauthenticating()}>
{t('sessionAuth.expired.loginAction')}
</Button>
) : (
<Button variant="outline" size="sm" className="mt-4" onClick={retrySessionLoad}>
{t('chat.container.sessionLoadError.retry')}
</Button>
)}
</div>
</div>
);
@@ -1381,7 +1547,8 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
retryOverlay={retryOverlay}
scrollToBottom={resumeToLatestInstant}
endPinningReleased={userOwnsScroll}
revealContent={hydrationRevealKeyRef.current !== null && hydrationRevealKeyRef.current === (currentSessionKey ?? currentSessionId ?? null)}
revealWaited={revealWaited}
revealGate={revealGate}
sessionQuestions={sessionQuestions}
sessionPermissions={sessionPermissions}
isProgrammaticFollowActive={isFollowingProgrammatically}
+298 -96
View File
@@ -17,7 +17,7 @@ import {
} from '@/sync/attachment-files';
import type { AttachedFile } from '@/stores/types/sessionTypes';
import * as sessionActions from '@/sync/session-actions';
import { buildLinkedIssue } from '@/lib/linkedIssues';
import { buildLinkedIssue, buildLinkedLinearIssue } from '@/lib/linkedIssues';
import { useUserMessageHistory } from "@/sync/sync-context";
import { getInlineCommentDraftKey, useInlineCommentDraftStore, type InlineCommentDraft, type InlineCommentDraftTarget } from '@/stores/useInlineCommentDraftStore';
import { useSnippetsStore } from '@/stores/useSnippetsStore';
@@ -35,7 +35,8 @@ import {
import { ReviewFlowDialog, type ReviewFlowExecution } from '@/components/session/ReviewFlowDialog';
import { BtwPanel } from './btw/BtwPanel';
import { useBtwPanelState } from './btw/useBtwPanelState';
import { destroyBtwSession, startBtwSession, type BtwSessionRef } from '@/lib/btw';
import { wasPromotedBtwSession } from '@/lib/sessionBtwMetadata';
import { buildBtwSyntheticTexts, destroyBtwSession, startBtwSession, type BtwSessionRef } from '@/lib/btw';
import { AttachedFilesList, AttachedVSCodeFileChips, ActiveEditorFileSuggestion } from './FileAttachment';
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
import type { ToolPopupContent } from './message/types';
@@ -65,18 +66,21 @@ import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { GitHubIssuePickerDialog } from '@/components/session/GitHubIssuePickerDialog';
import { GitHubPrPickerDialog } from '@/components/session/GitHubPrPickerDialog';
import { LinearIssuePickerDialog } from '@/components/session/LinearIssuePickerDialog';
import { Icon } from "@/components/icon/Icon";
import { DraftPresetChips } from './DraftPresetChips';
import { useChatSearchDirectory } from '@/hooks/useChatSearchDirectory';
import { opencodeClient } from '@/lib/opencode/client';
import { useGitStore, useIsGitRepo } from '@/stores/useGitStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useSkillsStore } from '@/stores/useSkillsStore';
import { useCommandsStore } from '@/stores/useCommandsStore';
import { selectSkillsForDirectory, useSkillsStore } from '@/stores/useSkillsStore';
import { selectCommandsForDirectory, useCommandsStore } from '@/stores/useCommandsStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { usePermissionStore } from '@/stores/permissionStore';
import { togglePermissionAutoAccept } from './permissionAutoAccept';
import { useKeybind } from '@/hooks/useKeybind';
import { useAuthSessionStore } from '@/lib/runtime-auth-expiry';
import { extractGitChangedFiles } from './changedFiles';
import { useI18n } from '@/lib/i18n';
import { sessionEvents } from '@/lib/sessionEvents';
@@ -87,7 +91,18 @@ import { eventMatchesShortcut, getEffectiveShortcutCombo, normalizeCombo } from
import {
assignImageAttachmentFilenames,
buildAttachmentCitationText,
nextPastedContextFilename,
} from './attachmentCitations';
import {
createPastedContextFile,
isLargePlainTextPaste,
} from './composer/largeTextPaste';
import {
LARGE_TEXT_PASTE_TOAST_CLASSNAME,
beginLargeTextPasteOffer,
resolveLargeTextPasteOffer,
} from './composer/largeTextPasteOffer';
import type { LargeTextPasteBehavior } from '@/stores/useUIStore';
import type { FileMentionAutocompleteInputSource } from './fileMentionAutocompleteState';
import {
classifyMention,
@@ -102,10 +117,12 @@ import {
type ComposerEditorHandle,
} from './composer/editor/ComposerEditor';
import { createComposerEditorViewStore } from './composer/editor/viewStore';
import { composerAutoCorrect } from './composer/editor/autocorrect';
import {
appendInlineText,
appendWithLineBreaks,
buildImagePasteInsertion,
getMarkdownAutoPairEdit,
shouldWrapSelectionAsLink,
withInlineInsertionBoundaries,
} from './composer/text';
@@ -310,6 +327,8 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const messageRef = React.useRef(message);
const currentChatDraftIdentityRef = React.useRef<ChatDraftIdentity | null>(initialDraftIdentityRef.current);
const pendingPastedAttachmentFilenamesRef = React.useRef<Set<string>>(new Set());
const largeTextPasteToastIdRef = React.useRef<string | number | null>(null);
const largeTextPasteOfferIdRef = React.useRef(0);
// TODO: port sendMessage to session-actions (complex — creates sessions, handles attachments, etc.)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -336,6 +355,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
[btwDirectory, btwSessionId, currentSessionId],
);
const isBtwActive = Boolean(btwSessionRef) && !btwPanel.collapsed;
// A session promoted out of `/btw` keeps the boundary instructions in its
// transcript — there is no way to delete a message part — so it has to say
// they no longer apply.
const isPromotedBtwSession = wasPromotedBtwSession(btwPanel.parentSession);
const activeRuntimeKey = getRuntimeKey();
const chatDraftIdentity = React.useMemo(
() => createChatDraftIdentity(
@@ -400,10 +423,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const inputBarOffset = useUIStore((state) => state.inputBarOffset);
const persistChatDraft = useUIStore((state) => state.persistChatDraft);
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
const largeTextPasteBehavior = useUIStore((state) => state.largeTextPasteBehavior);
const isExpandedInput = useUIStore((state) => state.isExpandedInput);
const setExpandedInput = useUIStore((state) => state.setExpandedInput);
const setTimelineDialogOpen = useUIStore((state) => state.setTimelineDialogOpen);
const { git: runtimeGit, vscode: vscodeApi } = useRuntimeAPIs();
const { git: runtimeGit, vscode: vscodeApi, linear: runtimeLinear } = useRuntimeAPIs();
const cycleAgentShortcutOverride = useUIStore((state) => state.shortcutOverrides.cycle_agent);
const cycleAgentShortcut = React.useMemo(() => (
getEffectiveShortcutCombo('cycle_agent', cycleAgentShortcutOverride ? { cycle_agent: cycleAgentShortcutOverride } : undefined)
@@ -417,7 +441,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const ensureGitStatus = useGitStore((state) => state.ensureStatus);
const fetchGitStatus = useGitStore((state) => state.fetchStatus);
const clearGitDiffCache = useGitStore((state) => state.clearDiffCache);
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
const setSessionAutoAccept = usePermissionStore((state) => state.setSessionAutoAccept);
const [isNarrowComposer, setIsNarrowComposer] = React.useState(false);
const [attachmentPreview, setAttachmentPreview] = React.useState<ToolPopupContent>({
@@ -580,8 +603,8 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
// Known slash-invocations (commands + skills + built-ins) used to highlight
// matching /tokens in the composer, the same way confirmed @files are.
const availableCommands = useCommandsStore((s) => s.commands);
const availableSkills = useSkillsStore((s) => s.skills);
const availableCommands = useCommandsStore((s) => selectCommandsForDirectory(s, currentDirectory));
const availableSkills = useSkillsStore((s) => selectSkillsForDirectory(s, currentDirectory));
const knownSlashNames = React.useMemo(() => {
const names = new Set<string>([
'init', 'review', 'undo', 'redo', 'timeline', 'compact', 'btw', 'summary', 'workspace-review', 'plan-feature', 'craft-goal', 'schedule-task', 'catch-up', 'debug', 'weigh', 'explore',
@@ -695,12 +718,12 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
attachments,
};
}, [resolveInlineFileMention]);
const abortTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const prevWasAbortedRef = React.useRef(false);
// Issue linking state
const [issuePickerOpen, setIssuePickerOpen] = React.useState(false);
const [prPickerOpen, setPrPickerOpen] = React.useState(false);
const [linearPickerOpen, setLinearPickerOpen] = React.useState(false);
const [linkedIssue, setLinkedIssue] = React.useState<{
number: number;
title: string;
@@ -719,6 +742,13 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
contextText: string;
author?: { login: string; avatarUrl?: string };
} | null>(null);
const [linkedLinearIssue, setLinkedLinearIssue] = React.useState<{
identifier: string;
title: string;
url: string;
contextText: string;
author?: { login: string; avatarUrl?: string };
} | null>(null);
// Message queue
const messageQueueTarget = currentSessionId
@@ -951,6 +981,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
setPrPickerOpen(true);
}, []);
const openLinearPicker = React.useCallback(() => {
setLinearPickerOpen(true);
}, []);
const getSubmitErrorMessage = (error: unknown, fallback: string) => {
const message = error instanceof Error ? error.message : '';
return message.toLowerCase().includes('runtime changed')
@@ -964,6 +998,14 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const queuedMessageId = options?.queuedMessageId;
const delivery = options?.delivery === 'steer' && sessionPhase !== 'idle' ? 'steer' : undefined;
const capturedTarget = messageQueueTarget;
// An expired session cannot deliver anything: keep the prompt in the
// composer and point at the login banner instead of burning the send
// on a guaranteed 401.
if (useAuthSessionStore.getState().state !== 'ok') {
toast.error(t('sessionAuth.expired.sendBlocked'));
return;
}
// Snapshot the draft and current-session identity before the first
// async gap so a later sidebar selection cannot reroute the send.
const capturedDraftSnapshot = newSessionDraftOpen ? { ...newSessionDraft } : null;
@@ -1002,6 +1044,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
if (!providerIdToSend || !modelIdToSend) {
console.warn('Cannot send message: provider or model not selected');
toast.error(t('chat.chatInput.toast.noModelSelected'));
return;
}
@@ -1110,7 +1153,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
: [];
const availableSkillNames = new Set(
useSkillsStore.getState().skills.map((skill) => skill.name),
selectSkillsForDirectory(useSkillsStore.getState(), currentDirectory).map((skill) => skill.name),
);
const outgoing = buildOutgoingMessage({
@@ -1118,13 +1161,19 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
composerText: !queuedOnly && inputSnapshot.hasContent ? inputSnapshot.message : null,
composerAttachments: attachedFiles,
inlineComments: drafts,
syntheticTexts: syntheticParts?.map((part) => part.text) ?? [],
syntheticTexts: [
...buildBtwSyntheticTexts({ isBtwActive, isPromotedBtwSession }),
...(syntheticParts?.map((part) => part.text) ?? []),
],
linkedIssue: linkedIssue
? { number: linkedIssue.number, title: linkedIssue.title, url: linkedIssue.url, contextText: linkedIssue.contextText }
: null,
linkedPr: linkedPr
? { number: linkedPr.number, title: linkedPr.title, url: linkedPr.url, instructions: linkedPr.instructionsText, context: linkedPr.contextText }
: null,
linkedLinearIssue: linkedLinearIssue
? { identifier: linkedLinearIssue.identifier, title: linkedLinearIssue.title, url: linkedLinearIssue.url, contextText: linkedLinearIssue.contextText }
: null,
}, {
parseAgentMention: (text) => {
const { sanitizedText, mention } = parseAgentMentions(text, agents);
@@ -1362,6 +1411,20 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
true,
).catch(() => undefined);
}
if (linkedLinearIssue && linkTargetSessionId) {
void sessionActions.setLinkedIssue(
linkTargetSessionId,
linkTargetDirectory,
buildLinkedLinearIssue({
identifier: linkedLinearIssue.identifier,
title: linkedLinearIssue.title,
url: linkedLinearIssue.url,
author: linkedLinearIssue.author,
linkedAt: Date.now(),
}),
true,
).catch(() => undefined);
}
// Clear linked issue after successful message send
if (linkedIssue) {
@@ -1370,6 +1433,9 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
if (linkedPr) {
setLinkedPr(null);
}
if (linkedLinearIssue) {
setLinkedLinearIssue(null);
}
}).catch((error: unknown) => {
const rawMessage =
error instanceof Error
@@ -1382,10 +1448,25 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
console.error('Message send failed:', rawMessage || error);
restoreConsumedDrafts();
const currentInput = composerRef.current?.getValue() ?? messageRef.current;
if (newSessionDraftOpen && inputSnapshot.message && (!currentInput || currentInput === inputSnapshot.message)) {
setMessage(inputSnapshot.message);
writeChatDraft(chatDraftIdentity, inputSnapshot.message, confirmedMentionsRef.current);
// A failed send returns the typed prompt no matter WHY it failed —
// auth, network, server, anything. Losing a long prompt to a toast
// is the one outcome this handler must never produce.
if (inputSnapshot.message) {
if (currentChatDraftIdentityRef.current !== chatDraftIdentity) {
// The user switched sessions mid-send: restore into that
// session's persisted draft, not the visible composer.
writeChatDraft(chatDraftIdentity, inputSnapshot.message, confirmedMentionsRef.current);
} else {
const currentInput = composerRef.current?.getValue() ?? messageRef.current;
if (!currentInput || currentInput === inputSnapshot.message) {
setMessage(inputSnapshot.message);
writeChatDraft(chatDraftIdentity, inputSnapshot.message, confirmedMentionsRef.current);
} else {
// New typing already lives in the composer; the failed
// prompt joins it instead of clobbering either text.
useInputStore.getState().setPendingInputText(inputSnapshot.message, 'append');
}
}
}
const isSoftNetworkError =
@@ -1597,39 +1678,18 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const selEnd = ta?.getSelection().end ?? -1;
if (ta && selStart >= 0) {
const applyEdit = (next: string, caretStart: number, caretEnd: number) => {
const edit = getMarkdownAutoPairEdit(message, e.key, selStart, selEnd);
if (edit) {
e.preventDefault();
setMessage(next);
composerRef.current?.setSelection(caretStart, caretEnd);
updateAutocompleteState(next, caretEnd);
};
// Wrap the current selection: select text, press ` * _ ~ ( [ { " '
const WRAP_PAIRS: Record<string, [string, string]> = {
'`': ['`', '`'], '*': ['*', '*'], '_': ['_', '_'], '~': ['~', '~'],
'(': ['(', ')'], '[': ['[', ']'], '{': ['{', '}'],
'"': ['"', '"'], "'": ["'", "'"],
};
if (selEnd > selStart && WRAP_PAIRS[e.key]) {
const [open, close] = WRAP_PAIRS[e.key];
const selected = message.slice(selStart, selEnd);
const next = `${message.slice(0, selStart)}${open}${selected}${close}${message.slice(selEnd)}`;
applyEdit(next, selStart + open.length, selEnd + open.length);
ta.replaceRange(
edit.from,
edit.to,
edit.insert,
edit.selectionStart,
edit.selectionEnd,
);
return;
}
// Typing the third backtick at line start expands into a fenced
// code block with the caret on the empty middle line (Slack-like).
if (e.key === '`' && selStart === selEnd) {
const before = message.slice(0, selStart);
if (/(^|\n)``$/.test(before)) {
const after = message.slice(selEnd);
const next = `${before}\`\n\n\`\`\`${after}`;
const caret = before.length + 2; // after the completed ``` and first newline
applyEdit(next, caret, caret);
return;
}
}
}
}
@@ -1696,29 +1756,15 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
containerRef: dropZoneRef,
});
const startAbortIndicator = React.useCallback(() => {
if (abortTimeoutRef.current) {
clearTimeout(abortTimeoutRef.current);
abortTimeoutRef.current = null;
}
setShowAbortStatus(true);
abortTimeoutRef.current = setTimeout(() => {
setShowAbortStatus(false);
abortTimeoutRef.current = null;
}, 1800);
}, []);
const handleAbort = React.useCallback(() => {
clearAbortPrompt();
startAbortIndicator();
// btw mode: the stop button stops the fork's turn, not the main
// session's.
const abortTarget = isBtwActive && btwSessionId ? btwSessionId : currentSessionId;
void abortCurrentOperation(abortTarget || undefined);
}, [abortCurrentOperation, btwSessionId, clearAbortPrompt, currentSessionId, isBtwActive, startAbortIndicator]);
}, [abortCurrentOperation, btwSessionId, clearAbortPrompt, currentSessionId, isBtwActive]);
const handleCycleAgent = React.useCallback((direction: 1 | -1 = 1) => {
const nextAgentName = getCycledPrimaryAgentName(agents, currentAgentName, direction);
@@ -1767,21 +1813,24 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
if (!editor) {
// No mounted editor (collapsed mobile pill): append to the state
// the editor will be seeded from.
const nextValue = message + text;
const nextValue = messageRef.current + text;
setMessage(nextValue);
updateAutocompleteState(nextValue, nextValue.length, inputSource, text);
return;
}
const { start, end } = editor.getSelection();
const nextValue = `${message.substring(0, start)}${text}${message.substring(end)}`;
// Read the live document — delayed toast actions must not use a
// paste-time React `message` closure.
const currentMessage = editor.getValue();
const nextValue = `${currentMessage.substring(0, start)}${text}${currentMessage.substring(end)}`;
const cursorPosition = start + text.length;
// One dispatch places both the text and the caret, so there is no
// frame where the caret sits at a stale offset.
editor.insertText(text);
updateAutocompleteState(nextValue, cursorPosition, inputSource, text);
}, [message, updateAutocompleteState]);
}, [updateAutocompleteState]);
const clearDropTextSuppression = React.useCallback(() => {
suppressNextFileDropTextInsertRef.current = false;
@@ -1922,14 +1971,131 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const imageFiles = Array.from(fileMap.values());
const pastedText = e.clipboardData.getData('text');
const sessionReady = Boolean(currentSessionId || newSessionDraftOpen);
if (imageFiles.length === 0) {
if (pastedText.includes('@')) {
markFileMentionPasteSuppression();
const behavior: LargeTextPasteBehavior = largeTextPasteBehavior;
const shouldOfferLargePaste = sessionReady
&& inputMode === 'normal'
&& behavior !== 'inline'
&& isLargePlainTextPaste(pastedText);
if (!shouldOfferLargePaste) {
if (pastedText.includes('@')) {
markFileMentionPasteSuppression();
}
return;
}
// Must run synchronously — ComposerEditor does not consume paste.
e.preventDefault();
const pasteInline = () => {
if (pastedText.includes('@')) {
markFileMentionPasteSuppression();
}
insertTextAtSelection(
pastedText,
getFileMentionInputSourceForInsertedText(pastedText),
);
};
const attachAsFile = async () => {
// Read live attachment + composer state at action time — the ask
// toast can outlive the paste while the user types or attaches more.
const liveAttachedFiles = useInputStore.getState().attachedFiles;
const filename = nextPastedContextFilename([
...liveAttachedFiles.map((file) => file.filename),
...pendingPastedAttachmentFilenamesRef.current,
]);
const citationText = buildAttachmentCitationText([filename]);
const editor = composerRef.current;
const currentMessage = editor?.getValue() ?? messageRef.current;
const selectionStart = editor?.getSelection().start ?? currentMessage.length;
const selectionEnd = editor?.getSelection().end ?? currentMessage.length;
const insertionText = withInlineInsertionBoundaries(
citationText,
currentMessage.slice(0, selectionStart),
currentMessage.slice(selectionEnd),
);
insertTextAtSelection(
insertionText,
getFileMentionInputSourceForInsertedText(insertionText),
);
const file = createPastedContextFile(pastedText, filename);
pendingPastedAttachmentFilenamesRef.current.add(filename);
try {
await addAttachedFile(file);
} catch (error) {
console.error('Clipboard text attach failed', error);
toast.error(
error instanceof Error
? error.message
: t('chat.chatInput.toast.clipboardTextAttachFailed'),
);
} finally {
pendingPastedAttachmentFilenamesRef.current.delete(filename);
}
};
if (behavior === 'attach') {
await attachAsFile();
return;
}
const offerId = beginLargeTextPasteOffer(largeTextPasteOfferIdRef.current);
largeTextPasteOfferIdRef.current = offerId;
if (largeTextPasteToastIdRef.current !== null) {
// Invalidate first so a synchronous onDismiss from dismiss()
// cannot apply the superseded paste.
toast.dismiss(largeTextPasteToastIdRef.current);
largeTextPasteToastIdRef.current = null;
}
const resolveLargePaste = (action: 'attach' | 'inline') => {
const resolution = resolveLargeTextPasteOffer(
largeTextPasteOfferIdRef.current,
offerId,
);
largeTextPasteOfferIdRef.current = resolution.nextOfferId;
if (!resolution.accepted) {
return;
}
largeTextPasteToastIdRef.current = null;
if (action === 'attach') {
void attachAsFile();
return;
}
pasteInline();
};
largeTextPasteToastIdRef.current = toast.info(
t('chat.chatInput.toast.largeTextPaste.title'),
{
duration: Infinity,
className: LARGE_TEXT_PASTE_TOAST_CLASSNAME,
action: {
label: t('chat.chatInput.toast.largeTextPaste.attach'),
onClick: () => resolveLargePaste('attach'),
},
cancel: {
label: t('chat.chatInput.toast.largeTextPaste.inline'),
onClick: () => resolveLargePaste('inline'),
},
onDismiss: () => {
// Dismissing without a choice keeps the paste — insert inline
// so clipboard content is not lost.
resolveLargePaste('inline');
},
},
);
return;
}
if (!currentSessionId && !newSessionDraftOpen) {
if (!sessionReady) {
if (pastedText.includes('@')) {
markFileMentionPasteSuppression();
}
@@ -1970,7 +2136,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
pendingPastedAttachmentFilenamesRef.current.delete(filename);
}
}
}, [addAttachedFile, attachedFiles, currentSessionId, inputMode, markFileMentionPasteSuppression, message, newSessionDraftOpen, insertTextAtSelection, setMessage, t, updateAutocompleteState]);
}, [addAttachedFile, attachedFiles, currentSessionId, inputMode, largeTextPasteBehavior, markFileMentionPasteSuppression, message, newSessionDraftOpen, insertTextAtSelection, setMessage, t, updateAutocompleteState]);
const handleFileSelect = (file: { name: string; path: string; relativePath?: string }) => {
@@ -2129,10 +2295,14 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
};
React.useEffect(() => {
if (active && currentSessionId && composerRef.current && !isMobile) {
composerRef.current.focus();
}
if (!active || !currentSessionId || isMobile) return;
// Focusing forces layout. Right after a session switch the layout is
// dirty from the whole timeline mounting, so the focus call would pay
// for that layout inside the commit; a frame later it is nearly free.
const frame = window.requestAnimationFrame(() => {
composerRef.current?.focus();
});
return () => window.cancelAnimationFrame(frame);
}, [active, currentSessionId, isMobile]);
React.useEffect(() => {
@@ -2391,6 +2561,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const footerGapClass = 'gap-x-1.5 gap-y-0';
const isVSCode = isVSCodeRuntime();
const showLinearPicker = Boolean(runtimeLinear) && !isVSCode;
// The work-status panel carries the agent's todos and the changed-file
// count, but only on the desktop/web layout — VS Code and mobile have no
// panel, so these keep their place above the composer there.
@@ -2471,6 +2642,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
draftPickerOpen: mobileDraftPicker !== null,
issuePickerOpen,
prPickerOpen,
linearPickerOpen,
isDragging,
},
});
@@ -2562,31 +2734,20 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
t,
]);
React.useEffect(() => {
const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId;
if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) {
startAbortIndicator();
if (currentSessionId) {
acknowledgeSessionAbort(currentSessionId);
}
}
prevWasAbortedRef.current = pendingAbortBanner;
}, [
abortPromptSessionId,
acknowledgeSessionAbort,
currentSessionId,
showAbortStatus,
startAbortIndicator,
]);
useKeybind('toggle_permission_auto_accept', () => {
if (!isPermissionAutoAcceptInteractive) return false;
handlePermissionAutoAcceptToggle();
});
// Acknowledging the abort record is what lets the working chip resume for
// the next run; the old "Aborted" banner that used to accompany it is gone.
React.useEffect(() => {
return () => {
if (abortTimeoutRef.current) {
clearTimeout(abortTimeoutRef.current);
abortTimeoutRef.current = null;
}
};
}, []);
const pendingAbort = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId;
if (!prevWasAbortedRef.current && pendingAbort && currentSessionId) {
acknowledgeSessionAbort(currentSessionId);
}
prevWasAbortedRef.current = pendingAbort;
}, [abortPromptSessionId, acknowledgeSessionAbort, currentSessionId]);
return (
<>
@@ -2652,12 +2813,23 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
onRemove={() => setLinkedPr(null)}
/>
) : null}
{linkedLinearIssue && !isVSCode ? (
<LinkedReferenceRow
numberLabel={linkedLinearIssue.identifier}
title={linkedLinearIssue.title}
url={linkedLinearIssue.url}
author={linkedLinearIssue.author}
openInBrowserLabel={t('chat.chatInput.linked.linearIssue.openInBrowserAria')}
removeLabel={t('chat.chatInput.linked.linearIssue.removeAria')}
onReopenPicker={() => setLinearPickerOpen(true)}
onRemove={() => setLinkedLinearIssue(null)}
/>
) : null}
<RevertedMessageDock
sessionId={currentSessionId}
directory={currentSessionDirectoryForSync ?? currentDirectory}
/>
<MemoComposerStatusBar
showAbortStatus={showAbortStatus}
showTodos={composerStatusExtrasEnabled}
leftAccessory={!composerStatusExtrasEnabled || newSessionDraftOpen || !hasPendingChanges
? null
@@ -2718,6 +2890,8 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
onPickLocalFiles={handlePickLocalFiles}
onOpenIssuePicker={openIssuePicker}
onOpenPrPicker={openPrPicker}
showLinearPicker={showLinearPicker}
onOpenLinearPicker={openLinearPicker}
onOpenAttachSheet={openMobileAttachSheet}
onStartDictation={toggleDictation}
onAbort={handleAbort}
@@ -2854,7 +3028,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
: t(useCompactChatPlaceholder ? 'chat.chatInput.placeholder.chatCompact' : 'chat.chatInput.placeholder.chat')
: t('chat.chatInput.placeholder.selectSession')}
editable={Boolean(currentSessionId || newSessionDraftOpen)}
autoCorrect={isMobile}
autoCorrect={composerAutoCorrect({ isMobile })}
autoCapitalize={isMobile ? 'sentences' : 'none'}
spellCheck={isMobile || inputSpellcheckEnabled}
fillContainer={isComposerExpanded}
@@ -2898,6 +3072,8 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
onPickLocalFiles={handlePickLocalFiles}
onOpenIssuePicker={openIssuePicker}
onOpenPrPicker={openPrPicker}
showLinearPicker={showLinearPicker}
onOpenLinearPicker={openLinearPicker}
onOpenAttachSheet={openMobileAttachSheet}
onToggleExpandedInput={handleToggleExpandedInput}
onTogglePermissionAutoAccept={handlePermissionAutoAcceptToggle}
@@ -2962,6 +3138,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
onSelect={(issue) => {
setLinkedIssue(issue);
setLinkedPr(null);
setLinkedLinearIssue(null);
}}
/>
<GitHubPrPickerDialog
@@ -2970,6 +3147,17 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
onSelect={(pr) => {
setLinkedPr(pr);
setLinkedIssue(null);
setLinkedLinearIssue(null);
}}
/>
<LinearIssuePickerDialog
open={linearPickerOpen}
onOpenChange={setLinearPickerOpen}
mode="select"
onSelect={(issue) => {
setLinkedLinearIssue(issue);
setLinkedIssue(null);
setLinkedPr(null);
}}
/>
<ReviewFlowDialog
@@ -3053,6 +3241,20 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
<Icon name="git-pull-request" className="h-[18px] w-[18px] flex-shrink-0 text-muted-foreground" />
{t('chat.chatInput.actions.linkGithubPr')}
</button>
{showLinearPicker ? (
<button
type="button"
className="flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2 py-3 text-left typography-ui-label hover:bg-[var(--interactive-hover)]"
onClick={() => {
mobileShell.skipNextOverlayCloseRestore();
setMobileAttachMenuOpen(false);
requestAnimationFrame(openLinearPicker);
}}
>
<Icon name="linear" className="h-[18px] w-[18px] flex-shrink-0 text-muted-foreground" />
{t('chat.chatInput.actions.linkLinearIssue')}
</button>
) : null}
</div>
</MobileOverlayPanel>
) : null}
@@ -21,7 +21,7 @@ import { deriveMessageRole } from './message/messageRole';
import { filterVisibleParts, normalizeParts } from './message/partUtils';
import { normalizeUserDisplayParts } from './message/normalizeUserDisplayParts';
import { isHiddenUserMessage } from './message/hiddenUserMessage';
import { flattenAssistantTextParts } from '@/lib/messages/messageText';
import { flattenAssistantTextParts, flattenUserTextParts } from '@/lib/messages/messageText';
import { isLikelyProviderAuthFailure, PROVIDER_AUTH_FAILURE_MESSAGE } from '@/lib/messages/providerAuthError';
import { getProviderModelDisplayName } from '@/lib/modelDisplay';
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
@@ -457,13 +457,6 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
}, [chatRenderMode, isMessageCompleted, isUser, visibleParts]);
const assistantTextParts = React.useMemo(() => {
if (isUser) {
return [];
}
return visibleParts.filter((part) => part.type === 'text');
}, [isUser, visibleParts]);
const toolParts = React.useMemo(() => {
if (isUser) {
return [];
@@ -545,19 +538,6 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
const shouldHideUserMessage = isUser && displayParts.length === 0;
// Message is considered to have an "open step" if info.finish is not yet present
const hasOpenStep = typeof messageFinish !== 'string';
const shouldCoordinateRendering = React.useMemo(() => {
if (isUser) {
return false;
}
if (assistantTextParts.length === 0 || toolParts.length === 0) {
return hasOpenStep;
}
return true;
}, [assistantTextParts.length, toolParts.length, hasOpenStep, isUser]);
const themeVariant = currentTheme?.metadata.variant;
const isDarkTheme = React.useMemo(() => {
if (themeVariant) {
@@ -722,40 +702,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
const messageTextContent = React.useMemo(() => {
if (isUser) {
const shellOutputs = displayParts
.filter((part): part is Part & { type: 'text'; shellAction?: { output?: unknown } } => part.type === 'text')
.map((part) => {
const output = part.shellAction?.output;
return typeof output === 'string' ? output.trim() : '';
})
.filter((output) => output.length > 0);
if (shellOutputs.length > 0) {
return shellOutputs.join('\n\n');
}
const shellCommands = displayParts
.filter((part): part is Part & { type: 'text'; shellAction?: { command?: unknown } } => part.type === 'text')
.map((part) => {
const command = part.shellAction?.command;
return typeof command === 'string' ? command.trim() : '';
})
.filter((command) => command.length > 0);
if (shellCommands.length > 0) {
return shellCommands.join('\n');
}
const textParts = displayParts
.filter((part): part is Part & { type: 'text'; text?: string; content?: string } => part.type === 'text')
.map((part) => {
const text = part.text || part.content || '';
return text.trim();
})
.filter((text) => text.length > 0);
const combined = textParts.join('\n');
return combined.replace(/\n\s*\n+/g, '\n');
return flattenUserTextParts(displayParts);
}
if (assistantErrorText && assistantErrorText.trim().length > 0) {
@@ -1,9 +1,9 @@
import React from 'react';
import { cn, fuzzyMatch } from '@/lib/utils';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionMessages } from '@/sync/sync-context';
import { useCommandsStore } from '@/stores/useCommandsStore';
import { useSkillsStore } from '@/stores/useSkillsStore';
import { selectCommandsForDirectory, useCommandsStore } from '@/stores/useCommandsStore';
import { selectSkillsForDirectory, useSkillsStore } from '@/stores/useSkillsStore';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
@@ -66,8 +66,6 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
}, ref) => {
const { t } = useI18n();
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const sessionMessages = useSessionMessages(currentSessionId ?? '');
const hasMessagesInCurrentSession = sessionMessages.length > 0;
const hasSession = Boolean(currentSessionId);
const hasNewSessionDraft = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open));
const canStartSessionCommand = hasSession || hasNewSessionDraft;
@@ -76,10 +74,16 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
const [commands, setCommands] = React.useState<CommandInfo[]>([]);
const [loading, setLoading] = React.useState(false);
const commandsWithMetadata = useCommandsStore((s) => s.commands);
const refreshCommands = useCommandsStore((s) => s.loadCommands);
const skills = useSkillsStore((s) => s.skills);
const refreshSkills = useSkillsStore((s) => s.loadSkills);
// Commands and skills belong to the directory the composer sends to — the
// session's own directory, or the Chats root for a chat draft — not to the
// project the app was on last.
const effectiveDirectory = useEffectiveDirectory();
const commandsWithMetadata = useCommandsStore((s) => selectCommandsForDirectory(s, effectiveDirectory));
const loadCommandsForDirectory = useCommandsStore((s) => s.loadCommands);
const skills = useSkillsStore((s) => selectSkillsForDirectory(s, effectiveDirectory));
const loadSkillsForDirectory = useSkillsStore((s) => s.loadSkills);
const refreshCommands = React.useCallback(() => loadCommandsForDirectory(effectiveDirectory), [effectiveDirectory, loadCommandsForDirectory]);
const refreshSkills = React.useCallback(() => loadSkillsForDirectory(effectiveDirectory), [effectiveDirectory, loadSkillsForDirectory]);
const [selectedIndex, setSelectedIndex] = React.useState(0);
const selectedIndexRef = React.useRef(0);
const keyboardNavigationRef = React.useRef(false);
@@ -140,7 +144,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
}));
const builtInCommands: CommandInfo[] = [
...(hasSession && !hasMessagesInCurrentSession
...(hasSession
? [{ id: 'openchamber:init', name: 'init', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.initDescription'), isBuiltIn: true }]
: []
),
@@ -200,10 +204,9 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
];
const allCommands = mergeCommandAutocompleteItems(builtInCommands, customCommands, skillCommands);
const allowInitCommand = !hasMessagesInCurrentSession;
const filtered = (searchQuery
const filtered = searchQuery
? allCommands.filter(cmd => commandMatchesSearch(cmd, searchQuery))
: allCommands).filter(cmd => allowInitCommand || cmd.name !== 'init');
: allCommands;
filtered.sort((a, b) => {
const aStartsWith = a.name.toLowerCase().startsWith(searchQuery.toLowerCase());
@@ -216,9 +219,8 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
setCommands(filtered);
} catch {
const allowInitCommand = !hasMessagesInCurrentSession;
const builtInCommands: CommandInfo[] = [
...(hasSession && !hasMessagesInCurrentSession
...(hasSession
? [{ id: 'openchamber:init', name: 'init', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.initDescription'), isBuiltIn: true }]
: []
),
@@ -277,12 +279,12 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
),
];
const filtered = (searchQuery
const filtered = searchQuery
? builtInCommands.filter(cmd =>
fuzzyMatch(cmd.name, searchQuery) ||
(cmd.description && fuzzyMatch(cmd.description, searchQuery))
)
: builtInCommands).filter(cmd => allowInitCommand || cmd.name !== 'init');
: builtInCommands;
setCommands(filtered);
} finally {
@@ -291,7 +293,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
};
loadCommands();
}, [searchQuery, hasMessagesInCurrentSession, hasSession, canStartSessionCommand, canUseReviewHandoffFlow, commandsWithMetadata, skills, t]);
}, [searchQuery, hasSession, canStartSessionCommand, canUseReviewHandoffFlow, commandsWithMetadata, skills, t]);
React.useEffect(() => {
setSelectedIndex(0);
@@ -116,13 +116,11 @@ const TodoItemRow: React.FC<{ todo: TodoItem }> = ({ todo }) => {
const EMPTY_TODOS: TodoItem[] = [];
interface ComposerStatusBarProps {
showAbortStatus?: boolean;
showTodos?: boolean;
leftAccessory?: React.ReactNode;
}
export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({
showAbortStatus,
showTodos = true,
leftAccessory,
}) => {
@@ -186,7 +184,7 @@ export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({
const hasTodoContent = showTodos && statusSummary.left > 0;
const hasLeftAccessory = Boolean(leftAccessory);
const hasContent = Boolean(showAbortStatus) || hasTodoContent || hasLeftAccessory;
const hasContent = hasTodoContent || hasLeftAccessory;
const popoverRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
@@ -252,16 +250,7 @@ export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({
<div className={cn("flex items-center justify-between gap-2 h-8", hasLeftAccessory && "px-0.5")}>
{/* Left: abort status | pending-changes accessory */}
<div className={cn("flex-1 flex items-center min-w-0 gap-2", hasLeftAccessory ? "pl-1.5" : "overflow-x-hidden")}>
{showAbortStatus ? (
<div className="flex h-full items-center text-[var(--status-error)] pl-0.5">
<span className="flex items-center gap-1.5 typography-ui-label">
<Icon name="close-circle" aria-hidden="true" />
{t('chat.statusRow.aborted')}
</span>
</div>
) : leftAccessory ? (
leftAccessory
) : null}
{leftAccessory ?? null}
</div>
{/* Right: todos dropdown */}
@@ -558,17 +558,29 @@ interface FilePart {
const GITHUB_ISSUE_LINK_MIME = 'application/vnd.github.issue-link';
const GITHUB_PR_LINK_MIME = 'application/vnd.github.pull-request-link';
const LINEAR_ISSUE_LINK_MIME = 'application/vnd.openchamber.linear-issue-link';
const getGitHubLinkKind = (file: FilePart): 'issue' | 'pr' | null => {
type IssueLinkKind = 'github-issue' | 'github-pr' | 'linear-issue';
const getIssueLinkKind = (file: FilePart): IssueLinkKind | null => {
if (file.mime === GITHUB_ISSUE_LINK_MIME) {
return 'issue';
return 'github-issue';
}
if (file.mime === GITHUB_PR_LINK_MIME) {
return 'pr';
return 'github-pr';
}
if (file.mime === LINEAR_ISSUE_LINK_MIME) {
return 'linear-issue';
}
return null;
};
const issueLinkIcon = (kind: IssueLinkKind): 'github' | 'git-pull-request' | 'linear' => {
if (kind === 'github-pr') return 'git-pull-request';
if (kind === 'linear-issue') return 'linear';
return 'github';
};
interface MessageFilesDisplayProps {
files: FilePart[];
onShowPopup?: (content: ToolPopupContent) => void;
@@ -591,7 +603,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
};
const resolveDisplayName = React.useCallback((file: FilePart): string => {
const isGitHubLink = getGitHubLinkKind(file) !== null;
const isGitHubLink = getIssueLinkKind(file) !== null;
if (isGitHubLink && typeof file.filename === 'string' && file.filename.trim().length > 0) {
return file.filename.trim();
}
@@ -665,11 +677,11 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
const fileName = resolveDisplayName(file);
const ext = fileName.split('.').pop() || '';
const sizeText = formatFileSize(file.size);
const githubLinkKind = getGitHubLinkKind(file);
const issueLinkKind = getIssueLinkKind(file);
return (
<Tooltip key={`file-${file.url || file.filename || index}`}>
<TooltipTrigger asChild>
{githubLinkKind && file.url ? (
{issueLinkKind && file.url ? (
<button
type="button"
onClick={() => {
@@ -677,11 +689,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
}}
className="inline-flex items-center bg-muted/30 border border-border/30 typography-meta gap-1 px-2 py-0.5 rounded-lg text-foreground hover:text-primary transition-colors"
>
{githubLinkKind === 'pr' ? (
<Icon name="git-pull-request" className="text-muted-foreground h-3.5 w-3.5" />
) : (
<Icon name="github" className="text-muted-foreground h-3.5 w-3.5" />
)}
<Icon name={issueLinkIcon(issueLinkKind)} className="text-muted-foreground h-3.5 w-3.5" />
<div className="overflow-hidden max-w-[220px]">
<span className="truncate block" title={fileName}>{fileName}</span>
</div>
@@ -764,7 +772,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
const fileName = resolveDisplayName(file);
const isImage = file.mime?.startsWith('image/');
const sizeText = formatFileSize(file.size);
const githubLinkKind = getGitHubLinkKind(file);
const issueLinkKind = getIssueLinkKind(file);
if (isImage && file.url) {
return (
@@ -787,7 +795,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
);
}
if (githubLinkKind && file.url) {
if (issueLinkKind && file.url) {
return (
<Tooltip key={file.url || `${fileName}-${index}`}>
<TooltipTrigger asChild>
@@ -802,11 +810,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false }
)}
>
<div className="flex-shrink-0">
{githubLinkKind === 'pr' ? (
<Icon name="git-pull-request" className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
) : (
<Icon name="github" className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
)}
<Icon name={issueLinkIcon(issueLinkKind)} className={cn("text-muted-foreground", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{fileName}</p>
@@ -2,7 +2,7 @@ import React from 'react';
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
import { isMobileSurfaceRuntime } from '@/lib/runtimeSurface';
import { cn } from '@/lib/utils';
import { loadMarkdownRendererModule } from './markdownRendererLoader';
import { getLoadedMarkdownRendererModule, loadMarkdownRendererModule } from './markdownRendererLoader';
// Thin lazy wrapper around the MarkdownRenderer implementation.
// The full implementation (marked + Shiki highlighting + KaTeX + morphdom
@@ -41,17 +41,29 @@ const MobileMarkdownFallback = (props: { content?: unknown; className?: unknown;
);
};
export const MarkdownRenderer: React.FC<React.ComponentPropsWithoutRef<typeof MarkdownRendererLazy>> = (props) => (
<React.Suspense fallback={<MobileMarkdownFallback {...props} />}>
<MarkdownRendererLazy {...props} />
</React.Suspense>
);
export const MarkdownRenderer: React.FC<React.ComponentPropsWithoutRef<typeof MarkdownRendererLazy>> = (props) => {
const loaded = getLoadedMarkdownRendererModule();
if (loaded) return <loaded.MarkdownRenderer {...props} />;
return (
<React.Suspense fallback={<MobileMarkdownFallback {...props} />}>
<MarkdownRendererLazy {...props} />
</React.Suspense>
);
};
export const SimpleMarkdownRenderer: React.FC<React.ComponentPropsWithoutRef<typeof SimpleMarkdownRendererLazy>> = (props) => (
<React.Suspense fallback={<MobileMarkdownFallback {...props} />}>
<SimpleMarkdownRendererLazy {...props} />
</React.Suspense>
);
type SimpleMarkdownRendererProps = React.ComponentPropsWithoutRef<typeof SimpleMarkdownRendererLazy> & {
fallbackContent?: React.ReactNode;
};
export const SimpleMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({ fallbackContent, ...props }) => {
const loaded = getLoadedMarkdownRendererModule();
if (loaded) return <loaded.SimpleMarkdownRenderer {...props} />;
return (
<React.Suspense fallback={fallbackContent ?? <MobileMarkdownFallback {...props} />}>
<SimpleMarkdownRendererLazy {...props} />
</React.Suspense>
);
};
export const MarkdownImageGallery: React.FC<React.ComponentPropsWithoutRef<typeof MarkdownImageGalleryLazy>> = (props) => (
<React.Suspense fallback={null}>
@@ -193,6 +193,8 @@ const fakeReact = {
return hookStates[index] as { current: T };
},
memo: <T>(component: T): T => component,
createContext: <T>(defaultValue: T) => ({ Provider: 'provider', defaultValue }),
useContext: <T>(context: { defaultValue: T }): T => context.defaultValue,
};
const fakeJsx = (_type: string, props: FakeJsxProps | null, ...children: FakeElement[]): FakeElement => {
@@ -51,6 +51,7 @@ import {
import { fileReferenceExists } from './fileReferenceStat';
import { streamPerfCount, streamPerfObserve } from '@/stores/utils/streamDebug';
import { detachedMarkdownDomCache, type DetachedMarkdownDomKey } from './markdown/detachedMarkdownDomCache';
import { TimelineRevealGateContext } from './timelineRevealGate';
import { getRuntimeKey } from '@/lib/runtime-switch';
const useCurrentMermaidTheme = () => {
@@ -692,6 +693,30 @@ const useMermaidInlineInteractions = ({
const MERMAID_RENDER_CACHE = new Map<string, MermaidRender>();
const MERMAID_RENDER_CACHE_MAX = 100;
const MARKDOWN_DECORATION_ID_ATTR = 'data-md-decoration-id';
// True when the container already holds exactly these settled blocks with the
// current decoration. The first paint of a remounted message is served from
// the block cache; when that paint is already final, the async render would
// only parse, highlight, sanitize, and morph the same HTML into place again.
const domMatchesRenderedBlocks = (
target: HTMLElement,
blocks: ReadonlyArray<{ id: string }>,
decorationId: string,
): boolean => {
const children = target.children;
if (children.length !== blocks.length) return false;
for (let index = 0; index < blocks.length; index += 1) {
const child = children[index];
if (
!child
|| child.getAttribute('data-md-id') !== blocks[index]?.id
|| child.getAttribute(MARKDOWN_DECORATION_ID_ATTR) !== decorationId
) {
return false;
}
}
return true;
};
const MARKDOWN_DECORATION_IDS = new WeakMap<DecorateContext, string>();
let nextMarkdownDecorationId = 0;
const MARKDOWN_DOM_CACHE_MAX_SOURCE_CHARS = 200_000;
@@ -804,6 +829,16 @@ const useMorphdomMarkdown = ({
const mermaidViewerRef = React.useRef<ReturnType<typeof createMermaidViewerRegistry> | null>(null);
const renderRevisionRef = React.useRef(0);
// A provisional first paint (blocks not in the settled cache) holds the
// timeline reveal until the async render lands, so the session opens with
// final code highlighting instead of a visible restyle.
const revealGate = React.useContext(TimelineRevealGateContext);
const releaseRevealHoldRef = React.useRef<(() => void) | null>(null);
const releaseRevealHold = React.useCallback(() => {
releaseRevealHoldRef.current?.();
releaseRevealHoldRef.current = null;
}, []);
React.useEffect(() => releaseRevealHold, [releaseRevealHold]);
// Only DOM that was actually restored or completed by the async pipeline is
// eligible for capture. A fallback from an earlier content revision is not.
const mountedDomRef = React.useRef<{
@@ -909,6 +944,9 @@ const useMorphdomMarkdown = ({
}
if (hasMermaidBlock) refreshMermaidViewers();
} else {
if (!streaming && !releaseRevealHoldRef.current) {
releaseRevealHoldRef.current = revealGate?.hold() ?? null;
}
const block = document.createElement('div');
block.setAttribute('data-md-block', '');
block.style.display = 'contents';
@@ -939,6 +977,18 @@ const useMorphdomMarkdown = ({
const renderRevision = renderRevisionRef.current;
const decorationId = getMarkdownDecorationId(ctx);
if (!streaming) {
const cachedBlocks = getCachedMarkdownBlocks(text, imageMode);
if (cachedBlocks && domMatchesRenderedBlocks(target, cachedBlocks, decorationId)) {
mountedDomRef.current = domCacheKey
? { key: domCacheKey, copiedLabel: ctx.labels.copied }
: null;
streamPerfCount('ui.markdown_renderer.settled_paint.reused');
releaseRevealHold();
return;
}
}
void renderMarkdownBlocks(text, streaming, imageMode).then((blocks) => {
if (!active || renderRevisionRef.current !== renderRevision) return;
const existing = Array.from(target.children) as HTMLElement[];
@@ -1028,12 +1078,13 @@ const useMorphdomMarkdown = ({
mountedDomRef.current = domCacheKey
? { key: domCacheKey, copiedLabel: ctx.labels.copied }
: null;
releaseRevealHold();
});
return () => {
active = false;
};
}, [containerRef, ctx, domCacheKey, imageMode, refreshMermaidViewers, streaming, text]);
}, [containerRef, ctx, domCacheKey, imageMode, refreshMermaidViewers, releaseRevealHold, streaming, text]);
React.useEffect(() => {
const container = containerRef.current;
@@ -1534,6 +1534,54 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
return true;
}, [allEntries.length]);
// A navigation scroll lands on estimates: an unmounted target teleports
// to its estimated offset, and even a mounted one drifts when neighbours
// finish measuring a frame later. This settle loop re-aligns the target to
// the requested viewport position until the layout stops moving, and backs
// off the moment the user touches the scroll.
const settleNavigationTarget = React.useCallback((
findElement: () => HTMLElement | null,
desiredOffsetTop: number,
) => {
const container = resolveScrollContainer();
if (!container || typeof window === 'undefined') {
return;
}
let frames = 0;
let stable = 0;
let cancelled = false;
const cancelOnUserInput = () => {
cancelled = true;
container.removeEventListener('touchstart', cancelOnUserInput);
container.removeEventListener('wheel', cancelOnUserInput);
};
container.addEventListener('touchstart', cancelOnUserInput, { passive: true });
container.addEventListener('wheel', cancelOnUserInput, { passive: true });
const step = () => {
if (cancelled) return;
const element = findElement();
if (element) {
const delta = element.getBoundingClientRect().top
- container.getBoundingClientRect().top
- desiredOffsetTop;
if (Math.abs(delta) > 0.5) {
container.scrollTop += delta;
stable = 0;
} else {
stable += 1;
}
}
frames += 1;
if (stable >= ANCHOR_HOLD_STABLE_FRAMES || frames >= ANCHOR_HOLD_MAX_FRAMES) {
container.removeEventListener('touchstart', cancelOnUserInput);
container.removeEventListener('wheel', cancelOnUserInput);
return;
}
window.requestAnimationFrame(step);
};
window.requestAnimationFrame(step);
}, [resolveScrollContainer]);
const scrollMessageElementIntoView = React.useCallback((messageId: string, behavior: ScrollBehavior = 'auto') => {
const container = resolveScrollContainer();
if (!container) {
@@ -1569,14 +1617,19 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
if (!container) {
return false;
}
const turnElement = container.querySelector<HTMLElement>(`[data-turn-id="${turnId}"]`);
const findTurnElement = () => container.querySelector<HTMLElement>(`[data-turn-id="${turnId}"]`);
const turnElement = findTurnElement();
if (turnElement) {
turnElement.scrollIntoView({ behavior, block: 'start' });
if (behavior !== 'smooth') settleNavigationTarget(findTurnElement, 0);
return true;
}
return scrollHistoryIndexIntoView(index);
if (!scrollHistoryIndexIntoView(index)) {
return false;
}
if (behavior !== 'smooth') settleNavigationTarget(findTurnElement, 0);
return true;
},
scrollToMessageId: (messageId: string, options?: { behavior?: ScrollBehavior }) => {
@@ -1586,8 +1639,12 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
return false;
}
return scrollMessageElementIntoView(messageId, behavior)
const didScroll = scrollMessageElementIntoView(messageId, behavior)
|| scrollHistoryIndexIntoView(index);
if (didScroll && behavior !== 'smooth') {
settleNavigationTarget(() => findMessageElement(messageId), 50);
}
return didScroll;
},
holdViewportAnchor: (anchor) => {
@@ -1730,7 +1787,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
return () => {
objectRef.current = null;
};
}, [findMessageElement, historyEntries.length, messageIndexMap, resolveScrollContainer, scrollHistoryIndexIntoView, scrollMessageElementIntoView, turnIndexMap, ref]);
}, [findMessageElement, historyEntries.length, messageIndexMap, resolveScrollContainer, scrollHistoryIndexIntoView, scrollMessageElementIntoView, settleNavigationTarget, turnIndexMap, ref]);
const anchoredEndSpace = React.useMemo<TimelineAnchoredEndSpace | undefined>(() => {
const resolved = resolveChatListAnchoredEndSpace(
@@ -324,7 +324,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const providers = useConfigStore((state) => state.providers);
const currentProviderId = useConfigStore((state) => state.currentProviderId);
const currentModelId = useConfigStore((state) => state.currentModelId);
const currentVariant = useConfigStore((state) => state.currentVariant);
const effectiveCurrentVariant = useConfigStore((state) => state.currentVariant);
const currentVariantSelection = useConfigStore((state) => state.currentVariantSelection);
const currentVariant = currentVariantSelection.override ?? undefined;
const currentAgentName = useConfigStore((state) => state.currentAgentName);
const settingsDefaultVariant = useConfigStore((state) => state.settingsDefaultVariant);
const settingsDefaultAgent = useConfigStore((state) => state.settingsDefaultAgent);
@@ -332,6 +334,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const setSelectedProvider = useConfigStore((state) => state.setSelectedProvider);
const setModel = useConfigStore((state) => state.setModel);
const setCurrentVariant = useConfigStore((state) => state.setCurrentVariant);
const setCurrentVariantOverride = useConfigStore((state) => state.setCurrentVariantOverride);
const getCurrentModelVariants = useConfigStore((state) => state.getCurrentModelVariants);
const setAgent = useConfigStore((state) => state.setAgent);
const getCurrentProvider = useConfigStore((state) => state.getCurrentProvider);
@@ -630,7 +633,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
];
const prevAgentNameRef = React.useRef<string | undefined>(undefined);
const explicitAgentSwitchRef = React.useRef<string | null>(null);
const latestLoadedUserChoiceRestoreRef = React.useRef<string | null>(null);
const currentSessionDirectory = currentSessionId ? getDirectoryForSession(currentSessionId) : undefined;
@@ -693,6 +695,30 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return variants ? Object.keys(variants) : [];
}, [providers]);
const resolveInheritedVariantForModel = React.useCallback((providerId: string, modelId: string, agentName?: string | null) => {
const variantOptions = getModelVariantOptions(providerId, modelId);
if (variantOptions.length === 0) return undefined;
let currentInherited: string | undefined;
if (currentProviderId === providerId && currentModelId === modelId) {
currentInherited = currentVariantSelection.inherited
?? (currentVariantSelection.override === null || currentVariantSelection.override === undefined
? effectiveCurrentVariant
: undefined);
}
const effectiveAgentName = agentName ?? uiAgentName ?? currentAgentName;
const agent = effectiveAgentName ? agents.find((candidate) => candidate.name === effectiveAgentName) : undefined;
const agentVariant = (
agent?.model?.providerID === providerId
&& agent.model.modelID === modelId
) ? agent.variant : undefined;
const candidates = currentSessionId
? [agentVariant, settingsDefaultVariant, currentInherited]
: [currentInherited, agentVariant, settingsDefaultVariant];
return candidates.find((candidate) => candidate !== undefined && variantOptions.includes(candidate));
}, [agents, currentAgentName, currentModelId, currentProviderId, currentSessionId, currentVariantSelection, effectiveCurrentVariant, getModelVariantOptions, settingsDefaultVariant, uiAgentName]);
const resolveModelVariantSelection = React.useCallback((providerId: string, modelId: string) => {
const variantOptions = getModelVariantOptions(providerId, modelId);
if (variantOptions.length === 0) {
@@ -711,10 +737,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return currentVariant;
}
if (!currentSessionId && settingsDefaultVariant && variantOptions.includes(settingsDefaultVariant)) {
return settingsDefaultVariant;
}
return undefined;
}, [
currentAgentName,
@@ -724,7 +746,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
currentVariant,
getAgentModelVariantForSession,
getModelVariantOptions,
settingsDefaultVariant,
uiAgentName,
]);
@@ -748,7 +769,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
}
manualVariantSelectionRef.current = true;
setCurrentVariant(variant);
setCurrentVariantOverride(
variant ?? null,
resolveInheritedVariantForModel(providerId, modelId, agentNameOverride),
);
addRecentEffort(providerId, modelId, variant);
const effectiveAgentName = agentNameOverride ?? resolveLiveAgentName();
@@ -759,9 +783,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
addRecentEffort,
currentSessionId,
getModelVariantOptions,
resolveInheritedVariantForModel,
resolveLiveAgentName,
saveAgentModelVariantForSession,
setCurrentVariant,
setCurrentVariantOverride,
]);
const applyModelSelectionWithVariant = React.useCallback((providerId: string, modelId: string, variant: string | undefined, agentNameOverride?: string | null) => {
@@ -1024,9 +1050,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
prevAgentNameRef.current = currentAgentName;
if (currentAgentName && currentSessionId) {
const shouldPreferAgentModel = explicitAgentSwitchRef.current === currentAgentName;
explicitAgentSwitchRef.current = null;
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 50);
abortController.signal.addEventListener('abort', () => {
@@ -1039,33 +1062,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return;
}
const selectedAgent = shouldPreferAgentModel
? agents.find((agent) => agent.name === currentAgentName)
: undefined;
if (selectedAgent?.model?.providerID && selectedAgent.model.modelID) {
const result = tryApplyModelSelection(
selectedAgent.model.providerID,
selectedAgent.model.modelID,
currentAgentName,
);
if (result === 'applied' || result === 'provider-missing') {
if (result === 'applied') {
saveSessionModelSelection(
currentSessionId,
selectedAgent.model.providerID,
selectedAgent.model.modelID,
);
saveAgentModelForSession(
currentSessionId,
currentAgentName,
selectedAgent.model.providerID,
selectedAgent.model.modelID,
);
}
return;
}
}
const persistedChoice = getAgentModelForSession(currentSessionId, currentAgentName);
if (persistedChoice) {
@@ -1091,12 +1087,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
abortController.abort();
};
}, [
agents,
currentAgentName,
currentSessionId,
getAgentModelForSession,
saveAgentModelForSession,
saveSessionModelSelection,
tryApplyModelSelection,
contextHydrated,
]);
@@ -1121,18 +1114,21 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
}
if (currentVariant && !availableVariants.includes(currentVariant)) {
setCurrentVariant(undefined);
setCurrentVariantOverride(
null,
resolveInheritedVariantForModel(currentProviderId, currentModelId),
);
return;
}
// Draft state (no session yet): seed from settings default, but don't override
// user selection while drafting.
if (!currentSessionId) {
if (!currentVariant && !manualVariantSelectionRef.current) {
if (currentVariantSelection.override === undefined && !manualVariantSelectionRef.current) {
const desired = settingsDefaultVariant && availableVariants.includes(settingsDefaultVariant)
? settingsDefaultVariant
: undefined;
setCurrentVariant(desired);
setCurrentVariantOverride(desired ?? null, desired);
}
return;
}
@@ -1144,13 +1140,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
currentModelId,
);
const resolvedSaved = savedVariant && availableVariants.includes(savedVariant)
? savedVariant
: settingsDefaultVariant && availableVariants.includes(settingsDefaultVariant)
? settingsDefaultVariant
: undefined;
setCurrentVariant(resolvedSaved);
const inheritedVariant = resolveInheritedVariantForModel(currentProviderId, currentModelId);
if (savedVariant && availableVariants.includes(savedVariant)) {
setCurrentVariantOverride(savedVariant, inheritedVariant);
} else if (currentVariantSelection.override === null) {
setCurrentVariantOverride(null, inheritedVariant);
} else {
setCurrentVariant(inheritedVariant);
}
manualVariantSelectionRef.current = false;
}, [
availableVariants,
@@ -1160,8 +1157,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
currentProviderId,
currentModelId,
currentVariant,
currentVariantSelection.override,
effectiveCurrentVariant,
getAgentModelVariantForSession,
resolveInheritedVariantForModel,
setCurrentVariant,
setCurrentVariantOverride,
settingsDefaultVariant,
]);
@@ -1177,7 +1178,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const handleAgentChange = React.useCallback((agentName: string, options?: { closeModelSelector?: boolean }) => {
try {
explicitAgentSwitchRef.current = agentName;
setAgent(agentName);
addRecentAgent(agentName);
if (options?.closeModelSelector ?? true) {
@@ -2248,7 +2248,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
: 'Default';
return (
<span className={cn('typography-micro whitespace-nowrap', wasAdjusted ? 'text-foreground' : 'text-muted-foreground')}>
<span className={cn(
'typography-micro whitespace-nowrap',
isHighlighted
? (wasAdjusted ? 'text-interactive-selection-foreground' : 'text-interactive-selection-foreground/70')
: (wasAdjusted ? 'text-foreground' : 'text-muted-foreground'),
)}>
Thinking: {displayLabel}
</span>
);
@@ -10,6 +10,10 @@ import { Icon } from "@/components/icon/Icon";
import { DiffPreview, WritePreview } from './DiffPreview';
import { useI18n } from '@/lib/i18n';
import { getVisiblePermissionPatterns } from './permissionCardPatterns';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
// Newest pending card owns the keyboard; older cards wait their turn.
const activePermissionCardIds: string[] = [];
const PERMISSION_BASH_CUSTOM_STYLE: React.CSSProperties = {
margin: 0,
@@ -126,6 +130,33 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
}
};
const handleResponseRef = React.useRef(handleResponse);
handleResponseRef.current = handleResponse;
React.useEffect(() => {
if (hasResponded) return;
activePermissionCardIds.push(permission.id);
const handleKeyDown = (event: KeyboardEvent) => {
if (activePermissionCardIds.at(-1) !== permission.id) return;
if (!event.altKey || event.metaKey || event.ctrlKey) return;
const response = event.key === 'Enter'
? (event.shiftKey ? 'always' as const : 'once' as const)
: event.key === 'Backspace' && !event.shiftKey
? 'reject' as const
: null;
if (!response) return;
event.preventDefault();
event.stopPropagation();
void handleResponseRef.current(response);
};
window.addEventListener('keydown', handleKeyDown, true);
return () => {
window.removeEventListener('keydown', handleKeyDown, true);
const index = activePermissionCardIds.lastIndexOf(permission.id);
if (index !== -1) activePermissionCardIds.splice(index, 1);
};
}, [hasResponded, permission.id]);
if (hasResponded) {
return null;
}
@@ -380,6 +411,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
>
<Icon name="check" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Allow Once
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+enter')}</kbd>
</button>
{permission.always.length > 0 ? (
@@ -436,6 +468,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
>
<Icon name="time" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Always Allow
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+shift+enter')}</kbd>
</button>
)}
@@ -459,6 +492,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
>
<Icon name="close" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Deny
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+backspace')}</kbd>
</button>
{isResponding && (
@@ -15,6 +15,7 @@ import * as sessionActions from '@/sync/session-actions';
import { useI18n } from '@/lib/i18n';
import { serializeQuestionAsJson, serializeQuestionAsMarkdown } from './questionSerializers';
import { QUESTION_CUSTOM_TEXTAREA_MIN_HEIGHT, getQuestionCustomTextareaHeight } from './questionTextareaSizing';
import { QuestionMarkdown } from './QuestionMarkdown';
interface QuestionCardProps {
question: QuestionRequest;
@@ -423,7 +424,11 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
</div>
) : activeQuestion ? (
<>
<div className="typography-meta font-medium text-foreground mb-1.5">{activeQuestion.question}</div>
<QuestionMarkdown
content={activeQuestion.question}
size="meta"
className="font-medium text-foreground mb-1.5"
/>
{isMultiple ? (
<div className="typography-micro text-muted-foreground mb-1.5">{t('chat.questionCard.selectMultiple')}</div>
@@ -0,0 +1,35 @@
import { describe, expect, test } from 'bun:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { QuestionMarkdown } from './QuestionMarkdown';
// The markdown renderer is lazy, so a synchronous server render always emits the
// Suspense fallback QuestionMarkdown supplies. That fallback is the surface that
// has to keep the exact question text and the question typography classes.
describe('QuestionMarkdown', () => {
test('renders the question content verbatim', () => {
const content = 'Choose **one** from `mode`: [details](https://example.com)';
const html = renderToStaticMarkup(<QuestionMarkdown content={content} size="meta" />);
expect(html).toBe(
`<div class="question-markdown typography-meta whitespace-pre-wrap">${content}</div>`,
);
});
test('applies meta typography and caller classes', () => {
const html = renderToStaticMarkup(
<QuestionMarkdown content="Meta" size="meta" className="font-medium text-foreground" />,
);
expect(html).toContain('class="question-markdown typography-meta font-medium text-foreground whitespace-pre-wrap"');
});
test('applies micro typography and caller classes', () => {
const html = renderToStaticMarkup(
<QuestionMarkdown content="Micro" size="micro" className="text-muted-foreground" />,
);
expect(html).toContain('class="question-markdown typography-micro text-muted-foreground whitespace-pre-wrap"');
});
});
@@ -0,0 +1,23 @@
import React from 'react';
import { cn } from '@/lib/utils';
import { SimpleMarkdownRenderer } from './MarkdownRenderer';
interface QuestionMarkdownProps {
content: string;
size: 'meta' | 'micro';
className?: string;
}
export function QuestionMarkdown({ content, size, className }: QuestionMarkdownProps) {
const classes = cn('question-markdown', size === 'meta' ? 'typography-meta' : 'typography-micro', className);
return (
<SimpleMarkdownRenderer
content={content}
variant="tool"
className={classes}
fallbackContent={<div className={cn(classes, 'whitespace-pre-wrap')}>{content}</div>}
/>
);
}
@@ -0,0 +1,112 @@
import React from 'react';
import { Icon } from '@/components/icon/Icon';
import { useI18n } from '@/lib/i18n';
import { useLatestSessionError } from '@/sync/notification-store';
import { useDirectoryStore, useSessionStatus } from '@/sync/sync-context';
interface SessionErrorNoticeProps {
sessionId: string;
directory?: string;
}
// How long a user message may sit unanswered on an idle session before the
// notice calls it a reply that never began.
const UNANSWERED_AFTER_MS = 5_000;
type LastMessageState = {
role: string;
timestamp: number;
hasError: boolean;
} | null;
// The last message of a session, with whether it already carries an error of
// its own: an assistant message that OpenCode marked failed renders its error
// inline, so the session-level notice must not repeat it.
const useLastMessageState = (sessionId: string, directory?: string): LastMessageState => {
const store = useDirectoryStore(directory);
const cacheRef = React.useRef<LastMessageState>(null);
const getSnapshot = React.useCallback((): LastMessageState => {
if (!sessionId) return null;
const messages = store.getState().message[sessionId];
const last = messages && messages.length > 0 ? messages[messages.length - 1] : null;
// SAFETY: store messages are SDK `Message` records; `error` is the optional
// assistant-message error the SDK types carry, read here only for presence.
const info = last as { role?: string; time?: { completed?: number; created?: number }; error?: unknown } | null;
if (!info) {
cacheRef.current = null;
return null;
}
const next: LastMessageState = {
role: typeof info.role === 'string' ? info.role : '',
timestamp: info.time?.completed ?? info.time?.created ?? 0,
hasError: Boolean(info.error),
};
const cached = cacheRef.current;
if (cached && cached.role === next.role && cached.timestamp === next.timestamp && cached.hasError === next.hasError) {
return cached;
}
cacheRef.current = next;
return next;
}, [sessionId, store]);
const subscribe = React.useCallback((notify: () => void) => {
if (!sessionId) return () => undefined;
return store.subscribe(notify);
}, [sessionId, store]);
return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
};
/**
* Shows what OpenCode reported when it stopped a turn without producing a
* reply. Rendered under the last message, only while that turn is the latest
* one: sending again moves the last message past the error and hides it.
*/
export const SessionErrorNotice: React.FC<SessionErrorNoticeProps> = ({ sessionId, directory }) => {
const { t } = useI18n();
const latestError = useLatestSessionError(sessionId);
const status = useSessionStatus(sessionId, directory);
const lastMessage = useLastMessageState(sessionId, directory);
const isIdle = !status || status.type === 'idle';
const reportedError = latestError && isIdle
&& (!lastMessage || latestError.time >= lastMessage.timestamp)
&& !(lastMessage?.role === 'assistant' && lastMessage.hasError)
? latestError
: null;
// A user message that the session is idle on, with nothing after it for a
// while, is a reply that never began: the send was accepted but OpenCode
// produced neither a message nor an error for it.
const unansweredSince = !reportedError && isIdle && lastMessage?.role === 'user' ? lastMessage.timestamp : null;
const [now, setNow] = React.useState(() => Date.now());
React.useEffect(() => {
if (unansweredSince === null) return undefined;
const remaining = UNANSWERED_AFTER_MS - (Date.now() - unansweredSince);
if (remaining <= 0) return undefined;
const timer = window.setTimeout(() => setNow(Date.now()), remaining + 50);
return () => window.clearTimeout(timer);
}, [unansweredSince]);
const unanswered = unansweredSince !== null && Math.max(now, Date.now()) - unansweredSince >= UNANSWERED_AFTER_MS;
if (!reportedError && !unanswered) return null;
const detail = reportedError
? (reportedError.error?.message ?? t('chat.sessionError.noDetails'))
: t('chat.sessionError.noDetails');
const name = reportedError?.error?.name;
return (
<div className="chat-message-column">
<div
role="status"
className="mt-3 max-w-full break-words rounded-2xl border border-[var(--status-error-border)] bg-[var(--status-error-background)] px-4 py-3 text-base leading-relaxed"
>
<div className="flex items-start gap-3">
<Icon name="error-warning" className="mt-0.5 size-4 shrink-0 text-[var(--status-error)]" />
<div className="min-w-0 flex-1 break-words">
<div className="font-medium text-foreground">{reportedError ? t('chat.sessionError.title') : t('chat.sessionError.noReply')}</div>
<div className="mt-1 text-foreground/80">{name ? `${name}: ${detail}` : detail}</div>
</div>
</div>
</div>
</div>
);
};
@@ -1,6 +1,7 @@
import React from 'react';
import { useSessionAssistState } from '@/hooks/useSessionAssist';
import { useI18n } from '@/lib/i18n';
import { TimelineRevealGateContext } from '@/components/chat/timelineRevealGate';
interface SessionRecapNoteProps {
sessionId: string;
@@ -12,8 +13,17 @@ interface SessionRecapNoteProps {
// the last message (above the reserved bottom gap). Appears only after the
// 1-minute quiet window, so the layout shift happens off-screen in practice.
export const SessionRecapNote: React.FC<SessionRecapNoteProps> = React.memo(({ sessionId, directory, isMobile }) => {
const { visibleRecap } = useSessionAssistState(sessionId, directory);
const { visibleRecap, sessionKnown } = useSessionAssistState(sessionId, directory);
const { t } = useI18n();
// The recap is part of the opened session's finished picture: until the
// session record is in memory it cannot be decided, and appearing a commit
// later would grow the footer under a viewport already pinned to the end.
const revealGate = React.useContext(TimelineRevealGateContext);
React.useLayoutEffect(() => {
if (sessionKnown) return undefined;
const release = revealGate?.hold();
return release ?? undefined;
}, [revealGate, sessionKnown]);
if (!visibleRecap) {
return null;
@@ -1,6 +1,7 @@
import React from 'react';
import { cn, fuzzyMatch } from '@/lib/utils';
import { useSkillsStore } from '@/stores/useSkillsStore';
import { selectSkillsForDirectory, useSkillsStore } from '@/stores/useSkillsStore';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { useUIStore } from '@/stores/useUIStore';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { useMobileAutocompleteMaxHeight } from './useMobileAutocompleteMaxHeight';
@@ -38,13 +39,16 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
const keyboardNavigationRef = React.useRef(false);
const [filteredSkills, setFilteredSkills] = React.useState<SkillInfo[]>([]);
const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
const skills = useSkillsStore((s) => s.skills);
// Skills of the directory the composer sends to (session directory, or the
// Chats root for a chat draft), not of the project the app was on last.
const effectiveDirectory = useEffectiveDirectory();
const skills = useSkillsStore((s) => selectSkillsForDirectory(s, effectiveDirectory));
const loadSkills = useSkillsStore((s) => s.loadSkills);
React.useEffect(() => {
// Always trigger loadSkills when autocomplete opens to ensure project context is fresh
void loadSkills();
}, [loadSkills]);
// Always trigger loadSkills when autocomplete opens to ensure the directory's skills are fresh
void loadSkills(effectiveDirectory);
}, [effectiveDirectory, loadSkills]);
React.useEffect(() => {
const normalizedQuery = searchQuery.trim();
+4 -18
View File
@@ -1,11 +1,9 @@
import React from "react";
import { useSessionUIStore } from '@/sync/session-ui-store';
import { WorkingPlaceholder } from "./message/parts/WorkingPlaceholder";
import { Icon } from "@/components/icon/Icon";
import { useI18n } from "@/lib/i18n";
// The floating assistant-status chip that hovers above the composer while the
// agent works ("Claude is working…", abort notice). ONLY that. The composer's
// agent works ("Claude is working…"). ONLY that. The composer's
// own bar — pending changes, todos dropdown — is ComposerStatusBar: they used
// to share this component, and every restyle of this chip (glass, placement)
// silently dragged the composer bar and its dropdown along with it.
@@ -17,10 +15,8 @@ interface StatusRowProps {
statusText?: string | null;
isGenericStatus?: boolean;
isWaitingForPermission?: boolean;
wasAborted?: boolean;
abortActive?: boolean;
retryInfo?: { attempt?: number; next?: number } | null;
showAbortStatus?: boolean;
agentName?: string;
modelName?: string | null;
providerId?: string | null;
@@ -31,19 +27,16 @@ export const StatusRow: React.FC<StatusRowProps> = ({
statusText = null,
isGenericStatus,
isWaitingForPermission,
wasAborted,
abortActive,
retryInfo,
showAbortStatus,
agentName,
modelName,
providerId,
}) => {
const { t } = useI18n();
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const shouldRenderPlaceholder = !showAbortStatus && (wasAborted || !abortActive);
const hasContent = isWorking || Boolean(wasAborted) || Boolean(showAbortStatus);
const shouldRenderPlaceholder = !abortActive;
const hasContent = isWorking;
if (!hasContent) {
return null;
@@ -63,14 +56,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
a shrink-to-fit wrapper around it always collapsed to zero. */}
<div className="oc-glass-popover inline-flex w-max max-w-full items-center gap-2 h-8 whitespace-nowrap rounded-full [corner-shape:round] px-3">
<div className="flex items-center min-w-0 gap-2 overflow-x-hidden">
{showAbortStatus ? (
<div className="flex h-full items-center text-[var(--status-error)] pl-0.5">
<span className="flex items-center gap-1.5 typography-ui-label">
<Icon name="close-circle" aria-hidden="true"/>
{t('chat.statusRow.aborted')}
</span>
</div>
) : shouldRenderPlaceholder ? (
{shouldRenderPlaceholder ? (
<WorkingPlaceholder
key={currentSessionId ?? "no-session"}
isWorking={isWorking}
@@ -2,7 +2,6 @@ import React from 'react';
import { useAssistantStatus } from '@/hooks/useAssistantStatus';
import { useConfigStore } from '@/stores/useConfigStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { getProviderModelDisplayName } from '@/lib/modelDisplay';
import { StatusRow } from './StatusRow';
@@ -12,15 +11,6 @@ import { StatusRow } from './StatusRow';
* labels while still limiting subscriptions to the active assistant message.
*/
export const StatusRowContainer: React.FC = React.memo(() => {
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const abortRecord = useSessionUIStore(
React.useCallback((state) => {
if (!currentSessionId) {
return null;
}
return state.sessionAbortFlags?.get(currentSessionId) ?? null;
}, [currentSessionId]),
);
const { activeModel, working } = useAssistantStatus();
const currentAgentName = useConfigStore((state) => state.currentAgentName);
const providers = useConfigStore((state) => state.providers);
@@ -35,16 +25,13 @@ export const StatusRowContainer: React.FC = React.memo(() => {
return getProviderModelDisplayName(provider, activeModel.modelId) || null;
}, [activeModel, providers]);
const wasAborted = Boolean(abortRecord && !abortRecord.acknowledged);
return (
<StatusRow
isWorking={working.isWorking}
statusText={working.statusText}
isGenericStatus={working.isGenericStatus}
isWaitingForPermission={working.isWaitingForPermission}
wasAborted={wasAborted || working.wasAborted}
abortActive={wasAborted || working.abortActive}
abortActive={working.abortActive}
retryInfo={working.retryInfo}
agentName={currentAgentName}
modelName={modelDisplayName}
@@ -301,7 +301,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
</div>
) : (
filteredMessages.map(({ message }, index) => {
const preview = getMessagePreview(message.parts);
const preview = getMessagePreview(message.parts, undefined, t);
const timestamp = message.info.time.created;
const dateGroup = formatDateGroup(timestamp);
const previous = filteredMessages[index - 1];
@@ -5,6 +5,7 @@ import {
buildAttachmentCitationText,
findAttachmentCitationRanges,
isGenericImageFilename,
nextPastedContextFilename,
} from '../attachmentCitations';
describe('attachment citations', () => {
@@ -53,4 +54,10 @@ describe('attachment citations', () => {
['desktop.jpg'],
)).toEqual([{ start: 8, end: 21 }]);
});
test('assigns sequential pasted-context filenames', () => {
expect(nextPastedContextFilename([])).toBe('pasted-context-1.txt');
expect(nextPastedContextFilename(['pasted-context-1.txt', 'notes.md'])).toBe('pasted-context-2.txt');
expect(nextPastedContextFilename(['PASTED-CONTEXT-2.TXT'])).toBe('pasted-context-1.txt');
});
});

Some files were not shown because too many files have changed in this diff Show More