feat: add complete French localization (#1482)
* feat: add French locale runtime Add French to OpenChamber's shared i18n runtime, dictionaries, and parity tests so the existing language picker can load a complete fr locale across shared UI surfaces. * fix: localize shared UI formatting Remove remaining shared UI locale hardcodings so dates, numbers, and first-party helper copy follow the active app locale instead of leaking English on French surfaces. * feat: localize VS Code French surfaces Localize VS Code bootstrap, native runtime messages, panel titles, and manifest contribution strings so French users get consistent first-party copy across the extension experience. * fix: TASK-2026-05-30-008 correct French review findings Fix broken French relative-time and weekday strings reported on PR #1482 and restore proper import order in quota utils without broadening scope. * fix: TASK-2026-05-30-008 address final PR review comments Capture the localized More Info label once in the VS Code CLI-missing flow and replace the remaining inline French-only utility strings with dictionary-driven copy plus required locale keys. * fix: TASK-2026-05-30-008 normalize French glossary Correct glossary-level French terminology on the live PR branch, keeping canonical technical terms like PR, worktree, stash, HEAD, Mermaid, Markdown, remote, and session while replacing misleading literal translations. * fix: TASK-2026-05-30-008 refine French terminology pass Clean up remaining glossary mistakes on the French PR branch, especially around Mermaid, Markdown, PR, worktree, stash, branch, remote, and commit terminology, while keeping behavior unchanged. * fix: TASK-2026-05-30-008 clean remaining French false friends Correct the SOCKS5 mistranslation and a final small set of obvious false-friend technical nouns on the French branch without changing behavior. * fix: TASK-2026-05-30-008 correct French glossary terms Replace remaining false-friend translations in the French UI dictionaries and normalize technical labels for the French PR branch. * fix: TASK-2026-05-30-008 remove remaining French Mermaid false friend Replace the last confirmed Sirène translation with Mermaid and re-run the requested blacklist and build verification on the PR branch. * fix: TASK-2026-05-30-008 enforce French glossary policy Keep skill/PR/worktree/remote terminology developer-credible in French and remove remaining machine-translated Git and settings copy. * fix: TASK-2026-05-30-008 keep prompt terminology in French Replace remaining technical invite translations with prompt wording across scheduled tasks, multi-run, prompt templates, and Magic Prompts. * fix: TASK-2026-05-30-008 finalize French terminology cleanup Polish remaining worktree/remote wording, remove visible metadata leakage, and correct final Git and settings labels on the French PR branch. * fix: TASK-2026-05-30-008 polish final French strings Correct the last aria-like artifacts and awkward worktree/remote/GitHub URL phrasing in the French dictionaries. * fix: TASK-2026-05-30-008 normalize final French glossary framing Tighten the last worktree/remote/checkout wording and fix remaining French grammar around canonical technical terms. * fix: TASK-2026-05-30-008 align final developer glossary wording Normalize the last French framing around canonical developer terms like worktree, remote, prompt, and checkout. * fix: TASK-2026-05-30-008 harmonize final French sentence framing Replace the last raw franglais around checkout, remote, worktree, and prompt-facing labels with more natural French framing while keeping the chosen technical terms. * fix: TASK-2026-05-30-008 add compact relative date keys Replace French-specific prefix stripping in compact session date labels with dedicated i18n keys across locale dictionaries, preserving existing compact label output while making French wording robust. * docs: add French documentation * docs: mention French locale folder --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
85bf7c7563
commit
49a1424e5f
@@ -202,12 +202,13 @@ other language mirrors the English files under a locale folder.
|
||||
| Brazilian Portuguese | `pt-br/` | `pt-BR` |
|
||||
| Korean | `ko/` | `ko` |
|
||||
| Polish | `pl/` | `pl` |
|
||||
| French | `fr/` | `fr` |
|
||||
|
||||
> [!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`, `en`) is identical
|
||||
> different rules. Everything else (`uk`, `es`, `ko`, `pl`, `fr`, `en`) is identical
|
||||
> in both columns.
|
||||
|
||||
This locale set is mirrored in the website at
|
||||
@@ -228,6 +229,7 @@ content/docs/
|
||||
pt-br/install.mdx # Brazilian Portuguese
|
||||
ko/install.mdx # Korean
|
||||
pl/install.mdx # Polish
|
||||
fr/install.mdx # French
|
||||
|
||||
guides/tunnels.mdx # nested English page
|
||||
uk/guides/tunnels.mdx # its Ukrainian translation
|
||||
@@ -263,7 +265,8 @@ to each section and item in `sidebar.config.json`:
|
||||
"es": "Empieza aquí",
|
||||
"pt-BR": "Comece aqui",
|
||||
"ko": "여기서 시작",
|
||||
"pl": "Zacznij tutaj"
|
||||
"pl": "Zacznij tutaj",
|
||||
"fr": "Commencer ici"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
@@ -275,7 +278,8 @@ to each section and item in `sidebar.config.json`:
|
||||
"es": "Instalación",
|
||||
"pt-BR": "Instalação",
|
||||
"ko": "설치",
|
||||
"pl": "Instalacja"
|
||||
"pl": "Instalacja",
|
||||
"fr": "Installation"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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/`); see `CONTRIBUTING.md` → Localization
|
||||
(e.g. `uk/`, `zh-cn/`, `pt-br/`, `fr/`); 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
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Commandes et snippets
|
||||
description: Créez des commandes slash et snippets de texte réutilisables pour le chat.
|
||||
---
|
||||
|
||||
# Commandes et snippets
|
||||
|
||||
Les commandes et les snippets vous évitent de retaper la même chose. Les commandes sont des prompts complets que vous déclenchez avec `/` ; les snippets sont des morceaux de texte que vous insérez dans un message avec `#`.
|
||||
|
||||
## Commandes
|
||||
|
||||
Une commande est un prompt enregistré que vous lancez avec un slash, comme `/review`. Gérez-les dans **Paramètres → Commandes**.
|
||||
|
||||
1. Ouvrez **Paramètres → Commandes** et créez une commande.
|
||||
2. Donnez-lui un nom, une description et le texte de prompt qu’elle doit envoyer.
|
||||
3. Épinglez-la éventuellement à un agent ou modèle précis.
|
||||
4. Choisissez un scope personnel ou projet.
|
||||
|
||||
Dans le chat, tapez `/` comme **premier** caractère du message pour afficher les commandes, puis choisissez-en une. Votre texte peut utiliser des placeholders :
|
||||
|
||||
- `$ARGUMENTS` — tout ce que vous tapez après la commande
|
||||
- `@filename` — insère le contenu d’un fichier
|
||||
- `` !`command` `` — insère la sortie d’une commande shell
|
||||
|
||||
Les commandes intégrées `init` et `review` peuvent être réinitialisées mais pas supprimées.
|
||||
|
||||
## Snippets
|
||||
|
||||
Un snippet est un texte réutilisable que vous référencez inline avec un hashtag, comme `#signoff`. Gérez-les dans **Paramètres → Snippets**.
|
||||
|
||||
1. Ouvrez **Paramètres → Snippets** et créez un snippet.
|
||||
2. Donnez-lui un nom et le texte qu’il représente. Ajoutez des alias si vous voulez plusieurs déclencheurs.
|
||||
3. Choisissez un scope personnel ou projet.
|
||||
|
||||
Dans le chat, tapez `#` et choisissez un snippet. OpenChamber remplace ce raccourci par le texte complet avant l’envoi.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Skills](/skills/) — charger de plus grands ensembles d’instructions à la demande
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Contexte
|
||||
description: Voyez quelle part de la mémoire du modèle une session utilise.
|
||||
---
|
||||
|
||||
# Contexte
|
||||
|
||||
Chaque modèle ne peut garder qu’une certaine quantité de conversation à la fois — son contexte. OpenChamber montre à quel point il est rempli, pour que vous sachiez quand une session approche de la limite et qu’une réponse risque de perdre des détails plus anciens.
|
||||
|
||||
## L’indicateur rapide
|
||||
|
||||
Pendant que vous discutez, une petite jauge affiche le pourcentage de contexte utilisé. Elle change de couleur à mesure qu’elle se remplit :
|
||||
|
||||
- vert — beaucoup de marge
|
||||
- jaune — presque plein (environ trois quarts)
|
||||
- rouge — presque à la limite
|
||||
|
||||
Survolez-la (ou touchez-la sur mobile) pour voir les nombres exacts de tokens.
|
||||
|
||||
## Le panneau de contexte complet
|
||||
|
||||
Ouvrez l’onglet **Contexte** dans la barre latérale droite pour une vue plus complète de la session actuelle :
|
||||
|
||||
- le modèle utilisé et l’heure de démarrage de la session
|
||||
- le total de tokens par rapport à la limite du modèle
|
||||
- les totaux de messages et de coûts
|
||||
- le détail des tokens de la dernière réponse
|
||||
- une répartition approximative de ce qui occupe le contexte (vos messages, ceux de l’agent, la sortie des outils)
|
||||
|
||||
La répartition est une estimation, pas un comptage exact — utilisez-la pour repérer ce qui remplit la fenêtre, pas pour la facturation.
|
||||
|
||||
## Que faire quand il est plein
|
||||
|
||||
Démarrez une nouvelle session pour une nouvelle tâche au lieu de laisser une seule session grossir indéfiniment. Un contexte plus court est plus rapide et garde le modèle concentré.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Projets](/projects/) — les sessions sont groupées par projet
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: Navigateur desktop
|
||||
description: Parcourez n’importe quelle page dans l’application desktop, avec inspection et capture de console.
|
||||
---
|
||||
|
||||
# Navigateur desktop
|
||||
|
||||
L’application desktop possède un navigateur intégré pour ouvrir n’importe quelle page juste à côté de votre chat, pointer des éléments pour poser des questions à leur sujet et capturer la console de la page. Ouvrez-le avec le bouton globe dans l’en-tête de l’application.
|
||||
|
||||
> Le navigateur desktop est une fonctionnalité **desktop uniquement**. Sur le web, le panneau [aperçu](/preview/) offre les mêmes outils d’inspection et de console pour votre serveur de dev local.
|
||||
|
||||
## Inspecter et annoter
|
||||
|
||||
Activez **inspect** et cliquez sur n’importe quel élément de la page. OpenChamber capture une note à son sujet — ce que c’est, ses styles, sa position et une capture d’écran — puis l’attache à votre message de chat. C’est le moyen le plus rapide de dire à l’agent « cet élément, juste ici ».
|
||||
|
||||
## Capture de console
|
||||
|
||||
Le navigateur collecte la sortie console de la page — erreurs, avertissements et logs — pour que vous puissiez la filtrer et la lire sans ouvrir les outils développeur.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Aperçu et serveurs de dev](/preview/) — les mêmes outils pour votre serveur de dev local
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Tunnels desktop
|
||||
description: Créez des tunnels Cloudflare ou Ngrok depuis l’application desktop.
|
||||
---
|
||||
|
||||
# Tunnels desktop
|
||||
|
||||
L’application desktop peut créer un tunnel public depuis **Paramètres → OpenChamber → Tunnel**. Vous n’avez pas besoin de démarrer OpenChamber depuis la CLI pour ce chemin.
|
||||
|
||||
## Installer un fournisseur
|
||||
|
||||
OpenChamber démarre la CLI du fournisseur sur votre machine. Installez d’abord le fournisseur que vous voulez utiliser :
|
||||
|
||||
```bash
|
||||
brew install cloudflared
|
||||
brew install ngrok
|
||||
```
|
||||
|
||||
Cloudflare utilise `cloudflared`. Ngrok nécessite un compte ngrok et un authtoken depuis le dashboard ngrok :
|
||||
|
||||
```bash
|
||||
ngrok config add-authtoken <your-ngrok-token>
|
||||
```
|
||||
|
||||
## Démarrer depuis l’application
|
||||
|
||||
1. Ouvrez **Paramètres → OpenChamber → Tunnel**.
|
||||
2. Choisissez **Cloudflare** ou **Ngrok**.
|
||||
3. Démarrez un tunnel rapide.
|
||||
4. Scannez le QR code généré depuis votre téléphone.
|
||||
|
||||
Ngrok prend actuellement en charge les tunnels rapides. Cloudflare prend en charge les tunnels rapides et les modes Cloudflare gérés.
|
||||
|
||||
## Protection de l’accès
|
||||
|
||||
Même lorsque l’URL du fournisseur est publique, OpenChamber protège toujours l’accès avec son propre token de connexion. Le lien de connexion généré contient un token à usage unique, possède une TTL, et les anciens liens inutilisés sont révoqués lorsque vous en générez un nouveau ou que vous arrêtez/redémarrez le tunnel.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Tunnels](/tunnels/) — utilisation des tunnels CLI et modes Cloudflare gérés
|
||||
- [PWA et mobile](/mobile/) — joindre OpenChamber depuis votre téléphone
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: Variables d’environnement
|
||||
description: Configurez OpenChamber et l’intégration OpenCode avec des variables d’environnement.
|
||||
---
|
||||
|
||||
# Variables d’environnement
|
||||
|
||||
OpenChamber lit ces variables d’environnement au démarrage. Pour les services de démarrage, `openchamber startup enable` capture l’environnement actuel par défaut ; relancez donc la commande après avoir changé les variables que le service doit utiliser.
|
||||
|
||||
## Serveur OpenChamber
|
||||
|
||||
### `OPENCHAMBER_HOST`
|
||||
|
||||
Adresse d’écoute du serveur web OpenChamber. Utilisez `0.0.0.0` pour autoriser l’accès depuis d’autres machines.
|
||||
|
||||
### `OPENCHAMBER_UI_PASSWORD`
|
||||
|
||||
Mot de passe pour l’UI navigateur. Utilisez-le lorsque vous écoutez hors de localhost, utilisez des tunnels ou passez derrière un reverse proxy.
|
||||
|
||||
### `OPENCHAMBER_API_ONLY`
|
||||
|
||||
Démarre OpenChamber en mode headless lorsqu’il vaut `true` ou `1`. Les routes API restent disponibles pour les clients desktop et mobile, mais l’UI navigateur n’est pas servie.
|
||||
|
||||
### `OPENCHAMBER_DATA_DIR`
|
||||
|
||||
Remplace le dossier de données OpenChamber. La valeur par défaut est `~/.config/openchamber`.
|
||||
|
||||
### `OPENCHAMBER_COMPRESS_API`
|
||||
|
||||
Contrôle la compression des réponses API. Utilisez `true` ou `1` pour la forcer, `false` ou `0` pour la désactiver.
|
||||
|
||||
### `OPENCHAMBER_SKIP_API_COMPRESSION`
|
||||
|
||||
Désactive la compression des réponses API lorsqu’il vaut `true` ou `1`. Cette variable a priorité sur `OPENCHAMBER_COMPRESS_API`.
|
||||
|
||||
### `OPENCHAMBER_VERBOSE_REQUEST_LOGS`
|
||||
|
||||
Active les logs HTTP détaillés lorsqu’il vaut `true` ou `1`.
|
||||
|
||||
### `OPENCHAMBER_UPDATE_API_URL`
|
||||
|
||||
Remplace l’endpoint API de vérification des mises à jour. La plupart des utilisateurs doivent laisser cette variable non définie.
|
||||
|
||||
### `OPENCHAMBER_PACKAGE_MANAGER`
|
||||
|
||||
Force le gestionnaire de paquets utilisé par les opérations de mise à jour lorsque l’auto-détection se trompe.
|
||||
|
||||
## Serveur OpenCode
|
||||
|
||||
### `OPENCODE_HOST`
|
||||
|
||||
Connecte OpenChamber à un serveur OpenCode existant. La valeur doit être une origine `http` ou `https` avec un port explicite, sans chemin, query ni hash. `OPENCODE_HOST` a priorité sur `OPENCODE_PORT`.
|
||||
|
||||
### `OPENCODE_PORT`
|
||||
|
||||
Définit le port du serveur OpenCode. Avec OpenCode géré, cela demande le port géré ; avec `OPENCODE_SKIP_START=true`, OpenChamber se connecte à un serveur externe sur ce port.
|
||||
|
||||
### `OPENCODE_SKIP_START`
|
||||
|
||||
Empêche OpenChamber de démarrer son propre serveur OpenCode lorsqu’il vaut `true`.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_HOSTNAME`
|
||||
|
||||
Nom d’hôte d’écoute du serveur OpenCode géré par OpenChamber. La valeur par défaut est `127.0.0.1`.
|
||||
|
||||
### `OPENCODE_BINARY`
|
||||
|
||||
Chemin vers l’exécutable `opencode` qu’OpenChamber doit lancer.
|
||||
|
||||
### `OPENCODE_CONFIG`
|
||||
|
||||
Chemin vers un fichier de configuration OpenCode précis.
|
||||
|
||||
### `OPENCODE_CONFIG_DIR`
|
||||
|
||||
Chemin vers un dossier de configuration OpenCode précis pour la découverte des agents, skills, snippets et configurations.
|
||||
|
||||
### `OPENCODE_DATA_DIR`
|
||||
|
||||
Dossier de données personnalisé pour le serveur OpenCode géré.
|
||||
|
||||
### `OPENCODE_WSL_DISTRO`
|
||||
|
||||
Sélectionne la distribution WSL utilisée pour l’intégration OpenCode sur Windows.
|
||||
|
||||
### `OPENCHAMBER_OPENCODE_WSL_DISTRO`
|
||||
|
||||
Alias propre à OpenChamber pour sélectionner la distribution WSL. `OPENCODE_WSL_DISTRO` a priorité lorsque les deux sont définies.
|
||||
|
||||
### `OPENCODE_JWT_SECRET`
|
||||
|
||||
Secret utilisé pour signer les tokens d’authentification UI. Utilisez une longue valeur aléatoire pour les déploiements de service persistants.
|
||||
|
||||
## Terminal et Git
|
||||
|
||||
### `OPENCHAMBER_TERMINAL_SHELL`
|
||||
|
||||
Exécutable shell utilisé pour les sessions de terminal OpenChamber.
|
||||
|
||||
### `OPENCHAMBER_GIT_BINARY`
|
||||
|
||||
Exécutable Git utilisé par les fonctionnalités Git d’OpenChamber.
|
||||
|
||||
### `GIT_BINARY`
|
||||
|
||||
Autre exécutable Git de remplacement. Préférez `OPENCHAMBER_GIT_BINARY` pour une configuration propre à OpenChamber.
|
||||
|
||||
### `OPENCHAMBER_GIT_READ_CACHE_TTL_MS`
|
||||
|
||||
Durée de vie, en millisecondes, du cache de lecture des fichiers adossés à Git. Définissez `0` pour désactiver ce cache pendant le débogage.
|
||||
|
||||
## Voix et tunnels
|
||||
|
||||
### `OPENAI_API_KEY`
|
||||
|
||||
Clé API utilisée par les fonctionnalités vocales d’OpenChamber qui appellent des services compatibles OpenAI.
|
||||
|
||||
### `OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS`
|
||||
|
||||
Autorise les URL de base compatibles OpenAI distantes pour les fonctionnalités vocales lorsqu’elle vaut `true` ou `1`.
|
||||
|
||||
### `NGROK_AUTHTOKEN`
|
||||
|
||||
Token d’authentification ngrok utilisé par les commandes de tunnel OpenChamber. Vous pouvez aussi configurer ngrok avec `ngrok config add-authtoken <token>`.
|
||||
|
||||
## Helpers runtime
|
||||
|
||||
### `BUN_BINARY`
|
||||
|
||||
Exécutable Bun qu’OpenChamber doit utiliser lorsqu’il lance des processus daemon.
|
||||
|
||||
### `BUN_INSTALL`
|
||||
|
||||
Racine d’installation de Bun. OpenChamber l’utilise pour trouver `bin/bun` lors du démarrage des daemons et des mises à jour.
|
||||
|
||||
### `VITE_OPENCODE_URL`
|
||||
|
||||
URL de base API à la compilation pour l’application web construite par Vite. La plupart des utilisateurs ne doivent pas la définir pour un usage CLI ou desktop normal.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Identités Git
|
||||
description: Commitez avec le bon nom et la bonne adresse e-mail pour chaque dépôt.
|
||||
---
|
||||
|
||||
# Identités Git
|
||||
|
||||
Une identité git est le nom et l’adresse e-mail avec lesquels vos commits sont signés. Si vous travaillez sur des dépôts personnels et professionnels, vous pouvez enregistrer des identités et appliquer la bonne par dépôt, au lieu de dépendre d’un seul réglage global. Gérez-les dans **Paramètres → Git**.
|
||||
|
||||
## Ajouter une identité
|
||||
|
||||
1. Ouvrez **Paramètres → Git** et choisissez **New**.
|
||||
2. Saisissez le **nom** et l’**e-mail** à utiliser pour les commits.
|
||||
3. Choisissez comment elle s’authentifie auprès du remote :
|
||||
- **SSH** — pointez vers une clé SSH
|
||||
- **token** — utilisez un identifiant enregistré pour un host
|
||||
4. Donnez-lui éventuellement une couleur et une icône pour la repérer facilement.
|
||||
|
||||
L’identité globale de votre système est aussi affichée, en lecture seule.
|
||||
|
||||
## Appliquer une identité à un dépôt
|
||||
|
||||
Appliquer une identité l’écrit dans la configuration git **locale** de ce dépôt — cela ne touche que ce dépôt, pas votre réglage global. Les identités SSH définissent aussi la commande SSH pour utiliser votre clé ; les identités par token configurent le stockage d’identifiants pour le host.
|
||||
|
||||
Vous pouvez importer les identités qu’OpenChamber découvre dans vos identifiants git existants et les enregistrer comme identités par token.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Workflows Git et GitHub](/git/) — commiter avec l’identité définie
|
||||
- [Issues et PR GitHub](/github/) — connecter un compte GitHub pour les PR
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Workflows Git et GitHub
|
||||
description: Stagez, commitez et gérez les branches sans quitter OpenChamber.
|
||||
---
|
||||
|
||||
# Workflows Git et GitHub
|
||||
|
||||
OpenChamber possède une vue git intégrée pour relire les changements, commiter et gérer les branches sans passer au terminal. Ouvrez-la depuis l’onglet **Git** dans la barre latérale droite.
|
||||
|
||||
## Relire et commiter
|
||||
|
||||
La vue git sépare vos changements en **staged** et **unstaged** :
|
||||
|
||||
- cliquez sur le **+** d’un fichier pour le stage, ou sur **−** pour le retirer du stage
|
||||
- stagez ou retirez du stage tout un groupe à la fois
|
||||
- cliquez sur un fichier pour voir son diff
|
||||
|
||||
Écrivez ensuite un message de commit et commitez. Vous pouvez demander à OpenChamber de **générer un message de commit** à partir de vos changements staged — il utilise le modèle de la session actuelle, donc une session doit être ouverte.
|
||||
|
||||
## Branches et historique
|
||||
|
||||
La vue git couvre aussi le reste du quotidien git :
|
||||
|
||||
- créer, changer, renommer et supprimer des branches
|
||||
- push, pull et fetch
|
||||
- parcourir l’historique et les diffs par commit
|
||||
- stash et restaurer des changements
|
||||
|
||||
## Pull requests
|
||||
|
||||
Connectez GitHub (voir [Issues et PR GitHub](/github/)) et l’onglet **PR** vous permet d’ouvrir une pull request, de la mettre à jour, de la marquer comme prête ou de la merger — et de générer son titre et sa description comme pour les messages de commit.
|
||||
|
||||
## Gérer les conflits
|
||||
|
||||
Si un merge, rebase ou integrate tombe sur un conflit, OpenChamber montre ce qui est bloqué et vous laisse le résoudre — y compris en le confiant à l’agent.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Issues et PR GitHub](/github/) — connecter GitHub et démarrer le travail depuis des issues
|
||||
- [Sessions worktree](/worktrees/) — isoler une branche dans son propre dossier
|
||||
- [Identités Git](/git-identities/) — commiter avec la bonne identité par dépôt
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Issues et PR GitHub
|
||||
description: Connectez GitHub et démarrez des sessions depuis des issues et pull requests.
|
||||
---
|
||||
|
||||
# Issues et PR GitHub
|
||||
|
||||
Connectez votre compte GitHub et OpenChamber peut récupérer les issues et pull requests, démarrer une session directement depuis l’une d’elles, et ouvrir ou mettre à jour des PR pour vous.
|
||||
|
||||
## Connecter GitHub
|
||||
|
||||
1. Ouvrez **Paramètres → Git**.
|
||||
2. Sous GitHub, choisissez **Connect**. OpenChamber affiche un lien et un code court.
|
||||
3. Ouvrez le lien, saisissez le code et approuvez.
|
||||
|
||||
Une fois connecté, votre compte apparaît dans la section GitHub. Vous pouvez connecter plusieurs comptes et passer de l’un à l’autre, ou vous déconnecter à tout moment.
|
||||
|
||||
## Démarrer le travail depuis une issue ou une PR
|
||||
|
||||
Lorsque vous créez une [session worktree](/worktrees/) avec GitHub connecté, vous pouvez choisir **Start from GitHub issue/PR** :
|
||||
|
||||
- choisissez une **issue** et OpenChamber nomme la branche d’après elle puis ouvre la session avec l’issue et ses commentaires comme premier message
|
||||
- choisissez une **pull request** et il récupère la branche de la PR ; vous pouvez inclure le diff de la PR pour que l’agent ait tout le changement
|
||||
|
||||
Vous arrivez directement dans une session avec le contexte déjà chargé.
|
||||
|
||||
## Ouvrir et gérer des pull requests
|
||||
|
||||
Depuis l’onglet **PR** de la [vue git](/git/), vous pouvez créer une pull request, la mettre à jour, marquer un brouillon comme prêt ou la merger. OpenChamber peut générer le titre et la description de la PR à partir de vos changements.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Workflows Git et GitHub](/git/) — commiter et gérer les branches
|
||||
- [Sessions worktree](/worktrees/) — là où démarrent les sessions issues et PR
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Documentation OpenChamber
|
||||
description: Guide d’installation et d’exploitation d’OpenChamber pour le web, le desktop et VS Code.
|
||||
---
|
||||
|
||||
# Documentation OpenChamber
|
||||
|
||||
OpenChamber est l’espace de travail visuel autour d’OpenCode (l’agent de codage IA qui tourne dans votre terminal). Il vous donne un écran pour suivre et piloter ce travail, au lieu de tout faire en ligne de commande.
|
||||
|
||||
Utilisez cette documentation pour :
|
||||
|
||||
- installer l’application qui correspond à votre façon de travailler
|
||||
- ouvrir OpenChamber pour un usage distant sûr
|
||||
- personnaliser l’apparence et corriger les problèmes courants
|
||||
|
||||
## À lire d’abord
|
||||
|
||||
- [Installation](/install/)
|
||||
- [Démarrage rapide](/quickstart/)
|
||||
- [Tunnels](/tunnels/)
|
||||
- [Dépannage](/troubleshooting/)
|
||||
|
||||
## Explorer
|
||||
|
||||
- [Projets](/projects/) et [sessions worktree](/worktrees/) — organiser et isoler votre travail
|
||||
- [Fournisseurs, modèles et agents](/providers/) — connecter OpenCode et choisir un modèle
|
||||
- [Workflows Git et GitHub](/git/) — commiter, relire et ouvrir des PR
|
||||
- [Sécurité](/security/) et [tunnels](/tunnels/) — protéger et joindre votre instance
|
||||
|
||||
## À quoi sert OpenChamber
|
||||
|
||||
OpenChamber sert aux parties du codage IA qui gagnent à être pilotées depuis une salle de contrôle : sessions par branche, revue des diffs, gestion des terminaux, suivi de l’avancement des outils, lancement d’actions de projet et vue d’ensemble pendant que l’agent travaille.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Installez OpenChamber pour le desktop, le web ou VS Code.
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
Il existe trois façons d’utiliser OpenChamber :
|
||||
|
||||
- l’application desktop pour macOS
|
||||
- l’application web servie par la CLI, que vous pouvez installer comme une application de téléphone (une PWA)
|
||||
- l’extension VS Code
|
||||
|
||||
## Prérequis
|
||||
|
||||
Installez d’abord [OpenCode](https://opencode.ai) — OpenChamber fonctionne par-dessus.
|
||||
|
||||
## Web + PWA
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/openchamber/openchamber/main/scripts/install.sh | bash
|
||||
openchamber --ui-password be-creative-here
|
||||
```
|
||||
|
||||
Ouvrez l’URL affichée par la CLI (souvent `http://localhost:3000`). Vous devriez voir la liste des sessions OpenChamber. Pour la garder à portée de main, utilisez l’option « Installer » de votre navigateur dans la barre d’adresse pour l’ajouter comme application.
|
||||
|
||||
## Desktop
|
||||
|
||||
Téléchargez la dernière version desktop depuis la page des releases GitHub ou la page de téléchargement OpenChamber. Ouvrez-la et connectez-la à votre workflow OpenCode habituel.
|
||||
|
||||
## VS Code
|
||||
|
||||
Installez l’extension depuis le VS Code Marketplace et connectez-la à votre workflow OpenCode habituel. La vue OpenChamber s’ouvre ensuite dans la barre latérale.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Magic Prompts
|
||||
description: Personnalisez les prompts intégrés derrière les flux automatisés d’OpenChamber.
|
||||
---
|
||||
|
||||
# Magic Prompts
|
||||
|
||||
OpenChamber utilise des prompts intégrés en arrière-plan chaque fois qu’il fait quelque chose automatiquement — écrire un message de commit, rédiger une PR, relire une issue, résoudre un conflit, résumer une session. Magic Prompts est l’endroit où vous lisez et réécrivez ces prompts. Ouvrez-le dans **Paramètres → Magic Prompts**.
|
||||
|
||||
Vous n’avez pas besoin de cette page pour un usage normal. Utilisez-la lorsque vous voulez qu’un flux se comporte autrement — par exemple des messages de commit dans un style précis.
|
||||
|
||||
## Modifier un prompt
|
||||
|
||||
1. Ouvrez **Paramètres → Magic Prompts**.
|
||||
2. Choisissez un prompt dans les groupes de la barre latérale — Git, GitHub, Planning et Session.
|
||||
3. Modifiez le texte et enregistrez.
|
||||
|
||||
Certains prompts ont une partie visible (le message que vous verriez) et une partie instructions (des consignes cachées pour l’agent). Les prompts peuvent contenir des `{{placeholders}}` qu’OpenChamber remplit, comme le diff ou le titre de l’issue — gardez-les en place.
|
||||
|
||||
## Réinitialiser
|
||||
|
||||
Vous avez changé d’avis ? Chaque prompt possède **reset to default**, et il existe aussi **reset all** si vous voulez tout reprendre depuis le début.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Workflows Git et GitHub](/git/) — beaucoup de ces prompts alimentent les flux git
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Serveurs MCP
|
||||
description: Ajoutez des serveurs MCP pour donner des outils supplémentaires aux agents.
|
||||
---
|
||||
|
||||
# Serveurs MCP
|
||||
|
||||
Un serveur MCP donne des outils supplémentaires à vos agents — par exemple chercher dans une base de données, appeler une API ou lire un service que vous utilisez. Ajoutez-les dans **Paramètres → MCP**.
|
||||
|
||||
## Ajouter un serveur
|
||||
|
||||
1. Ouvrez **Paramètres → MCP**.
|
||||
2. Ajoutez un serveur et choisissez son type :
|
||||
- **local** — OpenChamber lance une commande sur votre machine. Vous fournissez la commande à exécuter et, si nécessaire, des variables d’environnement.
|
||||
- **remote** — OpenChamber se connecte à une URL hébergée par quelqu’un d’autre. Vous fournissez l’URL et les headers nécessaires (par exemple un token d’authentification).
|
||||
3. Enregistrez. Le serveur est activé par défaut ; vous pouvez le désactiver sans le supprimer.
|
||||
|
||||
## Où il s’applique
|
||||
|
||||
Choisissez le scope quand vous ajoutez un serveur :
|
||||
|
||||
- **personal** — disponible dans tous les projets
|
||||
- **project** — disponible uniquement dans le projet actuel et enregistré avec les autres paramètres du projet
|
||||
|
||||
Les noms de serveur utilisent des lettres minuscules, des chiffres, des tirets et des underscores.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Fournisseurs, modèles et agents](/providers/) — connecter d’abord un modèle
|
||||
- [Skills](/skills/) — une autre façon d’étendre ce que les agents peuvent faire
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: PWA et accès mobile
|
||||
description: Installez OpenChamber comme application et utilisez-le depuis votre téléphone.
|
||||
---
|
||||
|
||||
# PWA et accès mobile
|
||||
|
||||
L’application web OpenChamber s’installe comme une application de téléphone (une PWA), pour rester sur votre écran d’accueil et s’utiliser en plein écran. Associez-la à un [tunnel](/tunnels/) et vous pouvez vérifier une session depuis n’importe où.
|
||||
|
||||
## L’installer
|
||||
|
||||
OpenChamber utilise l’installation intégrée de votre navigateur, donc il n’y a pas de téléchargement séparé :
|
||||
|
||||
- **navigateur desktop** — utilisez l’option **Installer** dans la barre d’adresse
|
||||
- **iPhone/iPad (Safari)** — Partager → **Ajouter à l’écran d’accueil**
|
||||
- **Android (Chrome)** — menu → **Installer l’application** / **Ajouter à l’écran d’accueil**
|
||||
|
||||
Une fois installée, elle s’ouvre dans sa propre fenêtre sans chrome de navigateur.
|
||||
|
||||
## Y accéder depuis votre téléphone
|
||||
|
||||
Pour ouvrir OpenChamber sur votre téléphone quand le serveur tourne sur votre ordinateur, démarrez un [tunnel](/tunnels/) et ouvrez le lien (ou scannez le QR code) sur le téléphone. Utilisez toujours un [mot de passe UI](/security/) robuste lorsque vous faites cela.
|
||||
|
||||
## Paramètres mobile
|
||||
|
||||
Sous **Paramètres → OpenChamber**, quelques options ajustent l’expérience mobile et installée — le nom installé de l’application, l’orientation de l’écran et le comportement du clavier à l’écran.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Tunnels](/tunnels/) — joindre votre instance depuis un autre réseau
|
||||
- [Sécurité](/security/) — protéger l’UI avant de l’exposer
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Multi-run
|
||||
description: Lancez le même prompt sur plusieurs modèles ou sessions à la fois.
|
||||
---
|
||||
|
||||
# Multi-run
|
||||
|
||||
Multi-run lance plusieurs sessions depuis un seul formulaire — pratique pour essayer la même tâche avec différents modèles et comparer les résultats. Ouvrez-le depuis le bouton en haut de la barre latérale des sessions.
|
||||
|
||||
## Démarrer un multi-run
|
||||
|
||||
1. Ouvrez le lanceur multi-run.
|
||||
2. Choisissez le projet et nommez le groupe d’exécutions.
|
||||
3. Écrivez le prompt et choisissez les modèles qui doivent l’exécuter (jusqu’à cinq par groupe).
|
||||
4. Choisissez si vous voulez **isoler les exécutions**.
|
||||
5. Lancez.
|
||||
|
||||
Chaque modèle reçoit sa propre session, et toutes commencent avec votre prompt.
|
||||
|
||||
## Exécutions isolées
|
||||
|
||||
Activez **isolate runs** pour donner à chaque exécution son propre [worktree](/worktrees/) et sa propre branche, afin qu’elles ne touchent jamais les mêmes fichiers. Cela nécessite un dépôt git — l’option est désactivée automatiquement pour les dossiers qui n’en sont pas un. Choisissez la branche de départ des exécutions.
|
||||
|
||||
Sans isolation, chaque exécution est une session simple dans le dossier du projet.
|
||||
|
||||
## Comparer les résultats
|
||||
|
||||
Chaque exécution est une session normale que vous pouvez ouvrir, lire, conserver ou jeter. Si vous avez lancé des exécutions pour comparer des approches, relisez-les côte à côte et continuez avec la meilleure.
|
||||
|
||||
Si une seule exécution ne démarre pas, les autres se lancent quand même — vous verrez simplement moins de sessions que demandé.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Sessions worktree](/worktrees/) — comment l’isolation fonctionne sous le capot
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Notes, todos et plans de projet
|
||||
description: Gardez des notes, une todo list et des plans enregistrés pour chaque projet.
|
||||
---
|
||||
|
||||
# Notes, todos et plans de projet
|
||||
|
||||
Chaque projet a son propre espace de brouillon pour les notes, une todo list et les plans enregistrés. Ils appartiennent au projet, pas à une session particulière, donc ils restent disponibles lorsque vous passez d’une session à l’autre. Retrouvez-les dans l’onglet **Contexte** de la barre latérale droite (un onglet dédié sur mobile).
|
||||
|
||||
## Notes
|
||||
|
||||
Une zone de notes libre pour tout ce que vous voulez retenir sur le projet. Elle s’enregistre toute seule pendant que vous tapez.
|
||||
|
||||
## Todos
|
||||
|
||||
Une checklist simple. Ajoutez des éléments, cochez-les, réordonnez-les par glisser-déposer et supprimez ceux qui sont terminés.
|
||||
|
||||
Chaque todo possède un menu **envoyer** pour la confier à l’agent :
|
||||
|
||||
- l’envoyer à la session actuelle
|
||||
- démarrer une nouvelle session avec elle
|
||||
- démarrer une nouvelle [session worktree](/worktrees/) avec elle (uniquement si le projet est un dépôt git)
|
||||
|
||||
## Plans
|
||||
|
||||
Un endroit pour conserver des plans plus longs sous forme de fichiers enregistrés. Vous pouvez :
|
||||
|
||||
- importer un plan depuis un fichier Markdown ou texte
|
||||
- ouvrir un plan pour le lire dans le panneau latéral
|
||||
- supprimer les plans dont vous n’avez plus besoin
|
||||
|
||||
Vous devriez revenir dans l’onglet Contexte avec votre note enregistrée, votre todo cochée ou votre plan listé — c’est le signe que l’action a réussi.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Sessions worktree](/worktrees/) — exécuter une todo dans sa propre branche
|
||||
- [Projets](/projects/) — ces éléments appartiennent au projet actif
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Notifications
|
||||
description: Soyez prévenu quand une session a besoin de vous ou se termine.
|
||||
---
|
||||
|
||||
# Notifications
|
||||
|
||||
Les notifications vous signalent quand quelque chose demande votre attention, sans devoir surveiller l’écran — une session s’est terminée, a rencontré une erreur, a posé une question ou a besoin d’une permission. Configurez-les dans **Paramètres → OpenChamber → Notifications**.
|
||||
|
||||
## Les activer
|
||||
|
||||
1. Ouvrez **Paramètres → OpenChamber → Notifications**.
|
||||
2. Autorisez les notifications lorsque votre navigateur ou système le demande.
|
||||
3. Choisissez ce dont vous voulez être informé :
|
||||
- une session **se termine**
|
||||
- une session rencontre une **erreur**
|
||||
- une session **pose une question**
|
||||
- une session a besoin d’une **permission**
|
||||
- des **sous-tâches** se terminent
|
||||
|
||||
## Comment elles vous atteignent
|
||||
|
||||
- sur **desktop**, vous recevez des notifications système natives
|
||||
- dans un **navigateur ou une application installée**, vous recevez des notifications web push, qui arrivent même lorsque l’onglet est en arrière-plan
|
||||
|
||||
Les sessions configurées en auto-accept ne vous dérangent pas avec des notifications de permission.
|
||||
|
||||
## Personnaliser le texte
|
||||
|
||||
Chaque type de notification a un modèle de titre et de message que vous pouvez modifier, avec des champs comme le nom de l’agent et le modèle. Il existe aussi une limite sur la quantité du dernier message incluse, pour que les notifications restent courtes.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Mode vocal](/voice/) — entendre les réponses lues à voix haute à la place
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Serveur OpenCode
|
||||
description: Connectez OpenChamber à un serveur OpenCode local ou distant.
|
||||
---
|
||||
|
||||
# Serveur OpenCode
|
||||
|
||||
OpenChamber fonctionne par-dessus un serveur OpenCode. Par défaut, il en démarre un pour vous, donc vous n’avez rien à faire. Vous n’avez besoin de cette page que si vous voulez pointer OpenChamber vers un serveur que vous exécutez déjà, ou gérer celui qu’il démarre.
|
||||
|
||||
## Comment OpenChamber trouve un serveur
|
||||
|
||||
Au démarrage, OpenChamber cherche un serveur dans cet ordre :
|
||||
|
||||
1. réutiliser un serveur qu’il a déjà démarré
|
||||
2. se connecter à un serveur externe si vous le lui avez indiqué (voir ci-dessous)
|
||||
3. détecter automatiquement un serveur sur le port par défaut (`4096`)
|
||||
4. sinon, démarrer et gérer son propre serveur
|
||||
|
||||
Si rien n’est configuré, l’étape 4 se fait automatiquement et vous pouvez commencer.
|
||||
|
||||
## Se connecter à un serveur que vous exécutez déjà
|
||||
|
||||
Définissez ces variables avant de lancer OpenChamber :
|
||||
|
||||
```bash
|
||||
OPENCODE_HOST=http://localhost:4096 OPENCODE_SKIP_START=true openchamber
|
||||
```
|
||||
|
||||
- `OPENCODE_HOST` — l’adresse complète de votre serveur OpenCode, port inclus (par exemple `http://localhost:4096`). Elle ne doit pas se terminer par un chemin.
|
||||
- `OPENCODE_SKIP_START=true` — indique à OpenChamber de ne pas démarrer son propre serveur.
|
||||
|
||||
Si vous voulez seulement changer le port, définissez `OPENCODE_PORT` au lieu de `OPENCODE_HOST`.
|
||||
|
||||
Si `OPENCODE_HOST` n’a pas de port ou contient un chemin, OpenChamber l’ignore et revient au démarrage de son propre serveur. Surveillez les logs de démarrage pour un avertissement `[config]` si une connexion attendue ne se fait pas.
|
||||
|
||||
## Gérer le serveur depuis la CLI
|
||||
|
||||
```bash
|
||||
openchamber status
|
||||
openchamber logs
|
||||
openchamber restart
|
||||
openchamber stop
|
||||
```
|
||||
|
||||
`openchamber` seul démarre le serveur en arrière-plan. Ajoutez `--foreground` pour le garder attaché à votre terminal.
|
||||
|
||||
## Lancer OpenChamber à la connexion
|
||||
|
||||
Utilisez `startup enable` pour installer un service utilisateur natif. OpenChamber utilise `launchd` sur macOS, `systemd --user` sur Linux et le Planificateur de tâches sur Windows.
|
||||
|
||||
```bash
|
||||
openchamber startup enable
|
||||
openchamber startup status
|
||||
openchamber startup disable
|
||||
```
|
||||
|
||||
Pour protéger l’UI, définissez le mot de passe lors de l’activation du service :
|
||||
|
||||
```bash
|
||||
OPENCHAMBER_UI_PASSWORD='secret' openchamber startup enable
|
||||
```
|
||||
|
||||
Pour un serveur headless qui démarre à la connexion et doit servir des clients desktop ou mobile, ajoutez `--api-only` et un host joignable :
|
||||
|
||||
```bash
|
||||
openchamber startup enable --port 3000 --api-only --host 0.0.0.0 --ui-password secret
|
||||
```
|
||||
|
||||
`startup enable` capture votre environnement actuel dans le service, pour qu’il se comporte davantage comme un lancement de `openchamber` depuis le même shell. Les tokens de fournisseurs, `PATH`, les réglages d’agent SSH et les autres variables d’auth/config CLI restent ainsi disponibles. Utilisez `--no-env-snapshot` si vous voulez un environnement de service minimal.
|
||||
|
||||
Le service de démarrage mémorise `--port`, `--host`, `--ui-password` et `--api-only`. Les redémarrages CLI et les redémarrages après mise à jour réutilisent ces réglages enregistrés.
|
||||
|
||||
Pour créer un lien de connexion pour une autre application OpenChamber, utilisez :
|
||||
|
||||
```bash
|
||||
openchamber connect-url --port 3000 --server http://your-host:3000 --qr
|
||||
```
|
||||
|
||||
Lancez `openchamber connect-url --help` pour voir toutes les options de lien, dont `--name`, `--lan`, `--server`, `--api-only`, `--ui-password` et `--qr`.
|
||||
|
||||
Vous pouvez toujours gérer les tunnels séparément pour ce service en cours d’exécution :
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --port 3000
|
||||
openchamber tunnel stop --port 3000
|
||||
```
|
||||
|
||||
Arrêter le tunnel ne redémarre ni le service ni l’application.
|
||||
|
||||
## « OpenCode redémarre »
|
||||
|
||||
Pendant que le serveur démarre ou redémarre, OpenChamber affiche l’état « OpenCode redémarre » et met les requêtes en pause jusqu’à ce qu’il soit prêt. C’est normal juste après un lancement ou un redémarrage. Si l’état ne disparaît jamais, consultez [Connexion à OpenCode](/troubleshooting/opencode-connection/).
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Fournisseurs, modèles et agents](/providers/) — configurer ce avec quoi le serveur communique
|
||||
- [Connexion à OpenCode](/troubleshooting/opencode-connection/) — si la connexion ne se fait pas
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Aperçu et serveurs de dev
|
||||
description: Ouvrez un serveur de dev en cours d’exécution dans OpenChamber.
|
||||
---
|
||||
|
||||
# Aperçu et serveurs de dev
|
||||
|
||||
Quand vous démarrez un serveur de dev, OpenChamber peut l’ouvrir directement dans l’application au lieu d’un onglet de navigateur séparé — vous voyez ainsi votre site à côté du chat, vous capturez sa console et vous pouvez pointer des éléments pour poser des questions à leur sujet.
|
||||
|
||||
## Ouvrir un aperçu
|
||||
|
||||
OpenChamber surveille la sortie du terminal pour détecter une adresse locale (la ligne `Local:` affichée par des outils comme Vite, Next.js ou Astro). Quand il en trouve une :
|
||||
|
||||
- dans le terminal, un bouton **Ouvrir l’aperçu** apparaît
|
||||
- une [action de projet](/project-actions/) avec l’ouverture automatique activée l’ouvre pour vous
|
||||
- un lien local dans un message de chat peut aussi l’ouvrir
|
||||
|
||||
Le site se charge dans le panneau latéral. Seules les adresses locales (sur votre propre machine) peuvent être prévisualisées.
|
||||
|
||||
## Console et inspection
|
||||
|
||||
Dans le panneau d’aperçu, vous pouvez :
|
||||
|
||||
- regarder la **console** de la page — erreurs, avertissements et logs, filtrés comme vous le voulez
|
||||
- activer **inspect**, cliquer sur n’importe quel élément et envoyer une note à son sujet — sélecteur, styles, position et capture d’écran — directement dans le chat
|
||||
|
||||
C’est le moyen le plus rapide de dire à l’agent « ce bouton, ici » sans devoir le décrire.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Actions de projet](/project-actions/) — ouvrir automatiquement un serveur quand vous le démarrez
|
||||
- [Navigateur desktop](/desktop-browser/) — les mêmes outils pour n’importe quelle page, sur desktop
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Actions de projet
|
||||
description: Enregistrez les commandes que vous lancez souvent et démarrez-les en un clic.
|
||||
---
|
||||
|
||||
# Actions de projet
|
||||
|
||||
Une action de projet est une commande shell que vous enregistrez une fois et lancez d’un clic — votre serveur de dev, un build, une passe de tests. Chaque projet garde sa propre liste. Configurez-les dans **Paramètres → Projets → Actions de projet**.
|
||||
|
||||
## Ajouter une action
|
||||
|
||||
1. Ouvrez **Paramètres → Projets** et trouvez la section **Actions de projet**.
|
||||
2. Ajoutez une action, donnez-lui un nom, choisissez une icône et saisissez la commande à lancer.
|
||||
3. Enregistrez.
|
||||
|
||||
Vous pouvez limiter une action à certains systèmes d’exploitation si une commande n’a de sens que sur l’un d’eux.
|
||||
|
||||
## Lancer une action
|
||||
|
||||
Les actions vivent dans un menu dans l’en-tête de l’application. Cliquez sur l’une d’elles et OpenChamber la lance dans un terminal dans le dossier de votre projet, puis bascule vers la vue terminal pour que vous puissiez suivre la sortie. Arrêtez-la depuis le même menu.
|
||||
|
||||
## Ouvrir automatiquement un serveur de dev
|
||||
|
||||
Activez **auto-open URL** pour une action qui démarre un serveur. OpenChamber surveille la sortie pour détecter une adresse locale et propose de l’ouvrir — voir [Aperçu et serveurs de dev](/preview/). Sur desktop, vous pouvez aussi la faire passer par un transfert de port SSH.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Aperçu et serveurs de dev](/preview/) — ouvrir un serveur de dev en cours d’exécution dans OpenChamber
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Icônes de projet
|
||||
description: Donnez à chaque projet une icône reconnaissable.
|
||||
---
|
||||
|
||||
# Icônes de projet
|
||||
|
||||
Une icône de projet permet de distinguer vos projets d’un coup d’œil. OpenChamber essaie d’en trouver une pour vous, et vous pouvez toujours définir la vôtre. Gérez-la dans **Paramètres → Projets**.
|
||||
|
||||
## Découverte automatique
|
||||
|
||||
Quand vous ajoutez un projet, OpenChamber cherche un fichier `favicon` dedans et l’utilise comme icône du projet. Si votre dépôt fournit déjà un favicon, l’icône apparaît généralement toute seule — rien à faire.
|
||||
|
||||
## Définir la vôtre
|
||||
|
||||
Ouvrez **Paramètres → Projets** et uploadez une image (PNG, JPEG ou SVG, jusqu’à 5 Mo). Une image personnalisée a priorité sur celle découverte automatiquement. Vous pouvez aussi choisir une couleur à la place, ou retirer l’image pour revenir à la découverte.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Projets](/projects/) — nommer, colorer et organiser vos projets
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Projets
|
||||
description: Organisez votre travail en projets et passez de l’un à l’autre.
|
||||
---
|
||||
|
||||
# Projets
|
||||
|
||||
Un projet est un dossier de votre ordinateur qu’OpenChamber suit — généralement un codebase. Changer de projet change le dossier dans lequel l’agent travaille, ainsi que les sessions et paramètres de ce projet.
|
||||
|
||||
## Ajouter un projet
|
||||
|
||||
Vous pouvez ajouter un projet depuis plusieurs endroits :
|
||||
|
||||
- l’entrée **Ajouter un projet** dans la palette de commandes
|
||||
- le bouton **+** en haut de la barre latérale des sessions
|
||||
- le navigateur de dossiers lorsque vous choisissez un répertoire
|
||||
|
||||
Pointez vers un dossier et OpenChamber s’en souvient. Le nom vient du dossier ; vous pouvez le modifier plus tard.
|
||||
|
||||
## Changer de projet
|
||||
|
||||
Choisissez un projet dans la barre latérale pour le rendre actif. Tout — sessions, git, notes — suit le projet ouvert.
|
||||
|
||||
## Rendre un projet reconnaissable
|
||||
|
||||
Ouvrez **Paramètres → Projets** pour donner à un projet un nom, une couleur ou une icône personnalisés. OpenChamber essaie de trouver une icône automatiquement — voir [Icônes de projet](/project-icons/).
|
||||
|
||||
> Dans VS Code, OpenChamber utilise toujours le dossier ouvert comme seul projet ; il n’y a donc rien à ajouter ni à changer. La page de paramètres Projets y est masquée.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Notes, todos et plans de projet](/notes-todos-plans/) — garder des notes de travail par projet
|
||||
- [Actions de projet](/project-actions/) — enregistrer les commandes que vous lancez souvent
|
||||
- [Contexte](/context/) — voir quelle part de la mémoire du modèle une session utilise
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Fournisseurs, modèles et agents
|
||||
description: Connectez des fournisseurs IA, choisissez des modèles et configurez des agents.
|
||||
---
|
||||
|
||||
# Fournisseurs, modèles et agents
|
||||
|
||||
Avant qu’OpenChamber puisse faire quoi que ce soit, au moins un fournisseur IA doit être connecté. Cette page explique comment connecter un fournisseur, choisir un modèle et adapter les agents.
|
||||
|
||||
## Connecter un fournisseur
|
||||
|
||||
1. Ouvrez **Paramètres → Fournisseurs**.
|
||||
2. Ouvrez le menu **Add provider** et choisissez un fournisseur qui n’est pas encore connecté.
|
||||
3. Connectez-vous de l’une des deux façons, selon le fournisseur :
|
||||
- **API key** — collez votre clé et enregistrez.
|
||||
- **Sign-in (device flow)** — OpenChamber affiche un lien et un code court. Ouvrez le lien, saisissez le code et approuvez. OpenChamber termine la connexion tout seul.
|
||||
|
||||
Quand un fournisseur apparaît comme connecté, ses modèles deviennent disponibles dans le chat.
|
||||
|
||||
Pour vous déconnecter, ouvrez le fournisseur et choisissez de supprimer sa connexion.
|
||||
|
||||
## Choisir un modèle
|
||||
|
||||
Vous choisissez le modèle là où vous travaillez :
|
||||
|
||||
- dans le chat, utilisez le sélecteur de modèle dans la barre de message pour définir le fournisseur et le modèle de cette session
|
||||
- par agent, définissez un modèle par défaut (ci-dessous)
|
||||
|
||||
## Configurer des agents
|
||||
|
||||
Un agent est une configuration nommée — un modèle, une personnalité et ce qu’il est autorisé à faire.
|
||||
|
||||
1. Ouvrez **Paramètres → Agents**.
|
||||
2. Choisissez un agent ou créez-en un nouveau.
|
||||
3. Modifiez au besoin :
|
||||
- **description** — à quoi sert l’agent
|
||||
- **model** — son modèle par défaut
|
||||
- **temperature** — le niveau de créativité de ses réponses
|
||||
- **prompt** — les instructions permanentes qu’il suit toujours
|
||||
- **tool rules** — les outils qu’il peut utiliser
|
||||
|
||||
## Où vivent vos connexions
|
||||
|
||||
Les connexions aux fournisseurs sont stockées par OpenCode, pas OpenChamber ; elles sont donc partagées avec la CLI OpenCode. Si vous définissez le même fournisseur à plusieurs endroits, le réglage le plus spécifique gagne : un réglage par projet remplace votre réglage personnel.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Serveurs MCP](/mcp/) — ajouter des outils supplémentaires aux agents
|
||||
- [Utilisation et quotas](/usage/) — suivre votre consommation
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Démarrage rapide
|
||||
description: Lancez OpenChamber rapidement et choisissez la bonne application pour la tâche.
|
||||
---
|
||||
|
||||
# Démarrage rapide
|
||||
|
||||
## Chemin le plus rapide
|
||||
|
||||
1. Installez [OpenCode](https://opencode.ai).
|
||||
2. Installez la CLI OpenChamber (voir [Installation](/install/) pour la commande en une ligne).
|
||||
3. Lancez `openchamber --ui-password be-creative-here`.
|
||||
4. Ouvrez l’URL affichée par la CLI (souvent `http://localhost:3000`).
|
||||
5. Pour l’utiliser depuis votre téléphone, démarrez un [tunnel](/tunnels/) et scannez le QR code.
|
||||
|
||||
Vous devriez voir la liste des sessions OpenChamber dans votre navigateur. Si elle se charge, tout est prêt.
|
||||
|
||||
Utilisez un mot de passe UI robuste, surtout si vous prévoyez d’ouvrir l’instance à internet.
|
||||
|
||||
Si la page ne se charge pas, consultez le [dépannage](/troubleshooting/).
|
||||
|
||||
## Quelle application utiliser ?
|
||||
|
||||
- utilisez **desktop** pour le travail quotidien sur macOS
|
||||
- utilisez **web** pour l’accès distant et la revue depuis votre téléphone
|
||||
- utilisez **VS Code** pour garder les sessions juste à côté de votre code
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Instances distantes
|
||||
description: Connectez l’application desktop à OpenChamber sur une autre machine via SSH.
|
||||
---
|
||||
|
||||
# Instances distantes
|
||||
|
||||
L’application desktop peut se connecter à OpenChamber exécuté sur une autre machine via SSH — serveur de travail, machine cloud, homelab — et afficher son UI sur votre écran comme si elle était locale. Configurez cela dans **Paramètres → Instances distantes**.
|
||||
|
||||
> Les instances distantes sont une fonctionnalité **desktop uniquement**. Sur le web ou dans VS Code, connectez-vous à un serveur distant avec les variables d’environnement de [Serveur OpenCode](/opencode-server/) à la place.
|
||||
|
||||
## Ajouter une instance distante
|
||||
|
||||
1. Ouvrez **Paramètres → Instances distantes** et ajoutez-en une.
|
||||
2. Donnez la commande SSH que vous utilisez normalement pour joindre la machine, plus un surnom.
|
||||
3. Choisissez comment OpenChamber s’y exécute :
|
||||
- **managed** — OpenChamber s’installe et se démarre sur la machine distante
|
||||
- **external** — se connecter à une instance déjà en cours d’exécution
|
||||
4. Connectez-vous.
|
||||
|
||||
OpenChamber déroule les étapes — vérification de la connexion, configuration du remote, démarrage du serveur et transfert du port — et montre où il en est à chaque étape. Quand il atteint **ready**, l’UI distante se charge localement.
|
||||
|
||||
## Identifiants
|
||||
|
||||
Vous décidez d’enregistrer les mots de passe SSH et UI ou de les saisir à chaque fois. Si la connexion tombe, OpenChamber indique l’étape qui a échoué pour vous aider à corriger — voir [Accès distant](/troubleshooting/remote-access/).
|
||||
|
||||
## Liens de connexion directe
|
||||
|
||||
Si une machine distante exécute déjà OpenChamber, créez un lien de connexion là-bas et importez-le dans **Paramètres → Instances distantes → Liens de serveur** :
|
||||
|
||||
```bash
|
||||
openchamber connect-url --port 3000 --server http://your-host:3000 --qr
|
||||
```
|
||||
|
||||
`connect-url` démarre d’abord le serveur si rien ne tourne sur ce port. Ajoutez `--api-only` pour un serveur headless, `--lan` pour écouter sur le LAN au démarrage, `--ui-password` pour protéger l’accès navigateur et `--name` pour nommer la connexion enregistrée.
|
||||
|
||||
Le lien généré contient un token client pour les applications OpenChamber. Ce token est séparé du mot de passe de l’UI navigateur et survit aux redémarrages du serveur jusqu’à révocation ou suppression.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Serveur OpenCode](/opencode-server/) — se connecter à un serveur distant sur web ou VS Code
|
||||
- [Accès distant](/troubleshooting/remote-access/) — quand une connexion ne se termine pas
|
||||
@@ -0,0 +1,347 @@
|
||||
---
|
||||
title: Reverse proxy
|
||||
description: Configurez correctement OpenChamber derrière Nginx, Nginx Proxy Manager ou un autre reverse proxy.
|
||||
---
|
||||
|
||||
# Reverse proxy
|
||||
|
||||
Utilisez cette page si vous exécutez OpenChamber derrière Nginx, Nginx Proxy Manager, Caddy, Cloudflare ou un autre reverse proxy.
|
||||
|
||||
## Avant de le proxifier
|
||||
|
||||
1. Confirmez d’abord qu’OpenChamber fonctionne en accès direct.
|
||||
2. Ouvrez `http://<server-ip>:3000` ou votre port personnalisé depuis le même réseau.
|
||||
3. N’ajoutez le reverse proxy qu’une fois la connexion directe fonctionnelle.
|
||||
|
||||
## Ce que le proxy doit prendre en charge
|
||||
|
||||
- WebSockets pour le transport des messages en direct :
|
||||
- `/api/event/ws`
|
||||
- `/api/global/event/ws`
|
||||
- `/api/terminal/ws`
|
||||
- SSE sans buffering :
|
||||
- `/api/event`
|
||||
- `/api/global/event`
|
||||
- `/api/notifications/stream`
|
||||
- `/api/openchamber/events`
|
||||
- `/api/terminal/:sessionId/stream`
|
||||
- corps de requête volumineux pour les pièces jointes et les opérations de fichiers
|
||||
- timeouts de lecture longs pour les flux en direct et les sessions de terminal
|
||||
|
||||
## Règles importantes
|
||||
|
||||
- Activez le proxy WebSocket.
|
||||
- Désactivez le buffering sur les routes SSE.
|
||||
- Désactivez gzip sur le proxy si OpenChamber compresse déjà les réponses.
|
||||
- Gardez la compression active dans une seule couche.
|
||||
- Transmettez les headers proxy normaux comme `Host`, `X-Forwarded-For` et `X-Forwarded-Proto`.
|
||||
- Augmentez les limites de taille de corps si les utilisateurs uploadent des fichiers.
|
||||
|
||||
## Checklist rapide
|
||||
|
||||
- OpenChamber joignable directement sur le LAN
|
||||
- WebSockets activés dans le proxy
|
||||
- buffering désactivé sur les routes SSE
|
||||
- `gzip off` sur le proxy host, ou compression proxy désactivée autrement
|
||||
- `client_max_body_size` assez grand pour les pièces jointes
|
||||
- `proxy_read_timeout` assez long pour les streams
|
||||
|
||||
## Exemple : Nginx
|
||||
|
||||
<details>
|
||||
<summary>Afficher l’exemple de configuration</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>
|
||||
|
||||
## Exemple : Nginx Proxy Manager
|
||||
|
||||
<details>
|
||||
<summary>Afficher l’exemple pour l’onglet Advanced</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>
|
||||
|
||||
Activez aussi `Websockets Support` dans Nginx Proxy Manager pour ce host.
|
||||
|
||||
## Signes courants de panne
|
||||
|
||||
### La page se charge, mais l’envoi de messages échoue
|
||||
|
||||
- les WebSockets ne sont pas activés dans le proxy
|
||||
- `/api/event/ws` ou `/api/global/event/ws` ne passe pas correctement
|
||||
|
||||
### Les notifications ou le statut en direct ne se mettent pas à jour
|
||||
|
||||
- l’une des routes SSE est bufferisée ou mise en cache
|
||||
- `X-Accel-Buffering "no"` manque
|
||||
|
||||
### Les uploads de fichiers échouent
|
||||
|
||||
- `client_max_body_size` est trop petit
|
||||
|
||||
### Tout fonctionne en local, mais casse seulement derrière le proxy
|
||||
|
||||
- le proxy compresse et bufferise le trafic en direct
|
||||
- le proxy n’a pas le support WebSocket
|
||||
|
||||
## Exemple : Caddy
|
||||
|
||||
<details>
|
||||
<summary>Afficher l’exemple de configuration</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 gère automatiquement les upgrades WebSocket — aucune configuration supplémentaire n’est nécessaire. La directive `flush_interval -1` garantit que les morceaux SSE sont transmis immédiatement, sans buffering.
|
||||
|
||||
## Avertissement CDN et double compression
|
||||
|
||||
Si vous placez un CDN (comme Cloudflare) devant votre reverse proxy, faites attention à la double compression :
|
||||
|
||||
- OpenChamber compresse les réponses HTTP avec gzip (seuil de 1 Ko).
|
||||
- Cloudflare et d’autres CDN compressent aussi les réponses par défaut.
|
||||
- Cela peut produire des réponses compressées deux fois ou des headers `Content-Encoding` incorrects.
|
||||
|
||||
Pour éviter cela, désactivez la compression à **une** couche :
|
||||
|
||||
- **Cloudflare :** Rules → Compression → désactiver (ou utiliser le mode « Passthrough »).
|
||||
- **Nginx :** `gzip off` (déjà montré dans les exemples ci-dessus).
|
||||
- **Caddy :** Caddy ne re-compresse pas par défaut si l’upstream envoie déjà du contenu compressé.
|
||||
|
||||
Les routes de streaming SSE sont exclues de la compression par OpenChamber, mais le CDN peut encore les bufferiser. Consultez la documentation de votre CDN pour désactiver le buffering sur les chemins SSE.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Tunnels](/tunnels/)
|
||||
- [Dépannage](/troubleshooting/)
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Tâches planifiées
|
||||
description: Lancez automatiquement un prompt selon un planning.
|
||||
---
|
||||
|
||||
# Tâches planifiées
|
||||
|
||||
Une tâche planifiée lance un prompt pour vous selon un planning — par exemple un « résume les changements d’hier » quotidien ou un nettoyage hebdomadaire. Quand elle s’exécute, OpenChamber démarre une nouvelle session et envoie le prompt tout seul. Ouvrez le planificateur depuis le bouton en haut de la barre latérale des sessions.
|
||||
|
||||
## Créer une tâche
|
||||
|
||||
1. Ouvrez la boîte de dialogue des tâches planifiées depuis la barre latérale des sessions.
|
||||
2. Ajoutez une tâche et donnez-lui un nom.
|
||||
3. Choisissez quand elle s’exécute :
|
||||
- **daily** — à une ou plusieurs heures chaque jour
|
||||
- **weekly** — certains jours de la semaine et à certaines heures
|
||||
- **once** — une seule date et heure
|
||||
4. Définissez ce qu’elle fait : le prompt à envoyer, ainsi que le fournisseur, le modèle et l’agent à utiliser. Le prompt peut être une commande slash, comme `/review`.
|
||||
5. Enregistrez, puis vérifiez que la tâche est activée.
|
||||
|
||||
Vous pouvez lancer n’importe quelle tâche immédiatement avec **run now** pour vérifier qu’elle fait ce que vous attendez.
|
||||
|
||||
## À quoi ressemble une réussite
|
||||
|
||||
Après une exécution, la tâche indique quand elle a tourné pour la dernière fois, si elle a réussi et un lien vers la session créée. Si une exécution échoue, l’erreur s’affiche aussi à cet endroit.
|
||||
|
||||
## À garder en tête
|
||||
|
||||
Les tâches ne se déclenchent que lorsque le serveur OpenChamber tourne. Si vous le fermez, les exécutions planifiées sont en pause jusqu’à son retour.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Commandes et snippets](/commands-snippets/) — réutiliser une commande slash comme prompt
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Sécurité
|
||||
description: Protégez l’UI avec un mot de passe et des passkeys avant de l’exposer.
|
||||
---
|
||||
|
||||
# Sécurité
|
||||
|
||||
OpenChamber donne accès à votre machine et à votre code ; verrouillez-le donc avant que quelqu’un d’autre que vous puisse l’atteindre. Cette page couvre le mot de passe UI, les passkeys et ce qu’il faut savoir avant d’exposer OpenChamber sur un réseau.
|
||||
|
||||
## Définir un mot de passe UI
|
||||
|
||||
Démarrez OpenChamber avec un mot de passe et l’UI navigateur le demandera :
|
||||
|
||||
```bash
|
||||
openchamber --ui-password be-creative-here
|
||||
```
|
||||
|
||||
Vous pouvez aussi le définir avec la variable d’environnement `OPENCHAMBER_UI_PASSWORD` au lieu de le mettre en ligne de commande. Après connexion, OpenChamber mémorise l’appareil pendant un moment pour ne pas vous le redemander à chaque fois.
|
||||
|
||||
Définissez toujours un mot de passe si l’instance est joignable par quelqu’un d’autre — surtout via un [tunnel](/tunnels/) ou internet public.
|
||||
|
||||
## Passkeys
|
||||
|
||||
Une fois un mot de passe défini, vous pouvez ajouter des passkeys (Face ID, Touch ID, clé de sécurité) pour vous connecter plus vite. Ajoutez-les dans **Paramètres → OpenChamber → Passkeys**.
|
||||
|
||||
Les passkeys sont liées au mot de passe actuel. Si vous changez ou supprimez le mot de passe, les passkeys enregistrées sont effacées et vous devrez les ajouter à nouveau.
|
||||
|
||||
## Avant de l’exposer
|
||||
|
||||
- Par défaut, OpenChamber n’écoute que sur votre propre machine (`127.0.0.1`). Écouter plus largement demande un changement volontaire, et vous devriez définir un mot de passe d’abord.
|
||||
- Préférez un [tunnel](/tunnels/) ou un réseau privé (comme un VPN) plutôt que d’ouvrir un port sur internet.
|
||||
- Si vous placez OpenChamber derrière votre propre serveur HTTPS, consultez [Reverse proxy](/reverse-proxy/).
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Tunnels](/tunnels/) — la façon recommandée de joindre une instance à distance
|
||||
- [Reverse proxy](/reverse-proxy/) — exécuter OpenChamber derrière votre propre serveur
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Catalogue de skills
|
||||
description: Parcourez et installez des skills prêts à l’emploi.
|
||||
---
|
||||
|
||||
# Catalogue de skills
|
||||
|
||||
Le catalogue de skills vous permet d’installer des skills publiés par d’autres personnes au lieu d’écrire les vôtres. Ouvrez-le dans **Paramètres → Skills → Catalog**.
|
||||
|
||||
Pour écrire vos propres skills, voir [Skills](/skills/).
|
||||
|
||||
## Installer un skill
|
||||
|
||||
1. Ouvrez le catalogue.
|
||||
2. Parcourez les sources intégrées — le dépôt de skills Anthropic et le registre communautaire ClawdHub — ou lancez une recherche.
|
||||
3. Choisissez un skill et installez-le.
|
||||
4. Choisissez où l’installer : pour tout ce que vous faites, ou seulement pour le projet actuel.
|
||||
|
||||
Si un skill du même nom existe déjà, OpenChamber vous demande quoi faire — l’ignorer, l’écraser ou décider skill par skill.
|
||||
|
||||
## Ajouter votre propre source
|
||||
|
||||
Vous pouvez ajouter n’importe quel dépôt Git comme source avec son nom `owner/repo` ou une URL Git complète. Les dépôts privés nécessitent un accès configuré sur votre machine (clé SSH ou identifiants enregistrés) ; si une source ne peut pas s’authentifier, le catalogue l’indique au lieu d’échouer silencieusement.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Skills](/skills/) — créer et gérer les skills installés
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Skills
|
||||
description: Créez des instructions réutilisables que les agents chargent à la demande.
|
||||
---
|
||||
|
||||
# Skills
|
||||
|
||||
Un skill est un ensemble réutilisable d’instructions qu’un agent peut charger quand c’est pertinent — par exemple « comment nous écrivons les messages de commit » ou « nos conventions d’API ». Gérez-les dans **Paramètres → Skills**.
|
||||
|
||||
Pour installer des skills prêts à l’emploi au lieu d’écrire les vôtres, voir [Catalogue de skills](/skills-catalog/).
|
||||
|
||||
## Créer un skill
|
||||
|
||||
1. Ouvrez **Paramètres → Skills**.
|
||||
2. Créez un skill et donnez-lui un nom et une courte description. La description est ce qui permet à l’agent de décider quand le skill s’applique, donc soyez précis.
|
||||
3. Écrivez les instructions. Ajoutez des fichiers de support si le skill en a besoin.
|
||||
4. Choisissez où il vit :
|
||||
- **personal** — disponible dans tous les projets
|
||||
- **project** — disponible uniquement dans le projet actuel
|
||||
|
||||
## Utiliser un skill dans le chat
|
||||
|
||||
Tapez `/` au milieu d’un message pour afficher le sélecteur de skills, puis choisissez-en un. L’agent charge les instructions de ce skill pour la réponse.
|
||||
|
||||
Un `/` au tout début du message ouvre plutôt les [commandes](/commands-snippets/).
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Catalogue de skills](/skills-catalog/) — installer des skills publiés par d’autres
|
||||
- [Commandes et snippets](/commands-snippets/) — autres façons de réutiliser du texte dans le chat
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Hosts SSH et proxy
|
||||
description: Importez des hosts SSH enregistrés et ajoutez des transferts de ports SSH dans l’application desktop.
|
||||
---
|
||||
|
||||
# Hosts SSH et proxy
|
||||
|
||||
Utilisez **Paramètres → Instances distantes** dans l’application desktop pour importer des hosts SSH enregistrés, vous connecter à une machine distante et rendre des ports supplémentaires disponibles via la même connexion SSH.
|
||||
|
||||
> Les hosts SSH et le proxy SSH sont des fonctionnalités **desktop uniquement**. Elles utilisent le client SSH de votre ordinateur.
|
||||
|
||||
## Importer un host SSH
|
||||
|
||||
OpenChamber peut lire les hosts depuis votre config SSH locale, le même endroit que les commandes comme `ssh work-server` utilisent.
|
||||
|
||||
1. Ouvrez **Paramètres → Instances distantes**.
|
||||
2. Choisissez un host dans **Saved SSH hosts**.
|
||||
3. Si le host est un pattern, saisissez la vraie destination, par exemple `deploy@app.example.com`.
|
||||
4. Enregistrez et connectez-vous.
|
||||
|
||||
OpenChamber crée une instance distante avec cette commande SSH. Quand elle atteint **ready**, l’UI OpenChamber distante s’ouvre dans l’application desktop.
|
||||
|
||||
## Ajouter des transferts de ports
|
||||
|
||||
Chaque instance distante possède une section **Port Forwards**. Utilisez-la quand quelque chose d’un côté de la connexion SSH doit joindre un port de l’autre côté.
|
||||
|
||||
OpenChamber prend en charge trois types de transfert :
|
||||
|
||||
- **Local (-L)** — ouvre un port sur votre ordinateur qui se connecte à quelque chose sur la machine distante.
|
||||
- **Remote (-R)** — ouvre un port sur la machine distante qui se reconnecte à votre ordinateur.
|
||||
- **Dynamic (-D)** — ouvre un proxy SOCKS local via la connexion SSH.
|
||||
|
||||
Pour la plupart des aperçus d’app et dashboards qui tournent sur la machine distante, utilisez **Local (-L)**.
|
||||
|
||||
## Utiliser le proxy SOCKS
|
||||
|
||||
Choisissez **Dynamic (-D)** quand vous voulez que d’autres outils sur votre ordinateur naviguent via la machine distante. OpenChamber ouvre un port proxy SOCKS local pour cette connexion SSH.
|
||||
|
||||
Une fois la connexion prête, copiez ou utilisez l’adresse proxy locale depuis la ligne du transfert. Configurez votre navigateur ou outil pour l’utiliser comme proxy SOCKS5.
|
||||
|
||||
## Garder cela privé
|
||||
|
||||
Utilisez `127.0.0.1` ou `localhost` comme hosts de bind locaux, sauf si vous voulez volontairement que d’autres appareils de votre réseau atteignent le port transféré.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Instances distantes](/remote-instances/) — connecter l’application desktop à OpenChamber sur une autre machine
|
||||
- [Accès distant](/troubleshooting/remote-access/) — quand SSH ou l’accès distant ne se connecte pas
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Thèmes
|
||||
description: Personnalisez OpenChamber avec des thèmes intégrés et définis par l’utilisateur.
|
||||
---
|
||||
|
||||
# Thèmes
|
||||
|
||||
OpenChamber prend en charge les thèmes intégrés et les fichiers JSON de thème personnalisés.
|
||||
|
||||
## Ajouter un thème personnalisé
|
||||
|
||||
1. Créez le dossier des thèmes :
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/openchamber/themes
|
||||
```
|
||||
|
||||
2. Ajoutez votre fichier JSON dans ce dossier (par exemple `my-theme.json`).
|
||||
3. Ouvrez OpenChamber, puis allez dans **Paramètres -> Thème -> Reload themes**.
|
||||
4. Choisissez votre thème dans le menu déroulant. Il s’applique immédiatement.
|
||||
|
||||
## Emplacement des thèmes
|
||||
|
||||
- macOS/Linux : `~/.config/openchamber/themes/`
|
||||
|
||||
## Référence complète du format JSON
|
||||
|
||||
Utilisez le guide complet du format dans la documentation du dépôt principal :
|
||||
|
||||
- [`docs/CUSTOM_THEMES.md`](https://github.com/openchamber/openchamber/blob/main/docs/CUSTOM_THEMES.md)
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Dépannage
|
||||
description: Problèmes courants d’installation et d’exécution, avec corrections rapides.
|
||||
---
|
||||
|
||||
# Dépannage
|
||||
|
||||
Vous êtes bloqué ? Trouvez le symptôme ci-dessous et essayez la correction.
|
||||
|
||||
## La commande OpenChamber se ferme ou ne démarre pas
|
||||
|
||||
- confirmez que Node.js est en version `>=22`
|
||||
- lancez `openchamber --version`
|
||||
- réinstallez la dernière CLI si nécessaire
|
||||
|
||||
## L’UI web n’est pas joignable
|
||||
|
||||
- consultez les logs serveur avec `openchamber logs`
|
||||
- vérifiez le port actif (par défaut `3000`)
|
||||
- ouvrez d’abord `http://localhost:3000` directement avant de tester les liens de tunnel
|
||||
|
||||
## Le lien distant/tunnel ne fonctionne pas
|
||||
|
||||
- lancez `openchamber tunnel status --all`
|
||||
- redémarrez le tunnel depuis la même instance/le même port
|
||||
- régénérez le lien de connexion si le token précédent a déjà été utilisé
|
||||
|
||||
Voir [Tunnels](/tunnels/) pour la configuration complète, et [Reverse proxy](/reverse-proxy/) si vous en utilisez un.
|
||||
|
||||
## L’extension VS Code ne se connecte pas
|
||||
|
||||
- confirmez que le serveur OpenChamber tourne
|
||||
- vérifiez que l’extension est à jour
|
||||
- rechargez la fenêtre VS Code et réessayez la connexion
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Démarrage rapide](/quickstart/)
|
||||
- [Tunnels](/tunnels/)
|
||||
- [Reverse proxy](/reverse-proxy/)
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Connexion à OpenCode
|
||||
description: Corrigez OpenChamber lorsqu’il ne se connecte pas à son serveur OpenCode.
|
||||
---
|
||||
|
||||
# Connexion à OpenCode
|
||||
|
||||
Si OpenChamber se charge mais reste bloqué sur « OpenCode redémarre », ou si le chat ne répond pas, le serveur auquel il parle n’est pas joignable. Suivez ces vérifications.
|
||||
|
||||
## Il reste bloqué sur « OpenCode redémarre »
|
||||
|
||||
- laissez-lui un moment juste après le lancement — cet état est normal pendant le démarrage du serveur
|
||||
- vérifiez si le serveur est vivant avec `openchamber status`
|
||||
- redémarrez-le avec `openchamber restart`
|
||||
- consultez les détails de démarrage avec `openchamber logs`
|
||||
|
||||
## Vous vous connectez à votre propre serveur
|
||||
|
||||
Si vous avez configuré OpenChamber pour utiliser un serveur existant, revérifiez la configuration depuis [Serveur OpenCode](/opencode-server/) :
|
||||
|
||||
- `OPENCODE_HOST` doit inclure le port, sans chemin — par exemple `http://localhost:4096`
|
||||
- définissez `OPENCODE_SKIP_START=true` pour qu’OpenChamber ne démarre pas aussi le sien
|
||||
- si l’adresse est invalide, OpenChamber l’ignore et démarre son propre serveur à la place — cherchez un avertissement `[config]` dans les logs
|
||||
|
||||
## Toujours en échec
|
||||
|
||||
- confirmez que Node.js est en version `20` ou plus récente
|
||||
- réinstallez la dernière CLI
|
||||
- ouvrez `http://localhost:3000` directement avant de tester un tunnel ou lien distant
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Serveur OpenCode](/opencode-server/) — comment OpenChamber trouve et gère le serveur
|
||||
- [Dépannage](/troubleshooting/) — autres problèmes courants
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Accès distant
|
||||
description: Corrigez les tunnels, instances distantes et accès à OpenChamber depuis un autre appareil.
|
||||
---
|
||||
|
||||
# Accès distant
|
||||
|
||||
Quand vous ne pouvez pas joindre OpenChamber depuis votre téléphone ou une autre machine, la correction dépend de votre méthode de connexion.
|
||||
|
||||
## Vérifiez d’abord les bases
|
||||
|
||||
- ouvrez d’abord `http://localhost:3000` sur le même ordinateur — si cela échoue, ce n’est pas un problème distant ; voir [Connexion à OpenCode](/troubleshooting/opencode-connection/)
|
||||
- confirmez que le serveur tourne avec `openchamber status`
|
||||
|
||||
## Le lien de tunnel ne fonctionne pas
|
||||
|
||||
- lancez `openchamber tunnel status --all`
|
||||
- redémarrez le tunnel depuis la même instance et le même port
|
||||
- régénérez le lien de connexion si le précédent a déjà été utilisé
|
||||
|
||||
Voir [Tunnels](/tunnels/) pour la configuration complète.
|
||||
|
||||
## L’instance distante ne se connecte pas (desktop)
|
||||
|
||||
Quand une [instance distante](/remote-instances/) bloque, OpenChamber nomme l’étape qui a échoué :
|
||||
|
||||
- **auth** — votre mot de passe SSH ou UI a été refusé ; saisissez-le à nouveau
|
||||
- **install / start** — OpenChamber n’a pas pu configurer ou démarrer le serveur sur la machine distante ; vérifiez les prérequis de cette machine
|
||||
- **forwarding** — la connexion est établie mais le port ne vous rejoint pas ; essayez un autre port local
|
||||
|
||||
## Derrière votre propre serveur
|
||||
|
||||
Si vous placez OpenChamber derrière un reverse proxy et qu’il se charge bizarrement ou ne se connecte pas, consultez [Reverse proxy](/reverse-proxy/).
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Tunnels](/tunnels/) · [Instances distantes](/remote-instances/) · [Reverse proxy](/reverse-proxy/)
|
||||
- [Sécurité](/security/) — protéger l’UI avant de l’exposer
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Worktrees et Git
|
||||
description: Corrigez les problèmes courants de worktree et de git.
|
||||
---
|
||||
|
||||
# Worktrees et Git
|
||||
|
||||
Problèmes qui apparaissent avec les [sessions worktree](/worktrees/) et la [vue git](/git/), et comment les résoudre.
|
||||
|
||||
## Un worktree demande votre attention
|
||||
|
||||
OpenChamber signale un worktree quand quelque chose ne va pas :
|
||||
|
||||
- **folder missing** — le dossier du worktree a été supprimé ou déplacé hors d’OpenChamber ; supprimez la session et créez un nouveau worktree
|
||||
- **detached or unborn branch** — le worktree n’est pas sur une branche normale ; checkout une branche
|
||||
- **merge, rebase, or cherry-pick in progress** — une opération est restée à moitié terminée ; terminez-la ou annulez-la depuis la vue git
|
||||
|
||||
## Impossible de créer un worktree
|
||||
|
||||
- **branch already exists** — choisissez un autre nom de branche, ou utilisez l’option branche existante
|
||||
- **name already in use** — choisissez un autre nom de worktree
|
||||
|
||||
## La génération de commit ou PR échoue
|
||||
|
||||
Générer un message de commit ou une description de PR s’exécute dans votre session active ; vous devez donc avoir une session ouverte avec un modèle fonctionnel sélectionné. Ouvrez ou choisissez une session et réessayez.
|
||||
|
||||
## Problèmes SSH ou chemins Windows
|
||||
|
||||
- vérifiez que la clé SSH utilisée par le dépôt est celle définie dans votre [identité git](/git-identities/)
|
||||
- sur Windows, git utilise des chemins de style Unix (comme `/c/Users/...`) — OpenChamber gère cela, mais les chemins de clés SSH personnalisés doivent suivre la même forme
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Sessions worktree](/worktrees/) — comment les worktrees sont créés et supprimés
|
||||
- [Identités Git](/git-identities/) — définir la bonne clé et identité par dépôt
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Tunnels
|
||||
description: Exposez OpenChamber en sécurité pour l’accès distant et mobile.
|
||||
---
|
||||
|
||||
# Tunnels
|
||||
|
||||
Un tunnel est un lien public vers votre OpenChamber, pour y accéder depuis votre téléphone ou un autre réseau. Utilisez `openchamber tunnel` pour en créer un pour une instance en cours d’exécution.
|
||||
|
||||
## Prérequis
|
||||
|
||||
OpenChamber démarre la CLI du fournisseur de tunnel sur votre machine. Installez d’abord le fournisseur que vous voulez utiliser :
|
||||
|
||||
```bash
|
||||
brew install cloudflared
|
||||
brew install ngrok
|
||||
```
|
||||
|
||||
Les tunnels rapides Cloudflare peuvent fonctionner avec `cloudflared`. Ngrok nécessite un compte ngrok et un authtoken depuis le dashboard ngrok :
|
||||
|
||||
```bash
|
||||
ngrok config add-authtoken <your-ngrok-token>
|
||||
```
|
||||
|
||||
## Démarrage rapide
|
||||
|
||||
1. Démarrez OpenChamber :
|
||||
|
||||
```bash
|
||||
openchamber
|
||||
```
|
||||
|
||||
Si vous sautez cette étape, `openchamber tunnel start` peut démarrer automatiquement un serveur CLI. Lors de l’auto-démarrage, vous pouvez passer des options serveur comme `--port`, `--host`, `--lan`, `--ui-password` et `--api-only`.
|
||||
|
||||
2. Démarrez un tunnel Cloudflare :
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --provider cloudflare --mode quick
|
||||
```
|
||||
|
||||
Ou démarrez un tunnel Ngrok :
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --provider ngrok --mode quick
|
||||
```
|
||||
|
||||
3. Vérifiez l’état :
|
||||
|
||||
```bash
|
||||
openchamber tunnel status
|
||||
```
|
||||
|
||||
Quand le tunnel est actif, `status` affiche une URL publique. Ouvrez-la, ou scannez le QR code, pour joindre OpenChamber depuis n’importe où.
|
||||
|
||||
Par défaut, OpenChamber affiche un QR code dans les sessions TTY interactives. Utilisez `--qr` pour forcer la sortie QR, ou `--no-qr` pour la désactiver.
|
||||
|
||||
## Fournisseurs
|
||||
|
||||
- `cloudflare` : modes quick, managed remote et managed local
|
||||
- `ngrok` : mode quick
|
||||
|
||||
## Modes gérés
|
||||
|
||||
### Managed remote
|
||||
|
||||
Utilisez un token + hostname gérés par Cloudflare :
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --provider cloudflare --mode managed-remote --token-file ~/.secrets/cf-token --hostname app.example.com
|
||||
```
|
||||
|
||||
### Managed local
|
||||
|
||||
Utilisez une configuration `cloudflared` locale :
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --provider cloudflare --mode managed-local --config ~/.cloudflared/config.yml
|
||||
```
|
||||
|
||||
## Profils (managed-remote)
|
||||
|
||||
Enregistrez un profil réutilisable :
|
||||
|
||||
```bash
|
||||
openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token-file ~/.secrets/cf-token
|
||||
```
|
||||
|
||||
Démarrez avec le profil enregistré :
|
||||
|
||||
```bash
|
||||
openchamber tunnel start --profile prod-main
|
||||
```
|
||||
|
||||
## Commandes utiles
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Notes de comportement
|
||||
|
||||
- un tunnel actif par instance OpenChamber (port)
|
||||
- démarrer un nouveau mode/fournisseur sur la même instance remplace le tunnel précédent
|
||||
- générer un nouveau lien de connexion révoque le précédent lien inutilisé
|
||||
- l’auto-démarrage du tunnel conserve les flags serveur comme `--ui-password` et `--api-only` dans les paramètres d’instance utilisés par les flux de redémarrage/mise à jour
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Sécurité](/security/) — protéger l’UI avant de l’exposer
|
||||
- [Tunnels desktop](/desktop-tunnels/) — configuration des tunnels de l’application desktop sans démarrage CLI
|
||||
- [PWA et mobile](/mobile/) — joindre OpenChamber depuis votre téléphone
|
||||
- [Dépannage](/troubleshooting/) — si le lien de tunnel ne fonctionne pas
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Mises à jour
|
||||
description: Gardez OpenChamber à jour sur desktop, web et VS Code.
|
||||
---
|
||||
|
||||
# Mises à jour
|
||||
|
||||
La façon de mettre OpenChamber à jour dépend de votre méthode d’installation. Dans tous les cas, vous pouvez vérifier votre version actuelle dans **Paramètres → OpenChamber → À propos**.
|
||||
|
||||
## Application desktop
|
||||
|
||||
L’application desktop vérifie les mises à jour via les releases GitHub. Quand une mise à jour est disponible, OpenChamber vous le signale, la télécharge lorsque vous le choisissez et l’installe au prochain redémarrage. Vous gardez toujours le contrôle — rien ne s’installe sans votre accord.
|
||||
|
||||
## Web / CLI
|
||||
|
||||
Si vous avez installé la CLI, mettez-la à jour depuis **À propos** avec les boutons de vérification et de mise à jour, ou depuis le terminal :
|
||||
|
||||
```bash
|
||||
openchamber update
|
||||
```
|
||||
|
||||
OpenChamber détecte comment il a été installé (npm, pnpm, yarn ou bun) et lance la bonne mise à jour pour vous.
|
||||
|
||||
## Le serveur OpenCode
|
||||
|
||||
OpenChamber et OpenCode se mettent à jour séparément. Quand une nouvelle version d’OpenCode est disponible, OpenChamber propose de la mettre à jour puis redémarre le serveur. Voir [Serveur OpenCode](/opencode-server/) pour comprendre comment ce serveur est géré.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Installation](/install/) — comment chaque application est installée au départ
|
||||
- [Serveur OpenCode](/opencode-server/) — mettre à jour le serveur sous-jacent
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Utilisation et quotas
|
||||
description: Suivez la part de votre forfait fournisseur déjà utilisée.
|
||||
---
|
||||
|
||||
# Utilisation et quotas
|
||||
|
||||
La page Utilisation montre quelle part du forfait de chaque fournisseur vous avez consommée, pour savoir à quel point vous approchez d’une limite. Ouvrez-la dans **Paramètres → Utilisation**.
|
||||
|
||||
## Ce que vous voyez
|
||||
|
||||
Pour chaque fournisseur connecté, OpenChamber affiche :
|
||||
|
||||
- la part de la fenêtre actuelle déjà utilisée, sous forme de barre
|
||||
- une répartition par modèle
|
||||
- un indicateur de rythme, pour savoir si vous êtes en voie d’épuiser le quota
|
||||
|
||||
Vous pouvez choisir les fournisseurs affichés, et le même résumé est disponible depuis un menu déroulant dans l’en-tête de l’application.
|
||||
|
||||
## Fournisseurs pris en charge
|
||||
|
||||
L’utilisation fonctionne pour les fournisseurs qui publient un quota, notamment Claude, Codex, GitHub Copilot, Google, OpenRouter, Kimi, NanoGPT, z.ai, Zhipu, MiniMax, Ollama Cloud et Wafer.
|
||||
|
||||
Un fournisseur n’affiche l’utilisation qu’après connexion sur la page [Fournisseurs](/providers/). Quelques fournisseurs demandent une étape supplémentaire — par exemple, Ollama Cloud lit un fichier de session que vous configurez séparément. Si un fournisseur n’affiche aucune donnée, cela signifie généralement que cet identifiant supplémentaire manque.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Fournisseurs, modèles et agents](/providers/) — connectez-vous avant que l’utilisation apparaisse
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Mode vocal
|
||||
description: Parlez à OpenChamber et écoutez les réponses lues à voix haute.
|
||||
---
|
||||
|
||||
# Mode vocal
|
||||
|
||||
Le mode vocal vous permet de dicter des messages et d’entendre les réponses lues à voix haute. Activez-le dans **Paramètres → OpenChamber → Voix**.
|
||||
|
||||
## Lecture des réponses à voix haute (text-to-speech)
|
||||
|
||||
Choisissez comment les réponses sont lues :
|
||||
|
||||
- **browser** — les voix intégrées de votre navigateur, sans configuration
|
||||
- **OpenAI** — les voix d’OpenAI ; collez votre clé API et choisissez une voix
|
||||
- **OpenAI-compatible** — tout service qui parle le format OpenAI ; saisissez son URL et, s’il en demande une, une clé API
|
||||
- **macOS say** — la commande intégrée `say`, sur les Mac qui l’ont
|
||||
|
||||
Une fois activé, un bouton de lecture apparaît sur les messages pour les écouter à voix haute.
|
||||
|
||||
## Dicter des messages (speech-to-text)
|
||||
|
||||
Choisissez comment votre parole est transcrite :
|
||||
|
||||
- **browser** — la reconnaissance intégrée de votre navigateur, sans configuration
|
||||
- **server** — un service de transcription compatible OpenAI ; saisissez son URL et, si nécessaire, une clé API
|
||||
- **on-device** — un modèle vocal qui s’exécute dans votre navigateur et se télécharge à la première utilisation
|
||||
|
||||
## Note pour les téléphones
|
||||
|
||||
Pour lire les réponses à voix haute sur un téléphone, les options OpenAI ou compatibles OpenAI sont les plus fiables — les navigateurs mobiles limitent les voix intégrées.
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Notifications](/notifications/) — recevoir un ping au lieu d’écouter
|
||||
- [Fournisseurs, modèles et agents](/providers/) — là où votre clé OpenAI existe peut-être déjà
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Sessions worktree
|
||||
description: Donnez à une session sa propre branche et son propre dossier pour garder le travail isolé.
|
||||
---
|
||||
|
||||
# Sessions worktree
|
||||
|
||||
Une session worktree s’exécute dans sa propre copie extraite de votre dépôt, sur sa propre branche (un git worktree). Les sessions parallèles ne se marchent donc pas sur les fichiers — l’une peut refactorer pendant qu’une autre corrige un bug, sans allers-retours constants.
|
||||
|
||||
## En créer une
|
||||
|
||||
1. Ouvrez la boîte de dialogue nouveau worktree depuis le bouton en haut de la barre latérale des sessions.
|
||||
2. Choisissez un point de départ :
|
||||
- **new branch** — nommez la branche et choisissez depuis quelle branche partir
|
||||
- **existing branch** — récupérez une branche que vous avez déjà
|
||||
3. Confirmez le dossier du worktree (OpenChamber en propose un à partir du nom de branche).
|
||||
4. Créez-le.
|
||||
|
||||
OpenChamber crée la branche, prépare le dossier et démarre une session dedans. Vous pouvez aussi en lancer une directement depuis une [todo](/notes-todos-plans/) ou une [issue ou PR GitHub](/github/).
|
||||
|
||||
## Ramener le travail
|
||||
|
||||
Quand le travail est bon, utilisez **Integrate** dans la vue Git pour ramener les commits du worktree sur une autre branche (comme `main`). Si un changement entre en conflit, vous pouvez confier la résolution du conflit à l’agent.
|
||||
|
||||
## Nettoyer
|
||||
|
||||
Supprimer ou archiver la session peut retirer le worktree. Vous choisissez aussi si la branche doit être supprimée — locale, et distante s’il y en a une. Rien n’est supprimé sans votre demande.
|
||||
|
||||
## Si quelque chose semble étrange
|
||||
|
||||
Un worktree peut demander votre attention si son dossier a disparu, si sa branche est détachée, ou si un merge ou rebase est à moitié terminé. OpenChamber signale ces cas pour que vous puissiez les corriger — voir [Worktrees et Git](/troubleshooting/worktrees-git/).
|
||||
|
||||
## Pages liées
|
||||
|
||||
- [Multi-run](/multi-run/) — lancer plusieurs sessions worktree à la fois
|
||||
- [Workflows Git et GitHub](/git/) — commiter et intégrer depuis OpenChamber
|
||||
@@ -8,7 +8,8 @@
|
||||
"es": "Empieza aquí",
|
||||
"pt-BR": "Comece aqui",
|
||||
"ko": "여기서 시작",
|
||||
"pl": "Zacznij tutaj"
|
||||
"pl": "Zacznij tutaj",
|
||||
"fr": "Commencer ici"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
@@ -20,7 +21,8 @@
|
||||
"es": "Visión general",
|
||||
"pt-BR": "Visão geral",
|
||||
"ko": "개요",
|
||||
"pl": "Przegląd"
|
||||
"pl": "Przegląd",
|
||||
"fr": "Vue d’ensemble"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -32,7 +34,8 @@
|
||||
"es": "Instalación",
|
||||
"pt-BR": "Instalação",
|
||||
"ko": "설치",
|
||||
"pl": "Instalacja"
|
||||
"pl": "Instalacja",
|
||||
"fr": "Installation"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -44,7 +47,8 @@
|
||||
"es": "Inicio rápido",
|
||||
"pt-BR": "Início rápido",
|
||||
"ko": "빠른 시작",
|
||||
"pl": "Szybki start"
|
||||
"pl": "Szybki start",
|
||||
"fr": "Démarrage rapide"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -56,7 +60,8 @@
|
||||
"es": "Servidor OpenCode",
|
||||
"pt-BR": "Servidor OpenCode",
|
||||
"ko": "OpenCode 서버",
|
||||
"pl": "Serwer OpenCode"
|
||||
"pl": "Serwer OpenCode",
|
||||
"fr": "Serveur OpenCode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68,7 +73,8 @@
|
||||
"es": "Variables de entorno",
|
||||
"pt-BR": "Variáveis de ambiente",
|
||||
"ko": "환경 변수",
|
||||
"pl": "Zmienne środowiskowe"
|
||||
"pl": "Zmienne środowiskowe",
|
||||
"fr": "Variables d’environnement"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -81,7 +87,8 @@
|
||||
"es": "Flujos de trabajo",
|
||||
"pt-BR": "Fluxos de trabalho",
|
||||
"ko": "워크플로",
|
||||
"pl": "Przepływy pracy"
|
||||
"pl": "Przepływy pracy",
|
||||
"fr": "Workflows"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
@@ -93,7 +100,8 @@
|
||||
"es": "Proyectos",
|
||||
"pt-BR": "Projetos",
|
||||
"ko": "프로젝트",
|
||||
"pl": "Projekty"
|
||||
"pl": "Projekty",
|
||||
"fr": "Projets"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -105,7 +113,8 @@
|
||||
"es": "Contexto",
|
||||
"pt-BR": "Contexto",
|
||||
"ko": "컨텍스트",
|
||||
"pl": "Kontekst"
|
||||
"pl": "Kontekst",
|
||||
"fr": "Contexte"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -117,7 +126,8 @@
|
||||
"es": "Notas, tareas y planes",
|
||||
"pt-BR": "Notas, tarefas e planos",
|
||||
"ko": "메모, 할 일, 계획",
|
||||
"pl": "Notatki, zadania i plany"
|
||||
"pl": "Notatki, zadania i plany",
|
||||
"fr": "Notes, todos et plans"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -129,7 +139,8 @@
|
||||
"es": "Tareas programadas",
|
||||
"pt-BR": "Tarefas agendadas",
|
||||
"ko": "예약 작업",
|
||||
"pl": "Zaplanowane zadania"
|
||||
"pl": "Zaplanowane zadania",
|
||||
"fr": "Tâches planifiées"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -141,7 +152,8 @@
|
||||
"es": "Acciones del proyecto",
|
||||
"pt-BR": "Ações do projeto",
|
||||
"ko": "프로젝트 작업",
|
||||
"pl": "Akcje projektu"
|
||||
"pl": "Akcje projektu",
|
||||
"fr": "Actions de projet"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -153,7 +165,8 @@
|
||||
"es": "Vista previa y servidores de desarrollo",
|
||||
"pt-BR": "Pré-visualização e servidores de desenvolvimento",
|
||||
"ko": "미리보기 및 개발 서버",
|
||||
"pl": "Podgląd i serwery deweloperskie"
|
||||
"pl": "Podgląd i serwery deweloperskie",
|
||||
"fr": "Aperçu et serveurs de dev"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -165,16 +178,23 @@
|
||||
"es": "Sesiones de worktree",
|
||||
"pt-BR": "Sessões de worktree",
|
||||
"ko": "Worktree 세션",
|
||||
"pl": "Sesje worktree"
|
||||
"pl": "Sesje worktree",
|
||||
"fr": "Sessions worktree"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Multi-run",
|
||||
"link": "/multi-run/"
|
||||
"link": "/multi-run/",
|
||||
"translations": {
|
||||
"fr": "Multi-run"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Git & GitHub",
|
||||
"link": "/git/"
|
||||
"link": "/git/",
|
||||
"translations": {
|
||||
"fr": "Git et GitHub"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "GitHub Issues & PRs",
|
||||
@@ -185,7 +205,8 @@
|
||||
"es": "Issues y PRs de GitHub",
|
||||
"pt-BR": "Issues e PRs do GitHub",
|
||||
"ko": "GitHub 이슈 및 PR",
|
||||
"pl": "Zgłoszenia i PR-y GitHub"
|
||||
"pl": "Zgłoszenia i PR-y GitHub",
|
||||
"fr": "Issues et PR GitHub"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -197,7 +218,8 @@
|
||||
"es": "Prompts mágicos",
|
||||
"pt-BR": "Prompts mágicos",
|
||||
"ko": "매직 프롬프트",
|
||||
"pl": "Magiczne prompty"
|
||||
"pl": "Magiczne prompty",
|
||||
"fr": "Magic Prompts"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -209,7 +231,8 @@
|
||||
"es": "Identidades de Git",
|
||||
"pt-BR": "Identidades do Git",
|
||||
"ko": "Git 아이덴티티",
|
||||
"pl": "Tożsamości Git"
|
||||
"pl": "Tożsamości Git",
|
||||
"fr": "Identités Git"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -222,7 +245,8 @@
|
||||
"es": "Configuración de OpenCode",
|
||||
"pt-BR": "Configuração do OpenCode",
|
||||
"ko": "OpenCode 설정",
|
||||
"pl": "Konfiguracja OpenCode"
|
||||
"pl": "Konfiguracja OpenCode",
|
||||
"fr": "Configuration OpenCode"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
@@ -234,7 +258,8 @@
|
||||
"es": "Proveedores, modelos y agentes",
|
||||
"pt-BR": "Provedores, modelos e agentes",
|
||||
"ko": "공급자, 모델, 에이전트",
|
||||
"pl": "Dostawcy, modele i agenci"
|
||||
"pl": "Dostawcy, modele i agenci",
|
||||
"fr": "Fournisseurs, modèles et agents"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -246,7 +271,8 @@
|
||||
"es": "Servidores MCP",
|
||||
"pt-BR": "Servidores MCP",
|
||||
"ko": "MCP 서버",
|
||||
"pl": "Serwery MCP"
|
||||
"pl": "Serwery MCP",
|
||||
"fr": "Serveurs MCP"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -258,7 +284,8 @@
|
||||
"es": "Habilidades",
|
||||
"pt-BR": "Habilidades",
|
||||
"ko": "스킬",
|
||||
"pl": "Umiejętności"
|
||||
"pl": "Umiejętności",
|
||||
"fr": "Skills"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -270,7 +297,8 @@
|
||||
"es": "Catálogo de habilidades",
|
||||
"pt-BR": "Catálogo de habilidades",
|
||||
"ko": "스킬 카탈로그",
|
||||
"pl": "Katalog umiejętności"
|
||||
"pl": "Katalog umiejętności",
|
||||
"fr": "Catalogue de skills"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -282,7 +310,8 @@
|
||||
"es": "Comandos y fragmentos",
|
||||
"pt-BR": "Comandos e trechos",
|
||||
"ko": "명령 및 스니펫",
|
||||
"pl": "Polecenia i fragmenty"
|
||||
"pl": "Polecenia i fragmenty",
|
||||
"fr": "Commandes et snippets"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -294,7 +323,8 @@
|
||||
"es": "Uso y cuotas",
|
||||
"pt-BR": "Uso e cotas",
|
||||
"ko": "사용량 및 할당량",
|
||||
"pl": "Zużycie i limity"
|
||||
"pl": "Zużycie i limity",
|
||||
"fr": "Utilisation et quotas"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -307,7 +337,8 @@
|
||||
"es": "Acceso remoto",
|
||||
"pt-BR": "Acesso remoto",
|
||||
"ko": "원격 접속",
|
||||
"pl": "Dostęp zdalny"
|
||||
"pl": "Dostęp zdalny",
|
||||
"fr": "Accès distant"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
@@ -319,7 +350,8 @@
|
||||
"es": "Túneles",
|
||||
"pt-BR": "Túneis",
|
||||
"ko": "터널",
|
||||
"pl": "Tunele"
|
||||
"pl": "Tunele",
|
||||
"fr": "Tunnels"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -331,7 +363,8 @@
|
||||
"es": "Proxy inverso",
|
||||
"pt-BR": "Proxy reverso",
|
||||
"ko": "리버스 프록시",
|
||||
"pl": "Reverse proxy"
|
||||
"pl": "Reverse proxy",
|
||||
"fr": "Reverse proxy"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -343,7 +376,8 @@
|
||||
"es": "PWA y móvil",
|
||||
"pt-BR": "PWA e celular",
|
||||
"ko": "PWA 및 모바일",
|
||||
"pl": "PWA i urządzenia mobilne"
|
||||
"pl": "PWA i urządzenia mobilne",
|
||||
"fr": "PWA et mobile"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -355,7 +389,8 @@
|
||||
"es": "Seguridad",
|
||||
"pt-BR": "Segurança",
|
||||
"ko": "보안",
|
||||
"pl": "Bezpieczeństwo"
|
||||
"pl": "Bezpieczeństwo",
|
||||
"fr": "Sécurité"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -368,7 +403,8 @@
|
||||
"es": "Personalizar",
|
||||
"pt-BR": "Personalizar",
|
||||
"ko": "맞춤 설정",
|
||||
"pl": "Dostosuj"
|
||||
"pl": "Dostosuj",
|
||||
"fr": "Personnaliser"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
@@ -380,7 +416,8 @@
|
||||
"es": "Temas",
|
||||
"pt-BR": "Temas",
|
||||
"ko": "테마",
|
||||
"pl": "Motywy"
|
||||
"pl": "Motywy",
|
||||
"fr": "Thèmes"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -392,7 +429,8 @@
|
||||
"es": "Notificaciones",
|
||||
"pt-BR": "Notificações",
|
||||
"ko": "알림",
|
||||
"pl": "Powiadomienia"
|
||||
"pl": "Powiadomienia",
|
||||
"fr": "Notifications"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -404,7 +442,8 @@
|
||||
"es": "Modo de voz",
|
||||
"pt-BR": "Modo de voz",
|
||||
"ko": "음성 모드",
|
||||
"pl": "Tryb głosowy"
|
||||
"pl": "Tryb głosowy",
|
||||
"fr": "Mode vocal"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -416,7 +455,8 @@
|
||||
"es": "Iconos de proyecto",
|
||||
"pt-BR": "Ícones de projeto",
|
||||
"ko": "프로젝트 아이콘",
|
||||
"pl": "Ikony projektów"
|
||||
"pl": "Ikony projektów",
|
||||
"fr": "Icônes de projet"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -429,7 +469,8 @@
|
||||
"es": "Escritorio",
|
||||
"pt-BR": "Desktop",
|
||||
"ko": "데스크톱",
|
||||
"pl": "Pulpit"
|
||||
"pl": "Pulpit",
|
||||
"fr": "Desktop"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
@@ -441,7 +482,8 @@
|
||||
"es": "Instancias remotas",
|
||||
"pt-BR": "Instâncias remotas",
|
||||
"ko": "원격 인스턴스",
|
||||
"pl": "Zdalne instancje"
|
||||
"pl": "Zdalne instancje",
|
||||
"fr": "Instances distantes"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -453,7 +495,8 @@
|
||||
"es": "Navegador de escritorio",
|
||||
"pt-BR": "Navegador desktop",
|
||||
"ko": "데스크톱 브라우저",
|
||||
"pl": "Przeglądarka na pulpicie"
|
||||
"pl": "Przeglądarka na pulpicie",
|
||||
"fr": "Navigateur desktop"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -465,7 +508,8 @@
|
||||
"es": "Túneles de escritorio",
|
||||
"pt-BR": "Túneis no desktop",
|
||||
"ko": "데스크톱 터널",
|
||||
"pl": "Tunele w aplikacji desktopowej"
|
||||
"pl": "Tunele w aplikacji desktopowej",
|
||||
"fr": "Tunnels desktop"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -477,7 +521,8 @@
|
||||
"es": "Hosts SSH y proxy",
|
||||
"pt-BR": "Hosts SSH e proxy",
|
||||
"ko": "SSH 호스트 및 프록시",
|
||||
"pl": "Hosty SSH i proxy"
|
||||
"pl": "Hosty SSH i proxy",
|
||||
"fr": "Hosts SSH et proxy"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -489,7 +534,8 @@
|
||||
"es": "Actualizaciones",
|
||||
"pt-BR": "Atualizações",
|
||||
"ko": "업데이트",
|
||||
"pl": "Aktualizacje"
|
||||
"pl": "Aktualizacje",
|
||||
"fr": "Mises à jour"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -502,7 +548,8 @@
|
||||
"es": "Ayuda",
|
||||
"pt-BR": "Ajuda",
|
||||
"ko": "도움말",
|
||||
"pl": "Pomoc"
|
||||
"pl": "Pomoc",
|
||||
"fr": "Aide"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
@@ -514,7 +561,8 @@
|
||||
"es": "Resolución de problemas",
|
||||
"pt-BR": "Solução de problemas",
|
||||
"ko": "문제 해결",
|
||||
"pl": "Rozwiązywanie problemów"
|
||||
"pl": "Rozwiązywanie problemów",
|
||||
"fr": "Dépannage"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -526,7 +574,8 @@
|
||||
"es": "Conexión con OpenCode",
|
||||
"pt-BR": "Conexão com o OpenCode",
|
||||
"ko": "OpenCode 연결",
|
||||
"pl": "Połączenie z OpenCode"
|
||||
"pl": "Połączenie z OpenCode",
|
||||
"fr": "Connexion à OpenCode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -538,7 +587,8 @@
|
||||
"es": "Worktrees y Git",
|
||||
"pt-BR": "Worktrees e Git",
|
||||
"ko": "Worktree 및 Git",
|
||||
"pl": "Worktree i Git"
|
||||
"pl": "Worktree i Git",
|
||||
"fr": "Worktrees et Git"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -550,7 +600,8 @@
|
||||
"es": "Acceso remoto",
|
||||
"pt-BR": "Acesso remoto",
|
||||
"ko": "원격 접속",
|
||||
"pl": "Dostęp zdalny"
|
||||
"pl": "Dostęp zdalny",
|
||||
"fr": "Accès distant"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -36,7 +36,7 @@ import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useModelLists } from '@/hooks/useModelLists';
|
||||
import { useIsTextTruncated } from '@/hooks/useIsTextTruncated';
|
||||
import { formatEffortLabel, getCycledPrimaryAgentName, type MobileControlsPanel } from './mobileControlsUtils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
||||
import { useOpenCodeReadiness } from '@/hooks/useOpenCodeReadiness';
|
||||
import { eventMatchesShortcut, getEffectiveShortcutCombo, normalizeCombo } from '@/lib/shortcuts';
|
||||
import { markStartupTrace } from '@/lib/startupTrace';
|
||||
@@ -164,27 +164,27 @@ const getModalityIcons = (metadata: ModelMetadata | undefined, direction: 'input
|
||||
return result;
|
||||
};
|
||||
|
||||
const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', {
|
||||
const formatCompactNumber = (value: number) => new Intl.NumberFormat(getCurrentIntlLocale(), {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
maximumFractionDigits: 1,
|
||||
minimumFractionDigits: 0,
|
||||
});
|
||||
}).format(value);
|
||||
|
||||
const CURRENCY_FORMATTER = new Intl.NumberFormat('en-US', {
|
||||
const formatUsdCurrency = (value: number) => new Intl.NumberFormat(getCurrentIntlLocale(), {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 4,
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
}).format(value);
|
||||
|
||||
const KNOWLEDGE_DATE_FORMATTER = new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' });
|
||||
const formatKnowledgeDate = (value: Date) => new Intl.DateTimeFormat(getCurrentIntlLocale(), { month: 'short', year: 'numeric' }).format(value);
|
||||
|
||||
const DATE_FORMATTER = new Intl.DateTimeFormat('en-US', {
|
||||
const formatReleaseDate = (value: Date) => new Intl.DateTimeFormat(getCurrentIntlLocale(), {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
}).format(value);
|
||||
|
||||
const ADD_PROVIDER_ID = '__add_provider__';
|
||||
|
||||
@@ -226,7 +226,7 @@ const formatTokens = (value?: number | null) => {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const formatted = COMPACT_NUMBER_FORMATTER.format(value);
|
||||
const formatted = formatCompactNumber(value);
|
||||
return formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted;
|
||||
};
|
||||
|
||||
@@ -235,7 +235,7 @@ const formatCost = (value?: number | null) => {
|
||||
return '—';
|
||||
}
|
||||
|
||||
return CURRENCY_FORMATTER.format(value);
|
||||
return formatUsdCurrency(value);
|
||||
};
|
||||
|
||||
const getCapabilityIcons = (metadata?: ModelMetadata) => {
|
||||
@@ -259,7 +259,7 @@ const formatKnowledge = (knowledge?: string) => {
|
||||
const monthIndex = Number.parseInt(match[2], 10) - 1;
|
||||
const knowledgeDate = new Date(Date.UTC(year, monthIndex, 1));
|
||||
if (!Number.isNaN(knowledgeDate.getTime())) {
|
||||
return KNOWLEDGE_DATE_FORMATTER.format(knowledgeDate);
|
||||
return formatKnowledgeDate(knowledgeDate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ const formatDate = (value?: string) => {
|
||||
return value;
|
||||
}
|
||||
|
||||
return DATE_FORMATTER.format(parsedDate);
|
||||
return formatReleaseDate(parsedDate);
|
||||
};
|
||||
|
||||
interface ModelControlsProps {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useSessionMessageRecords } from '@/sync/sync-context';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import type { Part } from '@opencode-ai/sdk/v2';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -56,7 +56,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({
|
||||
if (diffMins < 60) return t('chat.timeline.relative.minutesAgo', { count: diffMins });
|
||||
if (diffHours < 24) return t('chat.timeline.relative.hoursAgo', { count: diffHours });
|
||||
if (diffDays < 7) return t('chat.timeline.relative.daysAgo', { count: diffDays });
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
return new Date(timestamp).toLocaleDateString(getCurrentIntlLocale());
|
||||
}, [t]);
|
||||
|
||||
// Timeline actions are only valid for user messages.
|
||||
|
||||
@@ -969,7 +969,7 @@ const AssistantMessageBody = React.memo(({
|
||||
errorVariant = 'error',
|
||||
reviewTransferDirection = null,
|
||||
}: Omit<MessageBodyProps, 'isUser'>) => {
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const chatSurfaceMode = useChatSurfaceMode();
|
||||
const streamPhase = _streamPhase;
|
||||
void _allowAnimation;
|
||||
@@ -1879,6 +1879,7 @@ const AssistantMessageBody = React.memo(({
|
||||
}, [isLastAssistantInTurn, hasStopFinish, turnGroupingContext?.userMessageCreatedAt, messageCompletedAt]);
|
||||
|
||||
const footerTimestamp = React.useMemo(() => {
|
||||
void locale;
|
||||
const timestamp = typeof messageCompletedAt === 'number' && messageCompletedAt > 0
|
||||
? messageCompletedAt
|
||||
: (typeof messageCreatedAt === 'number' && messageCreatedAt > 0 ? messageCreatedAt : null);
|
||||
@@ -1886,7 +1887,7 @@ const AssistantMessageBody = React.memo(({
|
||||
|
||||
const formatted = formatTimestampForDisplay(timestamp, timeFormatPreference);
|
||||
return formatted.length > 0 ? formatted : null;
|
||||
}, [messageCompletedAt, messageCreatedAt, timeFormatPreference]);
|
||||
}, [messageCompletedAt, messageCreatedAt, timeFormatPreference, locale]);
|
||||
|
||||
const footerTimestampClassName = 'text-sm text-muted-foreground/60 tabular-nums flex items-center gap-1';
|
||||
const canOpenMessagePreview = !isMiniChatSurface && !isMobile && !isVSCode;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getCurrentIntlLocale } from '@/lib/i18n';
|
||||
import { formatMessage, useI18nStore } from '@/lib/i18n/store';
|
||||
import { formatTimeForPreference } from '@/lib/timeFormat';
|
||||
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
||||
|
||||
@@ -26,18 +28,19 @@ export const formatTimestampForDisplay = (timestamp: number, timeFormatPreferenc
|
||||
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
|
||||
const timePart = formatTimeForPreference(date, timeFormatPreference);
|
||||
const locale = getCurrentIntlLocale();
|
||||
const dictionary = useI18nStore.getState().dictionary;
|
||||
|
||||
if (isSameDay(date, now)) {
|
||||
return timePart;
|
||||
}
|
||||
|
||||
if (isYesterday(date, now)) {
|
||||
return `Yesterday ${timePart}`;
|
||||
return formatMessage(dictionary, 'common.date.yesterdayWithTime', { time: timePart });
|
||||
}
|
||||
|
||||
const monthPart = date.toLocaleString(undefined, { month: 'short' });
|
||||
const monthPart = date.toLocaleString(locale, { month: 'short' });
|
||||
const dayPart = date.getDate();
|
||||
const datePart = `${monthPart} ${dayPart}`;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useSessions, useSessionMessageRecords } from '@/sync/sync-context';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
||||
import { formatDateTimeForPreference } from '@/lib/timeFormat';
|
||||
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
|
||||
|
||||
@@ -223,12 +223,16 @@ const computeContextBreakdown = (
|
||||
};
|
||||
};
|
||||
|
||||
const formatNumber = (value: number): string => value.toLocaleString();
|
||||
const formatNumber = (value: number): string => value.toLocaleString(getCurrentIntlLocale());
|
||||
|
||||
const formatMoney = (value: number): string => {
|
||||
if (!Number.isFinite(value) || value <= 0) return '$0.00';
|
||||
if (value < 0.01) return `$${value.toFixed(4)}`;
|
||||
return `$${value.toFixed(2)}`;
|
||||
if (!Number.isFinite(value) || value <= 0) return new Intl.NumberFormat(getCurrentIntlLocale(), { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(0);
|
||||
return new Intl.NumberFormat(getCurrentIntlLocale(), {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: value < 0.01 ? 4 : 2,
|
||||
maximumFractionDigits: value < 0.01 ? 4 : 2,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatDateTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference): string => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { getCurrentIntlLocale } from '@/lib/i18n';
|
||||
import { mergeModelMetadataWithLiveModel } from '@/lib/modelMetadata';
|
||||
import { getModelDisplayName as getSharedModelDisplayName } from '@/lib/modelDisplay';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -44,19 +45,19 @@ type IndexSelectionStore = {
|
||||
set: (value: number) => void;
|
||||
};
|
||||
|
||||
const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', {
|
||||
const formatCompactNumber = (value: number) => new Intl.NumberFormat(getCurrentIntlLocale(), {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
maximumFractionDigits: 1,
|
||||
minimumFractionDigits: 0,
|
||||
});
|
||||
}).format(value);
|
||||
|
||||
const CURRENCY_FORMATTER = new Intl.NumberFormat('en-US', {
|
||||
const formatUsdCurrency = (value: number) => new Intl.NumberFormat(getCurrentIntlLocale(), {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 4,
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
}).format(value);
|
||||
|
||||
const getModelDisplayName = (model: Record<string, unknown>) => {
|
||||
return getSharedModelDisplayName(model, undefined, { maxLength: 40 });
|
||||
@@ -65,13 +66,13 @@ const getModelDisplayName = (model: Record<string, unknown>) => {
|
||||
const formatModelContextTokens = (value?: number | null) => {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return '';
|
||||
if (value === 0) return '0';
|
||||
const formatted = COMPACT_NUMBER_FORMATTER.format(value);
|
||||
const formatted = formatCompactNumber(value);
|
||||
return formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted;
|
||||
};
|
||||
|
||||
const formatCost = (value?: number | null) => {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) return '—';
|
||||
return CURRENCY_FORMATTER.format(value);
|
||||
return formatUsdCurrency(value);
|
||||
};
|
||||
|
||||
const hasTooltipMetadata = (metadata?: ModelMetadata) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type PasskeyStatus,
|
||||
type StoredPasskey,
|
||||
} from '@/lib/passkeys';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
||||
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
|
||||
|
||||
const formatTimestamp = (timestamp: number | null, neverUsedText: string, timeFormatPreference: TimeFormatPreference) => {
|
||||
@@ -22,7 +22,7 @@ const formatTimestamp = (timestamp: number | null, neverUsedText: string, timeFo
|
||||
return neverUsedText;
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
return new Intl.DateTimeFormat(getCurrentIntlLocale(), {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
hour12: timeFormatPreference === 'auto' ? undefined : timeFormatPreference === '12h',
|
||||
|
||||
@@ -20,16 +20,16 @@ import { cn } from '@/lib/utils';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { openExternalUrl } from '@/lib/url';
|
||||
import type { ModelMetadata } from '@/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
|
||||
const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', {
|
||||
const formatCompactNumber = (value: number) => new Intl.NumberFormat(getCurrentIntlLocale(), {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
maximumFractionDigits: 1,
|
||||
minimumFractionDigits: 0,
|
||||
});
|
||||
}).format(value);
|
||||
|
||||
const formatTokens = (value?: number | null) => {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
@@ -38,7 +38,7 @@ const formatTokens = (value?: number | null) => {
|
||||
if (value === 0) {
|
||||
return '0';
|
||||
}
|
||||
const formatted = COMPACT_NUMBER_FORMATTER.format(value);
|
||||
const formatted = formatCompactNumber(value);
|
||||
return formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted;
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ import { useInputStore } from '@/sync/input-store';
|
||||
import { createWorktreeSessionForNewBranch } from '@/lib/worktreeSessionCreator';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { renderMagicPrompt } from '@/lib/magicPrompts';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { TodoSendDialog, type TodoSendExecution } from './TodoSendDialog';
|
||||
|
||||
@@ -972,7 +972,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
||||
>
|
||||
<span className="min-w-0 truncate typography-ui-label text-foreground">{plan.title}</span>
|
||||
<span className="flex-shrink-0 typography-micro text-muted-foreground">
|
||||
{new Date(plan.createdAt).toLocaleDateString()}
|
||||
{new Date(plan.createdAt).toLocaleDateString(getCurrentIntlLocale())}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import React from 'react';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import { getCurrentIntlLocale } from '@/lib/i18n';
|
||||
import { formatMessage, useI18nStore } from '@/lib/i18n/store';
|
||||
import type { SessionSummaryMeta } from './types';
|
||||
|
||||
const t = (key: Parameters<typeof formatMessage>[1], params?: Parameters<typeof formatMessage>[2]) =>
|
||||
formatMessage(useI18nStore.getState().dictionary, key, params);
|
||||
|
||||
const formatDateLabel = (value: string | number) => {
|
||||
const targetDate = new Date(value);
|
||||
const today = new Date();
|
||||
@@ -14,12 +19,12 @@ const formatDateLabel = (value: string | number) => {
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
|
||||
if (isSameDay(targetDate, today)) {
|
||||
return 'Today';
|
||||
return t('common.date.today');
|
||||
}
|
||||
if (isSameDay(targetDate, yesterday)) {
|
||||
return 'Yesterday';
|
||||
return t('common.date.yesterday');
|
||||
}
|
||||
const formatted = targetDate.toLocaleDateString('en-US', {
|
||||
const formatted = targetDate.toLocaleDateString(getCurrentIntlLocale(), {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
@@ -37,9 +42,9 @@ export const formatSessionDateLabel = (updatedMs: number): string => {
|
||||
|
||||
if (isSameDay(updatedDate, today)) {
|
||||
const diff = Date.now() - updatedMs;
|
||||
if (diff < 60_000) return 'Just now';
|
||||
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}min ago`;
|
||||
return `${Math.floor(diff / 3_600_000)}h ago`;
|
||||
if (diff < 60_000) return t('common.relative.justNow');
|
||||
if (diff < 3_600_000) return t('common.relative.minutesAgoShort', { count: Math.floor(diff / 60_000) });
|
||||
return t('common.relative.hoursAgoShort', { count: Math.floor(diff / 3_600_000) });
|
||||
}
|
||||
|
||||
return formatDateLabel(updatedMs);
|
||||
@@ -62,15 +67,15 @@ export const formatSessionCompactDateLabel = (updatedMs: number): string => {
|
||||
return `${Math.floor(diff / hour)}h`;
|
||||
}
|
||||
if (diff < week) {
|
||||
return `${Math.floor(diff / day)}d`;
|
||||
return t('common.relative.daysAgoCompact', { count: Math.floor(diff / day) });
|
||||
}
|
||||
if (diff < 5 * week) {
|
||||
return `${Math.floor(diff / week)}w`;
|
||||
return t('common.relative.weeksAgoCompact', { count: Math.floor(diff / week) });
|
||||
}
|
||||
if (diff < year) {
|
||||
return `${Math.floor(diff / month)}mo`;
|
||||
}
|
||||
return `${Math.floor(diff / year)}y`;
|
||||
return t('common.relative.yearsAgoCompact', { count: Math.floor(diff / year) });
|
||||
};
|
||||
|
||||
export const normalizePath = (value?: string | null) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { cn } from '@/lib/utils';
|
||||
import type { UpdateInfo, UpdateProgress } from '@/lib/desktop';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { openExternalUrl } from '@/lib/url';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
|
||||
type WebUpdateState = 'idle' | 'updating' | 'restarting' | 'reconnecting' | 'error';
|
||||
@@ -57,7 +57,7 @@ function formatIsoDateForUI(isoDate: string): string {
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return isoDate;
|
||||
}
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
return new Intl.DateTimeFormat(getCurrentIntlLocale(), {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { FilesAPI } from '@/lib/api/types';
|
||||
import { MAX_OPEN_FILE_LINES, countLinesWithLimit } from '@/lib/fileOpenLimits';
|
||||
import { getCurrentIntlLocale } from '@/lib/i18n';
|
||||
import { formatMessage, useI18nStore } from '@/lib/i18n/store';
|
||||
|
||||
const t = (key: Parameters<typeof formatMessage>[1], params?: Parameters<typeof formatMessage>[2]) =>
|
||||
formatMessage(useI18nStore.getState().dictionary, key, params);
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
|
||||
export type ContextFileOpenFailureReason = 'too-large' | 'missing' | 'unreadable';
|
||||
@@ -60,12 +65,13 @@ export const validateContextFileOpen = async (files: FilesAPI, path: string): Pr
|
||||
|
||||
export const getContextFileOpenFailureMessage = (reason: ContextFileOpenFailureReason): string => {
|
||||
if (reason === 'too-large') {
|
||||
return `File is too large to open (>${MAX_OPEN_FILE_LINES.toLocaleString()} lines)`;
|
||||
const lines = MAX_OPEN_FILE_LINES.toLocaleString(getCurrentIntlLocale());
|
||||
return t('contextFileOpen.failure.tooLarge', { count: lines });
|
||||
}
|
||||
|
||||
if (reason === 'missing') {
|
||||
return 'File not found';
|
||||
return t('contextFileOpen.failure.missing');
|
||||
}
|
||||
|
||||
return 'Failed to open file';
|
||||
return t('contextFileOpen.failure.unreadable');
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Message, Part } from '@opencode-ai/sdk/v2';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { getCurrentIntlLocale } from '@/lib/i18n';
|
||||
import { isVSCodeRuntime, openDesktopPath, revealDesktopPath, saveDesktopMarkdownFile } from '@/lib/desktop';
|
||||
import { getRevealLabelKey } from '@/lib/utils';
|
||||
|
||||
@@ -22,7 +23,7 @@ function formatTimestamp(timestamp: number | undefined): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
const monthPart = date.toLocaleString(undefined, { month: 'short' });
|
||||
const monthPart = date.toLocaleString(getCurrentIntlLocale(), { month: 'short' });
|
||||
const dayPart = date.getDate();
|
||||
const yearPart = date.getFullYear();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { LOCALE_STORAGE_KEY, normalizeLocale, type Locale } from './runtime';
|
||||
|
||||
type BootstrapMessages = {
|
||||
startingApi: string;
|
||||
initializing: string;
|
||||
connecting: string;
|
||||
connected: string;
|
||||
connectionError: string;
|
||||
disconnected: string;
|
||||
reconnecting: string;
|
||||
initialDataLoadFailed: string;
|
||||
cliNotFound: string;
|
||||
providersReady: string;
|
||||
providersLoading: string;
|
||||
agentsReady: string;
|
||||
agentsLoading: string;
|
||||
startingDevServer: (hostLabel: string) => string;
|
||||
waitingDevServer: (hostLabel: string, attempt: number) => string;
|
||||
loadingData: (providersText: string, agentsText: string) => string;
|
||||
};
|
||||
|
||||
const EN_MESSAGES: BootstrapMessages = {
|
||||
startingApi: 'Starting OpenCode API…',
|
||||
initializing: 'Initializing…',
|
||||
connecting: 'Connecting…',
|
||||
connected: 'Connected!',
|
||||
connectionError: 'Connection error',
|
||||
disconnected: 'Disconnected',
|
||||
reconnecting: 'Reconnecting…',
|
||||
initialDataLoadFailed: 'OpenCode connected, but initial data load failed.',
|
||||
cliNotFound: 'OpenCode CLI not found. Please install it first.',
|
||||
providersReady: '✓ Providers',
|
||||
providersLoading: '… Providers',
|
||||
agentsReady: '✓ Agents',
|
||||
agentsLoading: '… Agents',
|
||||
startingDevServer: (hostLabel) => `Starting webview dev server (${hostLabel})...`,
|
||||
waitingDevServer: (hostLabel, attempt) => `Waiting for webview dev server (${hostLabel})... attempt ${attempt}`,
|
||||
loadingData: (providersText, agentsText) => `Loading data (${providersText}, ${agentsText})…`,
|
||||
};
|
||||
|
||||
const FR_MESSAGES: BootstrapMessages = {
|
||||
startingApi: 'Démarrage de l’API OpenCode…',
|
||||
initializing: 'Initialisation…',
|
||||
connecting: 'Connexion…',
|
||||
connected: 'Connecté !',
|
||||
connectionError: 'Erreur de connexion',
|
||||
disconnected: 'Déconnecté',
|
||||
reconnecting: 'Reconnexion…',
|
||||
initialDataLoadFailed: 'OpenCode est connecté, mais le chargement initial des données a échoué.',
|
||||
cliNotFound: 'L’interface en ligne de commande OpenCode est introuvable. Veuillez l’installer d’abord.',
|
||||
providersReady: '✓ Fournisseurs',
|
||||
providersLoading: '… Fournisseurs',
|
||||
agentsReady: '✓ Agents',
|
||||
agentsLoading: '… Agents',
|
||||
startingDevServer: (hostLabel) => `Démarrage du serveur de développement de la webview (${hostLabel})...`,
|
||||
waitingDevServer: (hostLabel, attempt) => `En attente du serveur de développement de la webview (${hostLabel})... tentative ${attempt}`,
|
||||
loadingData: (providersText, agentsText) => `Chargement des données (${providersText}, ${agentsText})…`,
|
||||
};
|
||||
|
||||
export const getBootstrapMessages = (locale: Locale): BootstrapMessages => (locale === 'fr' ? FR_MESSAGES : EN_MESSAGES);
|
||||
|
||||
export const readStoredLocaleForBootstrap = (): Locale => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'en';
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(LOCALE_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return 'en';
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw) as { locale?: unknown };
|
||||
return typeof parsed.locale === 'string' ? normalizeLocale(parsed.locale) : 'en';
|
||||
} catch {
|
||||
return 'en';
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
export { I18nProvider } from './context';
|
||||
export { getBootstrapMessages, readStoredLocaleForBootstrap } from './bootstrap';
|
||||
export { getCurrentIntlLocale, getIntlLocale } from './intl';
|
||||
export { useI18n } from './useI18n';
|
||||
export { formatMessage, initializeLocale, useI18nStore } from './store';
|
||||
export type { I18nKey, I18nParams, Locale } from './store';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useI18nStore } from './store';
|
||||
import type { Locale } from './runtime';
|
||||
|
||||
const INTL_LOCALE_BY_LOCALE: Record<Locale, string> = {
|
||||
en: 'en-US',
|
||||
fr: 'fr-FR',
|
||||
'zh-CN': 'zh-CN',
|
||||
'zh-TW': 'zh-TW',
|
||||
uk: 'uk-UA',
|
||||
es: 'es-ES',
|
||||
'pt-BR': 'pt-BR',
|
||||
ko: 'ko-KR',
|
||||
pl: 'pl-PL',
|
||||
};
|
||||
|
||||
export const getIntlLocale = (locale: Locale): string => INTL_LOCALE_BY_LOCALE[locale] ?? 'en-US';
|
||||
|
||||
export const getCurrentIntlLocale = (): string => getIntlLocale(useI18nStore.getState().locale);
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { dict as enDict } from './messages/en';
|
||||
import { dict as esDict } from './messages/es';
|
||||
import { dict as frDict } from './messages/fr';
|
||||
import { dict as koDict } from './messages/ko';
|
||||
import { dict as plDict } from './messages/pl';
|
||||
import { dict as ptBrDict } from './messages/pt-BR';
|
||||
import { dict as ukDict } from './messages/uk';
|
||||
import { dict as zhCnDict } from './messages/zh-CN';
|
||||
import { dict as zhTwDict } from './messages/zh-TW';
|
||||
|
||||
const localeDictionaries = {
|
||||
en: enDict,
|
||||
fr: frDict,
|
||||
es: esDict,
|
||||
'pt-BR': ptBrDict,
|
||||
uk: ukDict,
|
||||
ko: koDict,
|
||||
pl: plDict,
|
||||
'zh-CN': zhCnDict,
|
||||
'zh-TW': zhTwDict,
|
||||
} as const;
|
||||
|
||||
describe('i18n dictionaries', () => {
|
||||
test('french stays in key parity with english', () => {
|
||||
const englishKeys = Object.keys(enDict).sort();
|
||||
|
||||
expect(Object.keys(frDict).sort()).toEqual(englishKeys);
|
||||
});
|
||||
|
||||
test('all locales expose the french language label key', () => {
|
||||
for (const [, dictionary] of Object.entries(localeDictionaries)) {
|
||||
expect(dictionary['common.language.french']).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ export const dict = {
|
||||
'common.loading': 'Loading...',
|
||||
'common.unavailable': 'Unavailable',
|
||||
'common.language.english': 'English',
|
||||
'common.language.french': 'French',
|
||||
'common.language.simplifiedChinese': 'Chinese (Simplified)',
|
||||
'common.language.traditionalChinese': 'Chinese (Traditional)',
|
||||
'common.language.ukrainian': 'Ukrainian',
|
||||
@@ -2546,6 +2547,35 @@ export const dict = {
|
||||
'markdownRenderer.mermaid.actions.copySourceTitle': 'Copy source',
|
||||
'markdownRenderer.mermaid.actions.downloadSvgTitle': 'Download SVG',
|
||||
'markdownRenderer.mermaid.toast.downloadFailed': 'Failed to download diagram',
|
||||
'common.date.today': 'Today',
|
||||
'common.date.yesterday': 'Yesterday',
|
||||
'common.date.yesterdayWithTime': 'Yesterday {time}',
|
||||
'common.relative.justNow': 'Just now',
|
||||
'common.relative.minutesAgoShort': '{count}min ago',
|
||||
'common.relative.hoursAgoShort': '{count}h ago',
|
||||
'common.relative.daysAgoShort': '{count}d ago',
|
||||
'common.relative.weeksAgoShort': '{count}w ago',
|
||||
'common.relative.yearsAgoShort': '{count}y ago',
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
'quota.window.5h': '5-Hour',
|
||||
'quota.window.7d': '7-Day Limit',
|
||||
'quota.window.7dSonnet': '7-Day Sonnet Limit',
|
||||
'quota.window.7dOpus': '7-Day Opus Limit',
|
||||
'quota.window.weekly': 'Weekly Limit',
|
||||
'quota.window.daily': 'Daily',
|
||||
'quota.window.monthly': 'Monthly Limit',
|
||||
'quota.window.credits': 'Credits',
|
||||
'quota.window.creditsBalance': 'Credits Balance',
|
||||
'quota.window.session': 'Session',
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
} as const;
|
||||
|
||||
export type I18nKey = keyof typeof dict;
|
||||
|
||||
@@ -6,6 +6,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"common.loading": "Cargando...",
|
||||
"common.unavailable": "No disponible",
|
||||
"common.language.english": "Inglés",
|
||||
"common.language.french": "Francés",
|
||||
"common.language.simplifiedChinese": "Chino (simplificado)",
|
||||
"common.language.traditionalChinese": "Chino (tradicional)",
|
||||
"common.language.ukrainian": "Ucraniano",
|
||||
@@ -2547,4 +2548,33 @@ export const dict: Record<I18nKey, string> = {
|
||||
"contextPanel.preview.console.filter.errors": "Errores",
|
||||
"contextPanel.preview.console.filter.warnings": "Advertencias",
|
||||
"contextPanel.preview.console.filter.logs": "Registros",
|
||||
"common.date.today": "Today",
|
||||
"common.date.yesterday": "Yesterday",
|
||||
"common.date.yesterdayWithTime": "Yesterday {time}",
|
||||
"common.relative.justNow": "Just now",
|
||||
"common.relative.minutesAgoShort": "{count}min ago",
|
||||
"common.relative.hoursAgoShort": "{count}h ago",
|
||||
"common.relative.daysAgoShort": "{count}d ago",
|
||||
"common.relative.weeksAgoShort": "{count}w ago",
|
||||
"common.relative.yearsAgoShort": "{count}y ago",
|
||||
"common.relative.daysAgoCompact": "{count}d ago",
|
||||
"common.relative.weeksAgoCompact": "{count}w ago",
|
||||
"common.relative.yearsAgoCompact": "{count}y ago",
|
||||
"contextFileOpen.failure.tooLarge": "File is too large to open (>{count} lines)",
|
||||
"contextFileOpen.failure.missing": "File not found",
|
||||
"contextFileOpen.failure.unreadable": "Failed to open file",
|
||||
"quota.window.5h": "5-Hour",
|
||||
"quota.window.7d": "7-Day Limit",
|
||||
"quota.window.7dSonnet": "7-Day Sonnet Limit",
|
||||
"quota.window.7dOpus": "7-Day Opus Limit",
|
||||
"quota.window.weekly": "Weekly Limit",
|
||||
"quota.window.daily": "Daily",
|
||||
"quota.window.monthly": "Monthly Limit",
|
||||
"quota.window.credits": "Credits",
|
||||
"quota.window.creditsBalance": "Credits Balance",
|
||||
"quota.window.session": "Session",
|
||||
"quota.window.premium": "Premium Interactions",
|
||||
"quota.window.chat": "Chat Requests",
|
||||
"quota.window.completions": "Completions",
|
||||
"quota.window.premiumInteractions": "Premium interactions",
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.loading': '로딩 중...',
|
||||
'common.unavailable': '사용할 수 없음',
|
||||
'common.language.english': '영어',
|
||||
'common.language.french': '프랑스어',
|
||||
'common.language.simplifiedChinese': '중국어(간체)',
|
||||
'common.language.traditionalChinese': '중국어(번체)',
|
||||
'common.language.ukrainian': '우크라이나어',
|
||||
@@ -2546,4 +2547,33 @@ export const dict: Record<I18nKey, string> = {
|
||||
'markdownRenderer.mermaid.actions.copySourceTitle': '소스 복사',
|
||||
'markdownRenderer.mermaid.actions.downloadSvgTitle': 'SVG 다운로드',
|
||||
'markdownRenderer.mermaid.toast.downloadFailed': '다이어그램 다운로드 실패',
|
||||
'common.date.today': 'Today',
|
||||
'common.date.yesterday': 'Yesterday',
|
||||
'common.date.yesterdayWithTime': 'Yesterday {time}',
|
||||
'common.relative.justNow': 'Just now',
|
||||
'common.relative.minutesAgoShort': '{count}min ago',
|
||||
'common.relative.hoursAgoShort': '{count}h ago',
|
||||
'common.relative.daysAgoShort': '{count}d ago',
|
||||
'common.relative.weeksAgoShort': '{count}w ago',
|
||||
'common.relative.yearsAgoShort': '{count}y ago',
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
'quota.window.5h': '5-Hour',
|
||||
'quota.window.7d': '7-Day Limit',
|
||||
'quota.window.7dSonnet': '7-Day Sonnet Limit',
|
||||
'quota.window.7dOpus': '7-Day Opus Limit',
|
||||
'quota.window.weekly': 'Weekly Limit',
|
||||
'quota.window.daily': 'Daily',
|
||||
'quota.window.monthly': 'Monthly Limit',
|
||||
'quota.window.credits': 'Credits',
|
||||
'quota.window.creditsBalance': 'Credits Balance',
|
||||
'quota.window.session': 'Session',
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.loading': 'Ładowanie...',
|
||||
'common.unavailable': 'Niedostępne',
|
||||
'common.language.english': 'Angielski',
|
||||
'common.language.french': 'Francuski',
|
||||
'common.language.simplifiedChinese': 'Chiński (Uproszczony)',
|
||||
'common.language.traditionalChinese': 'Chiński (Tradycyjny)',
|
||||
'common.language.ukrainian': 'Ukraiński',
|
||||
@@ -2563,4 +2564,33 @@ export const dict: Record<I18nKey, string> = {
|
||||
'filesView.editor.manualSave': 'Zapis ręczny',
|
||||
'filesView.editor.saveNowManualTitle': 'Zapisz teraz (tryb ręczny)',
|
||||
'chat.commandAutocomplete.command.timelineDescription': 'Pokaż oś czasu sesji',
|
||||
'common.date.today': 'Today',
|
||||
'common.date.yesterday': 'Yesterday',
|
||||
'common.date.yesterdayWithTime': 'Yesterday {time}',
|
||||
'common.relative.justNow': 'Just now',
|
||||
'common.relative.minutesAgoShort': '{count}min ago',
|
||||
'common.relative.hoursAgoShort': '{count}h ago',
|
||||
'common.relative.daysAgoShort': '{count}d ago',
|
||||
'common.relative.weeksAgoShort': '{count}w ago',
|
||||
'common.relative.yearsAgoShort': '{count}y ago',
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
'quota.window.5h': '5-Hour',
|
||||
'quota.window.7d': '7-Day Limit',
|
||||
'quota.window.7dSonnet': '7-Day Sonnet Limit',
|
||||
'quota.window.7dOpus': '7-Day Opus Limit',
|
||||
'quota.window.weekly': 'Weekly Limit',
|
||||
'quota.window.daily': 'Daily',
|
||||
'quota.window.monthly': 'Monthly Limit',
|
||||
'quota.window.credits': 'Credits',
|
||||
'quota.window.creditsBalance': 'Credits Balance',
|
||||
'quota.window.session': 'Session',
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
} as const;
|
||||
|
||||
@@ -6,6 +6,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"common.loading": "Carregando...",
|
||||
"common.unavailable": "Indisponível",
|
||||
"common.language.english": "Inglês",
|
||||
"common.language.french": "Francês",
|
||||
"common.language.simplifiedChinese": "Chinês (simplificado)",
|
||||
"common.language.traditionalChinese": "Chinês (tradicional)",
|
||||
"common.language.ukrainian": "Ucraniano",
|
||||
@@ -2547,4 +2548,33 @@ export const dict: Record<I18nKey, string> = {
|
||||
"contextPanel.preview.console.filter.errors": "Erros",
|
||||
"contextPanel.preview.console.filter.warnings": "Avisos",
|
||||
"contextPanel.preview.console.filter.logs": "Logs",
|
||||
"common.date.today": "Today",
|
||||
"common.date.yesterday": "Yesterday",
|
||||
"common.date.yesterdayWithTime": "Yesterday {time}",
|
||||
"common.relative.justNow": "Just now",
|
||||
"common.relative.minutesAgoShort": "{count}min ago",
|
||||
"common.relative.hoursAgoShort": "{count}h ago",
|
||||
"common.relative.daysAgoShort": "{count}d ago",
|
||||
"common.relative.weeksAgoShort": "{count}w ago",
|
||||
"common.relative.yearsAgoShort": "{count}y ago",
|
||||
"common.relative.daysAgoCompact": "{count}d ago",
|
||||
"common.relative.weeksAgoCompact": "{count}w ago",
|
||||
"common.relative.yearsAgoCompact": "{count}y ago",
|
||||
"contextFileOpen.failure.tooLarge": "File is too large to open (>{count} lines)",
|
||||
"contextFileOpen.failure.missing": "File not found",
|
||||
"contextFileOpen.failure.unreadable": "Failed to open file",
|
||||
"quota.window.5h": "5-Hour",
|
||||
"quota.window.7d": "7-Day Limit",
|
||||
"quota.window.7dSonnet": "7-Day Sonnet Limit",
|
||||
"quota.window.7dOpus": "7-Day Opus Limit",
|
||||
"quota.window.weekly": "Weekly Limit",
|
||||
"quota.window.daily": "Daily",
|
||||
"quota.window.monthly": "Monthly Limit",
|
||||
"quota.window.credits": "Credits",
|
||||
"quota.window.creditsBalance": "Credits Balance",
|
||||
"quota.window.session": "Session",
|
||||
"quota.window.premium": "Premium Interactions",
|
||||
"quota.window.chat": "Chat Requests",
|
||||
"quota.window.completions": "Completions",
|
||||
"quota.window.premiumInteractions": "Premium interactions",
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"common.loading": "Завантаження...",
|
||||
"common.unavailable": "Недоступно",
|
||||
"common.language.english": "англійська",
|
||||
"common.language.french": "Французька",
|
||||
"common.language.simplifiedChinese": "Китайська (спрощена)",
|
||||
"common.language.traditionalChinese": "Китайська (традиційна)",
|
||||
"common.language.ukrainian": "Українська",
|
||||
@@ -2547,4 +2548,33 @@ export const dict: Record<I18nKey, string> = {
|
||||
"contextPanel.preview.console.filter.errors": "Помилки",
|
||||
"contextPanel.preview.console.filter.warnings": "Попередження",
|
||||
"contextPanel.preview.console.filter.logs": "Логи",
|
||||
"common.date.today": "Today",
|
||||
"common.date.yesterday": "Yesterday",
|
||||
"common.date.yesterdayWithTime": "Yesterday {time}",
|
||||
"common.relative.justNow": "Just now",
|
||||
"common.relative.minutesAgoShort": "{count}min ago",
|
||||
"common.relative.hoursAgoShort": "{count}h ago",
|
||||
"common.relative.daysAgoShort": "{count}d ago",
|
||||
"common.relative.weeksAgoShort": "{count}w ago",
|
||||
"common.relative.yearsAgoShort": "{count}y ago",
|
||||
"common.relative.daysAgoCompact": "{count}d ago",
|
||||
"common.relative.weeksAgoCompact": "{count}w ago",
|
||||
"common.relative.yearsAgoCompact": "{count}y ago",
|
||||
"contextFileOpen.failure.tooLarge": "File is too large to open (>{count} lines)",
|
||||
"contextFileOpen.failure.missing": "File not found",
|
||||
"contextFileOpen.failure.unreadable": "Failed to open file",
|
||||
"quota.window.5h": "5-Hour",
|
||||
"quota.window.7d": "7-Day Limit",
|
||||
"quota.window.7dSonnet": "7-Day Sonnet Limit",
|
||||
"quota.window.7dOpus": "7-Day Opus Limit",
|
||||
"quota.window.weekly": "Weekly Limit",
|
||||
"quota.window.daily": "Daily",
|
||||
"quota.window.monthly": "Monthly Limit",
|
||||
"quota.window.credits": "Credits",
|
||||
"quota.window.creditsBalance": "Credits Balance",
|
||||
"quota.window.session": "Session",
|
||||
"quota.window.premium": "Premium Interactions",
|
||||
"quota.window.chat": "Chat Requests",
|
||||
"quota.window.completions": "Completions",
|
||||
"quota.window.premiumInteractions": "Premium interactions",
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.loading': '加载中...',
|
||||
'common.unavailable': '不可用',
|
||||
'common.language.english': 'English',
|
||||
'common.language.french': '法语',
|
||||
'common.language.simplifiedChinese': '简体中文',
|
||||
'common.language.traditionalChinese': '繁体中文',
|
||||
'common.language.ukrainian': '乌克兰语',
|
||||
@@ -2547,4 +2548,33 @@ export const dict: Record<I18nKey, string> = {
|
||||
'contextPanel.preview.console.filter.errors': '错误',
|
||||
'contextPanel.preview.console.filter.warnings': '警告',
|
||||
'contextPanel.preview.console.filter.logs': '日志',
|
||||
'common.date.today': 'Today',
|
||||
'common.date.yesterday': 'Yesterday',
|
||||
'common.date.yesterdayWithTime': 'Yesterday {time}',
|
||||
'common.relative.justNow': 'Just now',
|
||||
'common.relative.minutesAgoShort': '{count}min ago',
|
||||
'common.relative.hoursAgoShort': '{count}h ago',
|
||||
'common.relative.daysAgoShort': '{count}d ago',
|
||||
'common.relative.weeksAgoShort': '{count}w ago',
|
||||
'common.relative.yearsAgoShort': '{count}y ago',
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
'quota.window.5h': '5-Hour',
|
||||
'quota.window.7d': '7-Day Limit',
|
||||
'quota.window.7dSonnet': '7-Day Sonnet Limit',
|
||||
'quota.window.7dOpus': '7-Day Opus Limit',
|
||||
'quota.window.weekly': 'Weekly Limit',
|
||||
'quota.window.daily': 'Daily',
|
||||
'quota.window.monthly': 'Monthly Limit',
|
||||
'quota.window.credits': 'Credits',
|
||||
'quota.window.creditsBalance': 'Credits Balance',
|
||||
'quota.window.session': 'Session',
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.loading': '載入中...',
|
||||
'common.unavailable': '無法使用',
|
||||
'common.language.english': 'English',
|
||||
'common.language.french': '法語',
|
||||
'common.language.simplifiedChinese': '簡體中文',
|
||||
'common.language.traditionalChinese': '繁體中文',
|
||||
'common.language.ukrainian': '烏克蘭語',
|
||||
@@ -2546,4 +2547,33 @@ export const dict: Record<I18nKey, string> = {
|
||||
'contextPanel.preview.console.filter.logs': '日誌',
|
||||
'filesView.tree.actions.refreshTitle': '重新整理',
|
||||
'sidebarFilesTree.actions.fileMenuTitle': '檔案選單',
|
||||
'common.date.today': 'Today',
|
||||
'common.date.yesterday': 'Yesterday',
|
||||
'common.date.yesterdayWithTime': 'Yesterday {time}',
|
||||
'common.relative.justNow': 'Just now',
|
||||
'common.relative.minutesAgoShort': '{count}min ago',
|
||||
'common.relative.hoursAgoShort': '{count}h ago',
|
||||
'common.relative.daysAgoShort': '{count}d ago',
|
||||
'common.relative.weeksAgoShort': '{count}w ago',
|
||||
'common.relative.yearsAgoShort': '{count}y ago',
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
'quota.window.5h': '5-Hour',
|
||||
'quota.window.7d': '7-Day Limit',
|
||||
'quota.window.7dSonnet': '7-Day Sonnet Limit',
|
||||
'quota.window.7dOpus': '7-Day Opus Limit',
|
||||
'quota.window.weekly': 'Weekly Limit',
|
||||
'quota.window.daily': 'Daily',
|
||||
'quota.window.monthly': 'Monthly Limit',
|
||||
'quota.window.credits': 'Credits',
|
||||
'quota.window.creditsBalance': 'Credits Balance',
|
||||
'quota.window.session': 'Session',
|
||||
'quota.window.premium': 'Premium Interactions',
|
||||
'quota.window.chat': 'Chat Requests',
|
||||
'quota.window.completions': 'Completions',
|
||||
'quota.window.premiumInteractions': 'Premium interactions',
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
export type Locale = 'en' | 'zh-CN' | 'zh-TW' | 'uk' | 'es' | 'pt-BR' | 'ko' | 'pl';
|
||||
export type Locale = 'en' | 'fr' | 'zh-CN' | 'zh-TW' | 'uk' | 'es' | 'pt-BR' | 'ko' | 'pl';
|
||||
|
||||
export const LOCALES = ['en', 'zh-CN', 'zh-TW', 'uk', 'es', 'pt-BR', 'ko', 'pl'] as const satisfies readonly Locale[];
|
||||
export const LOCALES = ['en', 'fr', 'zh-CN', 'zh-TW', 'uk', 'es', 'pt-BR', 'ko', 'pl'] as const satisfies readonly Locale[];
|
||||
|
||||
export const DEFAULT_LOCALE: Locale = 'en';
|
||||
|
||||
export const LOCALE_LABEL_KEYS: Record<Locale, 'common.language.english' | 'common.language.simplifiedChinese' | 'common.language.traditionalChinese' | 'common.language.ukrainian' | 'common.language.spanish' | 'common.language.brazilianPortuguese' | 'common.language.korean' | 'common.language.polish'> = {
|
||||
export const LOCALE_LABEL_KEYS: Record<Locale, 'common.language.english' | 'common.language.french' | 'common.language.simplifiedChinese' | 'common.language.traditionalChinese' | 'common.language.ukrainian' | 'common.language.spanish' | 'common.language.brazilianPortuguese' | 'common.language.korean' | 'common.language.polish'> = {
|
||||
en: 'common.language.english',
|
||||
fr: 'common.language.french',
|
||||
'zh-CN': 'common.language.simplifiedChinese',
|
||||
'zh-TW': 'common.language.traditionalChinese',
|
||||
uk: 'common.language.ukrainian',
|
||||
@@ -39,6 +40,9 @@ export function normalizeLocale(value: string | undefined | null): Locale {
|
||||
if (normalized.startsWith('en')) {
|
||||
return 'en';
|
||||
}
|
||||
if (normalized === 'fr' || normalized.startsWith('fr-')) {
|
||||
return 'fr';
|
||||
}
|
||||
if (normalized === 'uk' || normalized.startsWith('uk-') || normalized === 'ua' || normalized.startsWith('ua-')) {
|
||||
return 'uk';
|
||||
}
|
||||
|
||||
@@ -42,4 +42,16 @@ describe('i18n store', () => {
|
||||
resetStore();
|
||||
}
|
||||
});
|
||||
|
||||
test('loads the french dictionary', async () => {
|
||||
try {
|
||||
useI18nStore.getState().setLocale('fr');
|
||||
|
||||
expect(useI18nStore.getState().loadingLocale).toBe('fr');
|
||||
await waitForLocaleLoadToSettle('fr');
|
||||
expect(useI18nStore.getState().dictionary['common.language.french']).toBe('Français');
|
||||
} finally {
|
||||
resetStore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,8 @@ async function loadDictionary(locale: Locale): Promise<I18nDictionary> {
|
||||
|
||||
const mod = locale === 'zh-CN'
|
||||
? await import('./messages/zh-CN') as { dict: I18nDictionary }
|
||||
: locale === 'fr'
|
||||
? await import('./messages/fr') as { dict: I18nDictionary }
|
||||
: locale === 'zh-TW'
|
||||
? await import('./messages/zh-TW') as { dict: I18nDictionary }
|
||||
: locale === 'es'
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { formatMessage, useI18nStore } from '@/lib/i18n/store';
|
||||
import { formatDateTimeForPreference, formatTimeForPreference } from '@/lib/timeFormat';
|
||||
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
||||
|
||||
const t = (key: Parameters<typeof formatMessage>[1]) => formatMessage(useI18nStore.getState().dictionary, key);
|
||||
|
||||
export const clampPercent = (value: number | null): number | null => {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return null;
|
||||
@@ -70,20 +73,20 @@ export const resolveUsageTone = (percent: number | null): 'safe' | 'warn' | 'cri
|
||||
};
|
||||
|
||||
export const formatWindowLabel = (label: string): string => {
|
||||
if (label === '5h') return '5-Hour';
|
||||
if (label === '7d') return '7-Day Limit';
|
||||
if (label === '7d-sonnet') return '7-Day Sonnet Limit';
|
||||
if (label === '7d-opus') return '7-Day Opus Limit';
|
||||
if (label === 'weekly') return 'Weekly Limit';
|
||||
if (label === 'daily') return 'Daily';
|
||||
if (label === 'monthly') return 'Monthly Limit';
|
||||
if (label === 'credits') return 'Credits';
|
||||
if (label === 'credits_balance') return 'Credits Balance';
|
||||
if (label === 'session') return 'Session';
|
||||
if (label === 'premium') return 'Premium Interactions';
|
||||
if (label === 'chat') return 'Chat Requests';
|
||||
if (label === 'completions') return 'Completions';
|
||||
if (label === 'premium_interactions') return 'Premium interactions';
|
||||
if (label === '5h') return t('quota.window.5h');
|
||||
if (label === '7d') return t('quota.window.7d');
|
||||
if (label === '7d-sonnet') return t('quota.window.7dSonnet');
|
||||
if (label === '7d-opus') return t('quota.window.7dOpus');
|
||||
if (label === 'weekly') return t('quota.window.weekly');
|
||||
if (label === 'daily') return t('quota.window.daily');
|
||||
if (label === 'monthly') return t('quota.window.monthly');
|
||||
if (label === 'credits') return t('quota.window.credits');
|
||||
if (label === 'credits_balance') return t('quota.window.creditsBalance');
|
||||
if (label === 'session') return t('quota.window.session');
|
||||
if (label === 'premium') return t('quota.window.premium');
|
||||
if (label === 'chat') return t('quota.window.chat');
|
||||
if (label === 'completions') return t('quota.window.completions');
|
||||
if (label === 'premium_interactions') return t('quota.window.premiumInteractions');
|
||||
return label;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getCurrentIntlLocale } from '@/lib/i18n';
|
||||
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
||||
|
||||
type TimePrecision = 'minute' | 'second';
|
||||
@@ -33,7 +34,7 @@ export const formatTimeForPreference = (
|
||||
return options.fallback ?? '';
|
||||
}
|
||||
|
||||
return date.toLocaleTimeString(undefined, {
|
||||
return date.toLocaleTimeString(getCurrentIntlLocale(), {
|
||||
hour: options.hour ?? 'numeric',
|
||||
minute: '2-digit',
|
||||
second: options.precision === 'second' ? '2-digit' : undefined,
|
||||
@@ -51,7 +52,7 @@ export const formatDateTimeForPreference = (
|
||||
return '';
|
||||
}
|
||||
|
||||
return date.toLocaleString(undefined, {
|
||||
return date.toLocaleString(getCurrentIntlLocale(), {
|
||||
...options,
|
||||
hour12: options.hour ? getHour12Option(preference) : options.hour12,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"OpenChamber: Failed to open sidebar - {0}": "OpenChamber : impossible d’ouvrir la barre latérale — {0}",
|
||||
"OpenChamber: Chat sidebar is not ready": "OpenChamber : la barre latérale du chat n’est pas prête",
|
||||
"OpenChamber: No active session": "OpenChamber : aucune session active",
|
||||
"OpenChamber: API connection restarted": "OpenChamber : la connexion API a été redémarrée",
|
||||
"OpenChamber: Failed to restart API - {0}": "OpenChamber : impossible de redémarrer l’API — {0}",
|
||||
"OpenChamber [Add to Context]: No active editor": "OpenChamber [Ajouter au contexte] : aucun éditeur actif",
|
||||
"OpenChamber [Add to Context]: No text selected": "OpenChamber [Ajouter au contexte] : aucun texte sélectionné",
|
||||
"OpenChamber: No file selected to mention": "OpenChamber : aucun fichier sélectionné à mentionner",
|
||||
"OpenChamber: Some selected entries were skipped (folders or unsupported resources)": "OpenChamber : certaines entrées sélectionnées ont été ignorées (dossiers ou ressources non prises en charge)",
|
||||
"OpenChamber [Explain]: No active editor": "OpenChamber [Expliquer] : aucun éditeur actif",
|
||||
"Explain the following Code / Text:": "Expliquez le code / texte suivant :",
|
||||
"OpenChamber [Improve Code]: No active editor": "OpenChamber [Améliorer le code] : aucun éditeur actif",
|
||||
"OpenChamber [Improve Code]: No text selected": "OpenChamber [Améliorer le code] : aucun texte sélectionné",
|
||||
"Improve the following Code:": "Améliorez le code suivant :",
|
||||
"OpenCode CLI not found. Install it and ensure it's in PATH.": "OpenCode CLI introuvable. Installez-le et assurez-vous qu’il est dans le PATH.",
|
||||
"OpenCode CLI not found. Please install it and ensure it's in PATH.": "OpenCode CLI introuvable. Veuillez l’installer et vérifier qu’il est dans le PATH.",
|
||||
"More Info": "Plus d’informations",
|
||||
"Failed to start OpenCode: {0}": "Impossible de démarrer OpenCode : {0}",
|
||||
"New Session": "Nouvelle session",
|
||||
"Session": "Session",
|
||||
"Agent Manager": "Gestionnaire d’agents"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"OpenChamber: Failed to open sidebar - {0}": "OpenChamber: Failed to open sidebar - {0}",
|
||||
"OpenChamber: Chat sidebar is not ready": "OpenChamber: Chat sidebar is not ready",
|
||||
"OpenChamber: No active session": "OpenChamber: No active session",
|
||||
"OpenChamber: API connection restarted": "OpenChamber: API connection restarted",
|
||||
"OpenChamber: Failed to restart API - {0}": "OpenChamber: Failed to restart API - {0}",
|
||||
"OpenChamber [Add to Context]: No active editor": "OpenChamber [Add to Context]: No active editor",
|
||||
"OpenChamber [Add to Context]: No text selected": "OpenChamber [Add to Context]: No text selected",
|
||||
"OpenChamber: No file selected to mention": "OpenChamber: No file selected to mention",
|
||||
"OpenChamber: Some selected entries were skipped (folders or unsupported resources)": "OpenChamber: Some selected entries were skipped (folders or unsupported resources)",
|
||||
"OpenChamber [Explain]: No active editor": "OpenChamber [Explain]: No active editor",
|
||||
"Explain the following Code / Text:": "Explain the following Code / Text:",
|
||||
"OpenChamber [Improve Code]: No active editor": "OpenChamber [Improve Code]: No active editor",
|
||||
"OpenChamber [Improve Code]: No text selected": "OpenChamber [Improve Code]: No text selected",
|
||||
"Improve the following Code:": "Improve the following Code:",
|
||||
"OpenCode CLI not found. Install it and ensure it's in PATH.": "OpenCode CLI not found. Install it and ensure it's in PATH.",
|
||||
"OpenCode CLI not found. Please install it and ensure it's in PATH.": "OpenCode CLI not found. Please install it and ensure it's in PATH.",
|
||||
"More Info": "More Info",
|
||||
"Failed to start OpenCode: {0}": "Failed to start OpenCode: {0}",
|
||||
"New Session": "New Session",
|
||||
"Session": "Session",
|
||||
"Agent Manager": "Agent Manager"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "openchamber",
|
||||
"displayName": "OpenChamber",
|
||||
"description": "AI coding assistant powered by OpenCode",
|
||||
"description": "%extension.description%",
|
||||
"version": "1.12.3",
|
||||
"publisher": "fedaykindev",
|
||||
"private": true,
|
||||
@@ -33,6 +33,7 @@
|
||||
],
|
||||
"icon": "assets/app-icon.png",
|
||||
"main": "./dist/extension.js",
|
||||
"l10n": "./l10n",
|
||||
"activationEvents": [
|
||||
"onCommand:openchamber.openSidebar",
|
||||
"onCommand:openchamber.attachExplorerToChat",
|
||||
@@ -53,7 +54,7 @@
|
||||
{
|
||||
"type": "webview",
|
||||
"id": "openchamber.chatView",
|
||||
"name": "Chat"
|
||||
"name": "%views.chat.name%"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -61,7 +62,7 @@
|
||||
{
|
||||
"command": "openchamber.openSidebar",
|
||||
"category": "OpenChamber",
|
||||
"title": "Open Sidebar",
|
||||
"title": "%command.openSidebar.title%",
|
||||
"icon": {
|
||||
"light": "assets/icon.svg",
|
||||
"dark": "assets/icon-titlebar.svg"
|
||||
@@ -70,34 +71,34 @@
|
||||
{
|
||||
"command": "openchamber.focusChat",
|
||||
"category": "OpenChamber",
|
||||
"title": "Focus Chat"
|
||||
"title": "%command.focusChat.title%"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.restartApi",
|
||||
"category": "OpenChamber",
|
||||
"title": "Restart API Connection"
|
||||
"title": "%command.restartApi.title%"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.showOpenCodeStatus",
|
||||
"category": "OpenChamber",
|
||||
"title": "Show OpenCode Status"
|
||||
"title": "%command.showOpenCodeStatus.title%"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.openAgentManager",
|
||||
"category": "OpenChamber",
|
||||
"title": "Open Agent Manager",
|
||||
"title": "%command.openAgentManager.title%",
|
||||
"icon": "$(circuit-board)"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.openActiveSessionInEditor",
|
||||
"category": "OpenChamber",
|
||||
"title": "Open Active Session in Editor",
|
||||
"title": "%command.openActiveSessionInEditor.title%",
|
||||
"icon": "$(link-external)"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.openNewSessionInEditor",
|
||||
"category": "OpenChamber",
|
||||
"title": "Open New Session in Editor",
|
||||
"title": "%command.openNewSessionInEditor.title%",
|
||||
"icon": {
|
||||
"light": "assets/icon.svg",
|
||||
"dark": "assets/icon-titlebar.svg"
|
||||
@@ -106,40 +107,40 @@
|
||||
{
|
||||
"command": "openchamber.openCurrentOrNewSessionInEditor",
|
||||
"category": "OpenChamber",
|
||||
"title": "Open Session in Editor",
|
||||
"title": "%command.openCurrentOrNewSessionInEditor.title%",
|
||||
"icon": "$(link-external)"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.addToContext",
|
||||
"category": "OpenChamber",
|
||||
"title": "Add to Context"
|
||||
"title": "%command.addToContext.title%"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.explain",
|
||||
"category": "OpenChamber",
|
||||
"title": "Explain"
|
||||
"title": "%command.explain.title%"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.improveCode",
|
||||
"category": "OpenChamber",
|
||||
"title": "Improve Code"
|
||||
"title": "%command.improveCode.title%"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.newSession",
|
||||
"category": "OpenChamber",
|
||||
"title": "New Session",
|
||||
"title": "%command.newSession.title%",
|
||||
"icon": "$(add)"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.showSettings",
|
||||
"category": "OpenChamber",
|
||||
"title": "Settings",
|
||||
"title": "%command.showSettings.title%",
|
||||
"icon": "$(settings-gear)"
|
||||
},
|
||||
{
|
||||
"command": "openchamber.attachExplorerToChat",
|
||||
"category": "OpenChamber",
|
||||
"title": "Attach to OpenChamber Chat"
|
||||
"title": "%command.attachExplorerToChat.title%"
|
||||
}
|
||||
],
|
||||
"submenus": [
|
||||
@@ -208,12 +209,12 @@
|
||||
"openchamber.apiUrl": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "URL of an external OpenCode API server. Leave empty to auto-start a local instance."
|
||||
"description": "%configuration.apiUrl.description%"
|
||||
},
|
||||
"openchamber.opencodeBinary": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Optional absolute path to the opencode CLI binary. Useful if PATH lookup fails. Requires window reload or API restart to apply."
|
||||
"description": "%configuration.opencodeBinary.description%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extension.description": "Assistant de codage IA propulsé par OpenCode",
|
||||
"views.chat.name": "Chat",
|
||||
"command.openSidebar.title": "Ouvrir la barre latérale",
|
||||
"command.focusChat.title": "Mettre le chat au premier plan",
|
||||
"command.restartApi.title": "Redémarrer la connexion API",
|
||||
"command.showOpenCodeStatus.title": "Afficher l’état de OpenCode",
|
||||
"command.openAgentManager.title": "Ouvrir le gestionnaire d’agents",
|
||||
"command.openActiveSessionInEditor.title": "Ouvrir la session active dans l’éditeur",
|
||||
"command.openNewSessionInEditor.title": "Ouvrir une nouvelle session dans l’éditeur",
|
||||
"command.openCurrentOrNewSessionInEditor.title": "Ouvrir une session dans l’éditeur",
|
||||
"command.addToContext.title": "Ajouter au contexte",
|
||||
"command.explain.title": "Expliquer",
|
||||
"command.improveCode.title": "Améliorer le code",
|
||||
"command.newSession.title": "Nouvelle session",
|
||||
"command.showSettings.title": "Paramètres",
|
||||
"command.attachExplorerToChat.title": "Joindre au chat OpenChamber",
|
||||
"configuration.apiUrl.description": "URL d’un serveur API OpenCode externe. Laissez vide pour démarrer automatiquement une instance locale.",
|
||||
"configuration.opencodeBinary.description": "Chemin absolu facultatif vers le binaire CLI opencode. Utile si la recherche PATH échoue. Nécessite de recharger la fenêtre ou de redémarrer l’API pour être pris en compte."
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extension.description": "AI coding assistant powered by OpenCode",
|
||||
"views.chat.name": "Chat",
|
||||
"command.openSidebar.title": "Open Sidebar",
|
||||
"command.focusChat.title": "Focus Chat",
|
||||
"command.restartApi.title": "Restart API Connection",
|
||||
"command.showOpenCodeStatus.title": "Show OpenCode Status",
|
||||
"command.openAgentManager.title": "Open Agent Manager",
|
||||
"command.openActiveSessionInEditor.title": "Open Active Session in Editor",
|
||||
"command.openNewSessionInEditor.title": "Open New Session in Editor",
|
||||
"command.openCurrentOrNewSessionInEditor.title": "Open Session in Editor",
|
||||
"command.addToContext.title": "Add to Context",
|
||||
"command.explain.title": "Explain",
|
||||
"command.improveCode.title": "Improve Code",
|
||||
"command.newSession.title": "New Session",
|
||||
"command.showSettings.title": "Settings",
|
||||
"command.attachExplorerToChat.title": "Attach to OpenChamber Chat",
|
||||
"configuration.apiUrl.description": "URL of an external OpenCode API server. Leave empty to auto-start a local instance.",
|
||||
"configuration.opencodeBinary.description": "Optional absolute path to the opencode CLI binary. Useful if PATH lookup fails. Requires window reload or API restart to apply."
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { openSseProxy } from './sseProxy';
|
||||
import { resolveWebviewDevServerUrl } from './webviewDevServer';
|
||||
import { normalizeWindowsDriveLetter } from './pathUtils';
|
||||
|
||||
const t = vscode.l10n.t;
|
||||
|
||||
export class AgentManagerPanelProvider {
|
||||
public static readonly viewType = 'openchamber.agentManager';
|
||||
|
||||
@@ -40,7 +42,7 @@ export class AgentManagerPanelProvider {
|
||||
// Create new panel
|
||||
this._panel = vscode.window.createWebviewPanel(
|
||||
AgentManagerPanelProvider.viewType,
|
||||
'Agent Manager',
|
||||
t('Agent Manager'),
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
enableScripts: true,
|
||||
|
||||
@@ -8,6 +8,8 @@ import { openSseProxy } from './sseProxy';
|
||||
import { resolveWebviewDevServerUrl } from './webviewDevServer';
|
||||
import { normalizeWindowsDriveLetter } from './pathUtils';
|
||||
|
||||
const t = vscode.l10n.t;
|
||||
|
||||
type SessionPanelState = {
|
||||
panel: vscode.WebviewPanel;
|
||||
sseStreams: Map<string, AbortController>;
|
||||
@@ -62,7 +64,7 @@ export class SessionEditorPanelProvider {
|
||||
public createOrShowNewSession(): void {
|
||||
// Generate unique panel ID for new session drafts
|
||||
const panelId = `new_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
this._createPanel(panelId, 'New Session', null);
|
||||
this._createPanel(panelId, t('New Session'), null);
|
||||
}
|
||||
|
||||
public createOrShow(sessionId: string, title?: string): void {
|
||||
@@ -70,7 +72,7 @@ export class SessionEditorPanelProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionTitle = title && title.trim().length > 0 ? title.trim() : 'Session';
|
||||
const sessionTitle = title && title.trim().length > 0 ? title.trim() : t('Session');
|
||||
|
||||
const existing = this._panels.get(sessionId);
|
||||
if (existing) {
|
||||
@@ -134,7 +136,7 @@ export class SessionEditorPanelProvider {
|
||||
if (message.type === 'vscode:command') {
|
||||
const { command, args } = (message.payload || {}) as { command?: unknown; args?: unknown[] };
|
||||
if (command === 'openchamber.updateSessionEditorTitle') {
|
||||
const title = typeof args?.[1] === 'string' && args[1].trim().length > 0 ? args[1].trim() : 'Session';
|
||||
const title = typeof args?.[1] === 'string' && args[1].trim().length > 0 ? args[1].trim() : t('Session');
|
||||
state.panel.title = title;
|
||||
state.panel.webview.postMessage({ id: message.id, type: message.type, success: true, data: { result: true } });
|
||||
return;
|
||||
|
||||
@@ -14,6 +14,8 @@ let outputChannel: vscode.OutputChannel | undefined;
|
||||
let activeSessionId: string | null = null;
|
||||
let activeSessionTitle: string | null = null;
|
||||
|
||||
const t = vscode.l10n.t;
|
||||
|
||||
const SETTINGS_KEY = 'openchamber.settings';
|
||||
const CHAT_VIEW_BOOTSTRAP_DELAY_MS = 80;
|
||||
|
||||
@@ -147,13 +149,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
await vscode.commands.executeCommand('openchamber.chatView.focus');
|
||||
} catch (e) {
|
||||
outputChannel?.appendLine(`[OpenChamber] openchamber.chatView.focus failed: ${e}`);
|
||||
vscode.window.showErrorMessage(`OpenChamber: Failed to open sidebar - ${e}`);
|
||||
vscode.window.showErrorMessage(t('OpenChamber: Failed to open sidebar - {0}', String(e)));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!chatViewProvider?.hasResolvedView()) {
|
||||
outputChannel?.appendLine('[OpenChamber] Chat sidebar focus completed before the webview was resolved');
|
||||
vscode.window.showWarningMessage('OpenChamber: Chat sidebar is not ready');
|
||||
vscode.window.showWarningMessage(t('OpenChamber: Chat sidebar is not ready'));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -170,7 +172,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
await waitForChatViewBootstrap();
|
||||
if (!chatViewProvider?.hasResolvedView()) {
|
||||
outputChannel?.appendLine('[OpenChamber] Chat sidebar webview was disposed before payload delivery');
|
||||
vscode.window.showWarningMessage('OpenChamber: Chat sidebar is not ready');
|
||||
vscode.window.showWarningMessage(t('OpenChamber: Chat sidebar is not ready'));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -227,7 +229,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('openchamber.openActiveSessionInEditor', () => {
|
||||
if (!activeSessionId) {
|
||||
vscode.window.showInformationMessage('OpenChamber: No active session');
|
||||
vscode.window.showInformationMessage(t('OpenChamber: No active session'));
|
||||
return;
|
||||
}
|
||||
sessionEditorProvider?.createOrShow(activeSessionId, activeSessionTitle ?? undefined);
|
||||
@@ -270,9 +272,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
return;
|
||||
}
|
||||
await openCodeManager?.restart();
|
||||
vscode.window.showInformationMessage('OpenChamber: API connection restarted');
|
||||
vscode.window.showInformationMessage(t('OpenChamber: API connection restarted'));
|
||||
} catch (e) {
|
||||
vscode.window.showErrorMessage(`OpenChamber: Failed to restart API - ${e}`);
|
||||
vscode.window.showErrorMessage(t('OpenChamber: Failed to restart API - {0}', String(e)));
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -281,7 +283,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand('openchamber.addToContext', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
vscode.window.showWarningMessage('OpenChamber [Add to Context]:No active editor');
|
||||
vscode.window.showWarningMessage(t('OpenChamber [Add to Context]: No active editor'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -289,7 +291,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const selectedText = editor.document.getText(selection);
|
||||
|
||||
if (!selectedText) {
|
||||
vscode.window.showWarningMessage('OpenChamber [Add to Context]: No text selected');
|
||||
vscode.window.showWarningMessage(t('OpenChamber [Add to Context]: No text selected'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -370,7 +372,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
if (attachedFiles.length === 0) {
|
||||
vscode.window.showWarningMessage('OpenChamber: No file selected to mention');
|
||||
vscode.window.showWarningMessage(t('OpenChamber: No file selected to mention'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,7 +384,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
if (skippedEntries.length > 0) {
|
||||
vscode.window.showInformationMessage('OpenChamber: Some selected entries were skipped (folders or unsupported resources)');
|
||||
vscode.window.showInformationMessage(t('OpenChamber: Some selected entries were skipped (folders or unsupported resources)'));
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -391,7 +393,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand('openchamber.explain', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
vscode.window.showWarningMessage('OpenChamber [Explain]: No active editor');
|
||||
vscode.window.showWarningMessage(t('OpenChamber [Explain]: No active editor'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -407,10 +409,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const startLine = selection.start.line + 1;
|
||||
const endLine = selection.end.line + 1;
|
||||
const lineRange = startLine === endLine ? `${startLine}` : `${startLine}-${endLine}`;
|
||||
prompt = `Explain the following Code / Text:\n\n${filePath}:${lineRange}\n\`\`\`${languageId}\n${selectedText}\n\`\`\``;
|
||||
prompt = `${t('Explain the following Code / Text:')}\n\n${filePath}:${lineRange}\n\`\`\`${languageId}\n${selectedText}\n\`\`\``;
|
||||
} else {
|
||||
// No selection - explain the entire file
|
||||
prompt = `Explain the following Code / Text:\n\n${filePath}`;
|
||||
prompt = `${t('Explain the following Code / Text:')}\n\n${filePath}`;
|
||||
}
|
||||
|
||||
if (!sessionEditorProvider?.createSessionWithPromptInActivePanel(prompt)) {
|
||||
@@ -426,7 +428,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand('openchamber.improveCode', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
vscode.window.showWarningMessage('OpenChamber [Improve Code]: No active editor');
|
||||
vscode.window.showWarningMessage(t('OpenChamber [Improve Code]: No active editor'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -434,7 +436,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const selectedText = editor.document.getText(selection);
|
||||
|
||||
if (!selectedText) {
|
||||
vscode.window.showWarningMessage('OpenChamber [Improve Code]: No text selected');
|
||||
vscode.window.showWarningMessage(t('OpenChamber [Improve Code]: No text selected'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -444,7 +446,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const endLine = selection.end.line + 1;
|
||||
const lineRange = startLine === endLine ? `${startLine}` : `${startLine}-${endLine}`;
|
||||
|
||||
const prompt = `Improve the following Code:\n\n${filePath}:${lineRange}\n\`\`\`${languageId}\n${selectedText}\n\`\`\``;
|
||||
const prompt = `${t('Improve the following Code:')}\n\n${filePath}:${lineRange}\n\`\`\`${languageId}\n${selectedText}\n\`\`\``;
|
||||
|
||||
if (!sessionEditorProvider?.createSessionWithPromptInActivePanel(prompt)) {
|
||||
if (!(await revealChatViewForPayload())) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import { spawn } from 'child_process';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { normalizeWindowsDriveLetter } from './pathUtils';
|
||||
|
||||
const t = vscode.l10n.t;
|
||||
|
||||
const READY_CHECK_TIMEOUT_MS = 30000;
|
||||
const WINDOWS_EXECUTABLE_EXTENSIONS = (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
||||
.split(';')
|
||||
@@ -931,17 +933,18 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
||||
if (!cliPath) {
|
||||
cliPath = resolveOpencodeCliPath();
|
||||
}
|
||||
setStatus('error', 'OpenCode CLI not found. Install it and ensure it\'s in PATH.');
|
||||
const moreInfoLabel = t('More Info');
|
||||
setStatus('error', t('OpenCode CLI not found. Install it and ensure it\'s in PATH.'));
|
||||
vscode.window.showErrorMessage(
|
||||
'OpenCode CLI not found. Please install it and ensure it\'s in PATH.',
|
||||
'More Info'
|
||||
t('OpenCode CLI not found. Please install it and ensure it\'s in PATH.'),
|
||||
moreInfoLabel
|
||||
).then(selection => {
|
||||
if (selection === 'More Info') {
|
||||
if (selection === moreInfoLabel) {
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://github.com/anomalyco/opencode'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setStatus('error', `Failed to start OpenCode: ${message}`);
|
||||
setStatus('error', t('Failed to start OpenCode: {0}', message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
||||
</svg>
|
||||
<!-- Status text stays empty while things are fine; populated only on error. -->
|
||||
<div class="status-text" id="loading-status"></div>
|
||||
${!cliAvailable ? `<div class="error-text">OpenCode CLI not found. Please install it first.</div>` : ''}
|
||||
${!cliAvailable ? `<div class="error-text" id="cli-missing-text">OpenCode CLI not found. Please install it first.</div>` : ''}
|
||||
</div>
|
||||
|
||||
<div id="root"></div>
|
||||
@@ -188,16 +188,62 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
||||
};
|
||||
window.__OPENCHAMBER_HOME__ = "${workspaceFolder.replace(/\\/g, '\\\\')}";
|
||||
|
||||
function getBootstrapMessages() {
|
||||
var locale = 'en';
|
||||
try {
|
||||
var rawLocale = window.localStorage.getItem('openchamber.i18n.v1');
|
||||
if (rawLocale) {
|
||||
var parsedLocale = JSON.parse(rawLocale);
|
||||
if (parsedLocale && typeof parsedLocale.locale === 'string' && parsedLocale.locale.toLowerCase().indexOf('fr') === 0) {
|
||||
locale = 'fr';
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return locale === 'fr'
|
||||
? {
|
||||
startingApi: 'Démarrage de l’API OpenCode…',
|
||||
initializing: 'Initialisation…',
|
||||
connecting: 'Connexion…',
|
||||
connected: 'Connecté !',
|
||||
connectionError: 'Erreur de connexion',
|
||||
reconnecting: 'Reconnexion…',
|
||||
cliNotFound: 'L’interface en ligne de commande OpenCode est introuvable. Veuillez l’installer d’abord.',
|
||||
}
|
||||
: {
|
||||
startingApi: 'Starting OpenCode API…',
|
||||
initializing: 'Initializing…',
|
||||
connecting: 'Connecting…',
|
||||
connected: 'Connected!',
|
||||
connectionError: 'Connection error',
|
||||
reconnecting: 'Reconnecting…',
|
||||
cliNotFound: 'OpenCode CLI not found. Please install it first.',
|
||||
};
|
||||
}
|
||||
|
||||
(function applyBootstrapLocale() {
|
||||
var statusEl = document.getElementById('loading-status');
|
||||
var cliMissingEl = document.getElementById('cli-missing-text');
|
||||
var messages = getBootstrapMessages();
|
||||
if (cliMissingEl) {
|
||||
cliMissingEl.textContent = messages.cliNotFound;
|
||||
}
|
||||
if (statusEl) {
|
||||
statusEl.textContent = '';
|
||||
}
|
||||
})();
|
||||
|
||||
// Handle connection status updates to update loading screen
|
||||
window.addEventListener('message', function(event) {
|
||||
var msg = event.data;
|
||||
if (msg && msg.type === 'connectionStatus') {
|
||||
var messages = getBootstrapMessages();
|
||||
var statusEl = document.getElementById('loading-status');
|
||||
if (statusEl) {
|
||||
// Only show text when something is wrong — progress states stay silent
|
||||
// (the animated logo already signals "working").
|
||||
if (msg.status === 'error') {
|
||||
statusEl.textContent = msg.error || 'Connection error';
|
||||
statusEl.textContent = msg.error || messages.connectionError;
|
||||
statusEl.classList.add('error-text');
|
||||
} else {
|
||||
statusEl.textContent = '';
|
||||
@@ -224,6 +270,24 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
||||
const baseUrl = devServerUrl;
|
||||
|
||||
const statusEl = document.getElementById('loading-status');
|
||||
const getDevMessages = () => {
|
||||
try {
|
||||
const rawLocale = window.localStorage.getItem('openchamber.i18n.v1');
|
||||
if (rawLocale) {
|
||||
const parsedLocale = JSON.parse(rawLocale);
|
||||
if (parsedLocale && typeof parsedLocale.locale === 'string' && parsedLocale.locale.toLowerCase().indexOf('fr') === 0) {
|
||||
return {
|
||||
startingDevServer: (host) => 'Démarrage du serveur de développement de la webview (' + host + ')...',
|
||||
waitingDevServer: (host, attempt) => 'En attente du serveur de développement de la webview (' + host + ')... tentative ' + attempt,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return {
|
||||
startingDevServer: (host) => 'Starting webview dev server (' + host + ')...',
|
||||
waitingDevServer: (host, attempt) => 'Waiting for webview dev server (' + host + ')... attempt ' + attempt,
|
||||
};
|
||||
};
|
||||
const setStatus = (text) => {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = text;
|
||||
@@ -272,7 +336,8 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
||||
}
|
||||
})();
|
||||
|
||||
setStatus('Starting webview dev server (' + hostLabel + ')...');
|
||||
const devMessages = getDevMessages();
|
||||
setStatus(devMessages.startingDevServer(hostLabel));
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => import(viteClientUrl))
|
||||
@@ -296,7 +361,7 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
||||
.catch((error) => {
|
||||
attempt += 1;
|
||||
console.warn('[OpenChamber] VS Code webview dev bundle unavailable, retrying...', error);
|
||||
setStatus('Waiting for webview dev server (' + hostLabel + ')... attempt ' + attempt);
|
||||
setStatus(devMessages.waitingDevServer(hostLabel, attempt));
|
||||
window.setTimeout(() => {
|
||||
tryLoadDevBundle();
|
||||
}, retryDelayMs);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type VSCodeThemeKind,
|
||||
type VSCodeThemePayload,
|
||||
} from '@openchamber/ui/lib/theme/vscode/adapter';
|
||||
import { getBootstrapMessages, readStoredLocaleForBootstrap } from '@openchamber/ui/lib/i18n';
|
||||
import type { VSCodeActiveEditorFile } from '@/sync/input-store';
|
||||
|
||||
type ConnectionStatus = 'connecting' | 'connected' | 'error' | 'disconnected';
|
||||
@@ -55,6 +56,9 @@ try {
|
||||
|
||||
window.__OPENCHAMBER_RUNTIME_APIS__ = createVSCodeAPIs();
|
||||
|
||||
const bootstrapLocale = readStoredLocaleForBootstrap();
|
||||
const bootstrapMessages = getBootstrapMessages(bootstrapLocale);
|
||||
|
||||
const bootstrapConnectionStatus = () => {
|
||||
const initialStatus = (window.__VSCODE_CONFIG__?.connectionStatus as ConnectionStatus | undefined) || 'connecting';
|
||||
const cliAvailable = window.__VSCODE_CONFIG__?.cliAvailable ?? true;
|
||||
@@ -173,7 +177,7 @@ const maybeHideLoadingOverlay = () => {
|
||||
|
||||
if (connectionStatus === 'connected') {
|
||||
if (bootstrapFailed) {
|
||||
setLoadingStatusText('OpenCode connected, but initial data load failed.', 'error');
|
||||
setLoadingStatusText(bootstrapMessages.initialDataLoadFailed, 'error');
|
||||
fadeOutLoadingScreen();
|
||||
return;
|
||||
}
|
||||
@@ -190,13 +194,13 @@ const maybeHideLoadingOverlay = () => {
|
||||
|
||||
if (connectionStatus === 'error') {
|
||||
const error = window.__OPENCHAMBER_CONNECTION__?.error;
|
||||
setLoadingStatusText(error || 'Connection error', 'error');
|
||||
setLoadingStatusText(error || bootstrapMessages.connectionError, 'error');
|
||||
fadeOutLoadingScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionStatus === 'disconnected') {
|
||||
setLoadingStatusText('Disconnected', 'error');
|
||||
setLoadingStatusText(bootstrapMessages.disconnected, 'error');
|
||||
fadeOutLoadingScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user