diff --git a/README.de.md b/README.de.md index fcbafb5f..e028ba4d 100644 --- a/README.de.md +++ b/README.de.md @@ -1,18 +1,18 @@
- CloudCLI UI -

Cloud CLI (auch bekannt als Claude Code UI)

-

Eine Desktop- und Mobile-Oberfläche für Claude Code, Cursor CLI, Codex und Gemini-CLI.
Lokal oder remote nutzbar – verwalte deine aktiven Projekte und Sitzungen von überall.

+ CloudCLI UI +

Cloud CLI (auch bekannt als Claude Code UI)

+

Eine Desktop- und Mobile-Oberfläche für Claude Code, Cursor CLI und Codex.
Lokal oder remote nutzbar – verwalte deine aktiven Projekte und Sitzungen von überall.

- CloudCLI Cloud · Dokumentation · Discord · Fehler melden · Mitwirken + CloudCLI Cloud · Dokumentation · Discord · Fehler melden · Mitwirken

- CloudCLI Cloud - Join Community -

- siteboon%2Fclaudecodeui | Trendshift + CloudCLI Cloud + Join Community +

+ siteboon%2Fclaudecodeui | Trendshift

English · Русский · Deutsch · 한국어 · 简体中文 · 繁體中文 · 日本語 · Türkçe
@@ -43,7 +43,7 @@

CLI-Auswahl

CLI-Auswahl
-Wähle zwischen Claude Code, Gemini, Cursor CLI und Codex +Wähle zwischen Claude Code, Cursor CLI und Codex @@ -62,7 +62,7 @@ - **Sitzungsverwaltung** – Gespräche fortsetzen, mehrere Sitzungen verwalten und Verlauf nachverfolgen - **Plugin-System** – CloudCLI mit eigenen Plugins erweitern – neue Tabs, Backend-Dienste und Integrationen hinzufügen. [Eigenes Plugin erstellen →](https://github.com/cloudcli-ai/cloudcli-plugin-starter) - **TaskMaster AI Integration** *(Optional)* – Erweitertes Projektmanagement mit KI-gestützter Aufgabenplanung, PRD-Parsing und Workflow-Automatisierung -- **Modell-Kompatibilität** – Funktioniert mit Claude, GPT und Gemini (vollständige Liste unterstützter Modelle zur Laufzeit über `GET /api/providers/:provider/models`) +- **Modell-Kompatibilität** – Funktioniert mit Claude, GPT und (vollständige Liste unterstützter Modelle zur Laufzeit über `GET /api/providers/:provider/models`) ## Schnellstart @@ -103,7 +103,7 @@ Agents in isolierten Sandboxes mit Hypervisor-Isolation ausführen. Standardmä npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project ``` -Unterstützt Claude Code, Codex und Gemini CLI. Weitere Details in der [Sandbox-Dokumentation](docker/). +Unterstützt Claude Code, Codex und. Weitere Details in der [Sandbox-Dokumentation](docker/). --- @@ -119,7 +119,7 @@ CloudCLI UI ist die Open-Source-UI-Schicht, die CloudCLI Cloud antreibt. Du kann | **Rechner muss laufen** | Ja | Nein | | **Mobiler Zugriff** | Jeder Browser im Netzwerk | Jedes Gerät, native App in Entwicklung | | **Verfügbare Sitzungen** | Alle Sitzungen automatisch aus `~/.claude` erkannt | Alle Sitzungen in deiner Cloud-Umgebung | -| **Unterstützte Agents** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI | +| **Unterstützte Agents** | Claude Code, Cursor CLI, Codex | Claude Code, Cursor CLI, Codex | | **Datei-Explorer und Git** | Ja, direkt in der UI | Ja, direkt in der UI | | **MCP-Konfiguration** | Über UI verwaltet, synchronisiert mit lokalem `~/.claude` | Über UI verwaltet | | **IDE-Zugriff** | Deine lokale IDE | Jede IDE, die mit deiner Cloud-Umgebung verbunden ist | @@ -166,7 +166,7 @@ CloudCLI verfügt über ein Plugin-System, mit dem benutzerdefinierte Tabs mit e | **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | Zeigt Dateianzahl, Codezeilen, Dateityp-Aufschlüsselung, größte Dateien und zuletzt geänderte Dateien des aktuellen Projekts | | **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | Vollwertiges xterm.js-Terminal mit Multi-Tab-Unterstützung | | **[Claude Watch](https://github.com/satsuki19980613/cloudcli-claude-watch)** | Überwacht lange laufende Claude-Code-Sitzungen auf Hänger und stellt Prozesssteuerungen bereit | -| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | Erstellt arbeitsbereichsbezogene geplante Prompts und führt sie über eine lokale CLI wie Codex, Claude Code oder Gemini CLI aus | +| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | Erstellt arbeitsbereichsbezogene geplante Prompts und führt sie über eine lokale CLI wie Codex, Claude Code oder aus | | **[PRISM CloudCLI](https://github.com/jakeefr/cloudcli-plugin-prism)** | Sitzungsintelligenz für Claude Code in CloudCLI, inklusive Sichtbarkeit des Token-Verbrauchs | | **[Sessions](https://github.com/strykereye2/cloudcli-plugin-session-manager)** | Aktive Claude-Code-Sitzungen anzeigen, verwalten und beenden | | **[Token Cost Calculator](https://github.com/NightmareAway/cloudcli-plugin-token-cost-calculator)** | API-Kosten anhand von Modellpreisen und Token-Nutzung berechnen, mit Unterstützung für Preisvorlagen | @@ -193,7 +193,7 @@ Das bedeutet in der Praxis: - **Alle Sitzungen, nicht nur eine** – CloudCLI UI erkennt automatisch jede Sitzung aus dem `~/.claude`-Ordner. Remote Control stellt nur die einzelne aktive Sitzung bereit, um sie in der Claude Mobile App verfügbar zu machen. - **Deine Einstellungen sind deine Einstellungen** – MCP-Server, Tool-Berechtigungen und Projektkonfiguration, die in CloudCLI UI geändert werden, werden direkt in die Claude Code-Konfiguration geschrieben und treten sofort in Kraft – und umgekehrt. -- **Funktioniert mit mehr Agents** – Claude Code, Cursor CLI, Codex und Gemini CLI, nicht nur Claude Code. +- **Funktioniert mit mehr Agents** – Claude Code, Cursor CLI, Codex und, nicht nur Claude Code. - **Vollständige UI, nicht nur ein Chat-Fenster** – Datei-Explorer, Git-Integration, MCP-Verwaltung und ein Shell-Terminal sind alle eingebaut. - **CloudCLI Cloud läuft in der Cloud** – Laptop zuklappen, der Agent läuft weiter. Kein Terminal zu überwachen, kein Rechner, der laufen muss. @@ -202,7 +202,7 @@ Das bedeutet in der Praxis:
Muss ich ein KI-Abonnement separat bezahlen? -Ja. CloudCLI stellt die Umgebung bereit, nicht die KI. Du bringst dein eigenes Claude-, Cursor-, Codex- oder Gemini-Abonnement mit. CloudCLI Cloud beginnt bei €7/Monat für die gehostete Umgebung zusätzlich dazu. +Ja. CloudCLI stellt die Umgebung bereit, nicht die KI. Du bringst dein eigenes Claude-, Cursor-, Codex- oder -Abonnement mit. CloudCLI Cloud beginnt bei €7/Monat für die gehostete Umgebung zusätzlich dazu.
@@ -241,7 +241,6 @@ Dieses Projekt ist Open Source und kann unter der GPL v3-Lizenz kostenlos genutz - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropics offizielle CLI - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursors offizielle CLI - **[Codex](https://developers.openai.com/codex)** - OpenAI Codex -- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI - **[React](https://react.dev/)** - UI-Bibliothek - **[Vite](https://vitejs.dev/)** - Schnelles Build-Tool und Dev-Server - **[Tailwind CSS](https://tailwindcss.com/)** - Utility-first CSS-Framework @@ -254,5 +253,5 @@ Dieses Projekt ist Open Source und kann unter der GPL v3-Lizenz kostenlos genutz ---
- Mit Sorgfalt für die Claude Code-, Cursor- und Codex-Community erstellt. + Mit Sorgfalt für die Claude Code-, Cursor- und Codex-Community erstellt.
diff --git a/README.ja.md b/README.ja.md index 8872b51e..8691e2eb 100644 --- a/README.ja.md +++ b/README.ja.md @@ -1,18 +1,18 @@
- CloudCLI UI -

Cloud CLI(別名 Claude Code UI)

-

Claude CodeCursor CLICodexGemini-CLI のためのデスクトップ/モバイル UI。
ローカルでもリモートでも使え、アクティブなプロジェクトとセッションをどこからでも閲覧できます。

+ CloudCLI UI +

Cloud CLI(別名 Claude Code UI)

+

Claude CodeCursor CLICodex、 のためのデスクトップ/モバイル UI。
ローカルでもリモートでも使え、アクティブなプロジェクトとセッションをどこからでも閲覧できます。

- CloudCLI Cloud · ドキュメント · Discord · バグ報告 · コントリビュート + CloudCLI Cloud · ドキュメント · Discord · バグ報告 · コントリビュート

- CloudCLI Cloud - Discord コミュニティに参加 -

- siteboon%2Fclaudecodeui | Trendshift + CloudCLI Cloud + Discord コミュニティに参加 +

+ siteboon%2Fclaudecodeui | Trendshift

English · Русский · Deutsch · 한국어 · 简体中文 · 繁體中文 · 日本語 · Türkçe
@@ -43,7 +43,7 @@

CLI 選択

CLI 選択
-Claude Code、Gemini、Cursor CLI、Codex から選択 +Claude Code、、Cursor CLI、Codex から選択 @@ -99,7 +99,7 @@ cloudcli npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project ``` -Claude Code、Codex、Gemini CLI に対応。詳細は[サンドボックスのドキュメント](docker/)をご覧ください。 +Claude Code、Codex、 に対応。詳細は[サンドボックスのドキュメント](docker/)をご覧ください。 --- @@ -115,7 +115,7 @@ CloudCLI UI は、CloudCLI Cloud を支えるオープンソースの UI レイ | **マシンの稼働継続** | はい | いいえ | | **モバイルアクセス** | 同一ネットワーク内の任意のブラウザ | 任意のデバイス(ネイティブアプリも準備中) | | **利用可能なセッション** | `~/.claude` から全セッションを自動検出 | クラウド環境内の全セッション | -| **対応エージェント** | Claude Code、Cursor CLI、Codex、Gemini CLI | Claude Code、Cursor CLI、Codex、Gemini CLI | +| **対応エージェント** | Claude Code、Cursor CLI、Codex、 | Claude Code、Cursor CLI、Codex、 | | **ファイルエクスプローラとGit** | はい(UI に内蔵) | はい(UI に内蔵) | | **MCP設定** | UI で管理し、ローカルの `~/.claude` 設定と同期 | UI で管理 | | **IDEアクセス** | ローカル IDE | クラウド環境に接続された任意の IDE | @@ -160,7 +160,7 @@ CloudCLI にはプラグインシステムがあり、独自のフロントエ | **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | 現在のプロジェクトについて、ファイル数、コード行数、ファイル種別の内訳、最大ファイル、最近変更されたファイルを表示 | | **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | 複数タブに対応した本格的な xterm.js ターミナル | | **[Claude Watch](https://github.com/satsuki19980613/cloudcli-claude-watch)** | 長時間実行中の Claude Code セッションのハングを監視し、プロセス操作を提供 | -| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | ワークスペース単位のスケジュール済みプロンプトを作成し、Codex、Claude Code、Gemini CLI などのローカル CLI で実行 | +| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | ワークスペース単位のスケジュール済みプロンプトを作成し、Codex、Claude Code、 などのローカル CLI で実行 | | **[PRISM CloudCLI](https://github.com/jakeefr/cloudcli-plugin-prism)** | CloudCLI 内で Claude Code のセッション分析を行い、トークン消費の可視化も提供 | | **[Sessions](https://github.com/strykereye2/cloudcli-plugin-session-manager)** | アクティブな Claude Code セッションを表示、管理、終了 | | **[Token Cost Calculator](https://github.com/NightmareAway/cloudcli-plugin-token-cost-calculator)** | モデル価格とトークン使用量から API コストを計算し、モデル価格プリセットにも対応 | @@ -185,7 +185,7 @@ CloudCLI UI と CloudCLI Cloud は、Claude Code の横に別物として存在 - **すべてのセッションにアクセス** — CloudCLI UI は `~/.claude` フォルダのすべてのセッションを自動検出します。Remote Control は、Claude モバイルアプリで利用可能にするため、1つのアクティブセッションだけを公開します。 - **設定はあなたの設定** — CloudCLI UI で変更した MCP サーバー、ツール権限、プロジェクト構成は、Claude Code の設定に直接書き込まれて即座に反映され、その逆(Claude Code での変更が UI に反映)も同様です。 -- **対応エージェントがさらに充実** — Claude Code に加えて Cursor CLI、Codex、Gemini CLI にも対応しています。 +- **対応エージェントがさらに充実** — Claude Code に加えて Cursor CLI、Codex、 にも対応しています。 - **チャット窓だけではない完全な UI** — ファイルエクスプローラー、Git 統合、MCP 管理、シェル端末などがすべて組み込まれています。 - **CloudCLI Cloud はクラウド上で稼働** — ノートパソコンを閉じてもエージェントは動き続けます。監視が要る端末も、スリープ防止も不要です。 @@ -194,7 +194,7 @@ CloudCLI UI と CloudCLI Cloud は、Claude Code の横に別物として存在
AI のサブスクリプションは別途支払いが必要ですか? -はい。CloudCLI は環境を提供するものであり、AI は含まれません。Claude、Cursor、Codex、または Gemini のサブスクリプションはご自身でご用意ください。CloudCLI Cloud のホスティング環境はそれに加えて月額 €7 から提供されます。 +はい。CloudCLI は環境を提供するものであり、AI は含まれません。Claude、Cursor、Codex、または のサブスクリプションはご自身でご用意ください。CloudCLI Cloud のホスティング環境はそれに加えて月額 €7 から提供されます。
@@ -234,7 +234,6 @@ GNU General Public License v3.0 - 詳細は [LICENSE](LICENSE) ファイルを - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropic の公式 CLI - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursor の公式 CLI - **[Codex](https://developers.openai.com/codex)** - OpenAI Codex -- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI - **[React](https://react.dev/)** - ユーザーインターフェースライブラリ - **[Vite](https://vitejs.dev/)** - 高速ビルドツールと開発サーバー - **[Tailwind CSS](https://tailwindcss.com/)** - ユーティリティファーストの CSS フレームワーク @@ -246,5 +245,5 @@ GNU General Public License v3.0 - 詳細は [LICENSE](LICENSE) ファイルを ---
- Claude Code、Cursor、Codex コミュニティのために心を込めて作りました。 + Claude Code、Cursor、Codex コミュニティのために心を込めて作りました。
diff --git a/README.ko.md b/README.ko.md index a782efdc..cbd2317b 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,18 +1,18 @@
- CloudCLI UI -

Cloud CLI (일명 Claude Code UI)

-

Claude Code, Cursor CLI, Codex, Gemini-CLI 용 데스크톱 및 모바일 UI입니다.
로컬 또는 원격에서 실행하여 어디서나 활성 프로젝트와 세션을 확인하세요.

+ CloudCLI UI +

Cloud CLI (일명 Claude Code UI)

+

Claude Code, Cursor CLI, Codex, 용 데스크톱 및 모바일 UI입니다.
로컬 또는 원격에서 실행하여 어디서나 활성 프로젝트와 세션을 확인하세요.

- CloudCLI Cloud · 문서 · Discord · 버그 신고 · 기여 안내 + CloudCLI Cloud · 문서 · Discord · 버그 신고 · 기여 안내

- CloudCLI Cloud - Discord 커뮤니티 -

- siteboon%2Fclaudecodeui | Trendshift + CloudCLI Cloud + Discord 커뮤니티 +

+ siteboon%2Fclaudecodeui | Trendshift

English · Русский · Deutsch · 한국어 · 简体中文 · 繁體中文 · 日本語 · Türkçe
@@ -43,7 +43,7 @@

CLI 선택

CLI 선택
-Claude Code, Gemini, Cursor CLI 및 Codex 중 선택 +Claude Code, Cursor CLI 및 Codex 중 선택 @@ -60,7 +60,7 @@ - **세션 관리** - 대화를 재개하고, 여러 세션을 관리하며 기록을 추적 - **플러그인 시스템** - 커스텀 탭, 백엔드 서비스, 통합을 추가하여 CloudCLI 확장. [직접 빌드 →](https://github.com/cloudcli-ai/cloudcli-plugin-starter) - **TaskMaster AI 통합** *(선택사항)* - AI 중심의 작업 계획, PRD 파싱, 워크플로 자동화를 통한 고급 프로젝트 관리 -- **모델 호환성** - Claude, GPT, Gemini 모델 계열에서 작동 (`GET /api/providers/:provider/models` API에서 전체 지원 모델 확인) +- **모델 호환성** - Claude, GPT 모델 계열에서 작동 (`GET /api/providers/:provider/models` API에서 전체 지원 모델 확인) ## 빠른 시작 @@ -99,7 +99,7 @@ cloudcli npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project ``` -Claude Code, Codex, Gemini CLI를 지원합니다. 자세한 내용은 [샌드박스 문서](docker/)를 참고하세요. +Claude Code, Codex를 지원합니다. 자세한 내용은 [샌드박스 문서](docker/)를 참고하세요. --- @@ -115,7 +115,7 @@ CloudCLI UI는 CloudCLI Cloud를 구동하는 오픈 소스 UI 계층입니다. | **기기 유지 필요 여부** | 예 (머신 켜둬야 함) | 아니오 | | **모바일 접근** | 네트워크 내 브라우저 | 모든 기기 (네이티브 앱 예정) | | **세션 접근** | `~/.claude`에서 자동 발견 | 클라우드 환경 내 세션 | -| **지원 에이전트** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI | +| **지원 에이전트** | Claude Code, Cursor CLI, Codex | Claude Code, Cursor CLI, Codex | | **파일 탐색기 및 Git** | UI에 통합됨 | UI에 통합됨 | | **MCP 구성** | UI에서 관리, 로컬 `~/.claude` 설정과 동기화됨 | UI에서 관리 | | **IDE 접근** | 로컬 IDE | 클라우드 환경에 연결된 모든 IDE | @@ -160,7 +160,7 @@ CloudCLI는 커스텀 탭과 선택적 Node.js 백엔드가 포함된 플러그 | **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | 현재 프로젝트의 파일 수, 코드 줄 수, 파일 유형 분포, 가장 큰 파일, 최근 수정 파일을 표시 | | **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | 다중 탭을 지원하는 전체 xterm.js 터미널 | | **[Claude Watch](https://github.com/satsuki19980613/cloudcli-claude-watch)** | 장시간 실행 중인 Claude Code 세션의 중단 상태를 감시하고 프로세스 제어를 제공 | -| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | 워크스페이스 범위 예약 프롬프트를 만들고 Codex, Claude Code, Gemini CLI 같은 로컬 CLI로 실행 | +| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | 워크스페이스 범위 예약 프롬프트를 만들고 Codex, Claude Code 같은 로컬 CLI로 실행 | | **[PRISM CloudCLI](https://github.com/jakeefr/cloudcli-plugin-prism)** | CloudCLI 안에서 Claude Code 세션 인텔리전스와 토큰 소모 가시성을 제공 | | **[Sessions](https://github.com/strykereye2/cloudcli-plugin-session-manager)** | 활성 Claude Code 세션을 보고, 관리하고, 종료 | | **[Token Cost Calculator](https://github.com/NightmareAway/cloudcli-plugin-token-cost-calculator)** | 모델 가격과 토큰 사용량으로 API 비용을 계산하고 모델 가격 프리셋을 지원 | @@ -186,7 +186,7 @@ CloudCLI UI와 CloudCLI Cloud는 Claude Code를 확장하며 별도로 존재하 - **모든 세션을 다룬다** — CloudCLI UI는 `~/.claude` 폴더에서 모든 세션을 자동 발견합니다. Remote Control은 단일 활성 세션만 노출합니다. - **설정은 그대로** — CloudCLI UI에서 변경한 MCP, 도구 권한, 프로젝트 설정은 Claude Code에 즉시 반영됩니다. -- **지원 에이전트가 더 많음** — Claude Code, Cursor CLI, Codex, Gemini CLI 지원. +- **지원 에이전트가 더 많음** — Claude Code, Cursor CLI, Codex 지원. - **전체 UI 제공** — 단일 채팅 창이 아닌 파일 탐색기, Git 통합, MCP 관리 및 셸 터미널 포함. - **CloudCLI Cloud는 클라우드에서 실행** — 노트북을 닫아도 에이전트가 실행됩니다. 터미널을 계속 확인할 필요 없음. @@ -195,7 +195,7 @@ CloudCLI UI와 CloudCLI Cloud는 Claude Code를 확장하며 별도로 존재하
AI 구독을 별도로 결제해야 하나요? -네. CloudCLI는 환경만 제공합니다. Claude, Cursor, Codex, Gemini 구독 비용은 별도로 부과됩니다. CloudCLI Cloud는 관리형 환경을 월 €7부터 제공합니다. +네. CloudCLI는 환경만 제공합니다. Claude, Cursor, Codex 구독 비용은 별도로 부과됩니다. CloudCLI Cloud는 관리형 환경을 월 €7부터 제공합니다.
@@ -234,7 +234,6 @@ GNU General Public License v3.0 - 자세한 내용은 [LICENSE](LICENSE) 파일 - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropic 공식 CLI - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursor 공식 CLI - **[Codex](https://developers.openai.com/codex)** - OpenAI Codex -- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI - **[React](https://react.dev/)** - 사용자 인터페이스 라이브러리 - **[Vite](https://vitejs.dev/)** - 빠른 빌드 도구 및 개발 서버 - **[Tailwind CSS](https://tailwindcss.com/)** - 유틸리티 우선 CSS 프레임워크 @@ -246,5 +245,5 @@ GNU General Public License v3.0 - 자세한 내용은 [LICENSE](LICENSE) 파일 ---
- Claude Code, Cursor, Codex 커뮤니티를 위해 정성껏 제작되었습니다. + Claude Code, Cursor, Codex 커뮤니티를 위해 정성껏 제작되었습니다.
diff --git a/README.md b/README.md index 65184902..9fa3d665 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@
- CloudCLI UI -

Cloud CLI (aka Claude Code UI)

-

A desktop and mobile UI for Claude Code, Cursor CLI, Codex, and Gemini-CLI.
Use it locally or remotely to view your active projects and sessions from everywhere.

+ CloudCLI UI +

Cloud CLI (aka Claude Code UI)

+

A desktop and mobile UI for Claude Code, Cursor CLI, and Codex.
Use it locally or remotely to view your active projects and sessions from everywhere.

- CloudCLI Cloud · Documentation · Discord · Bug Reports · Contributing + CloudCLI Cloud · Documentation · Discord · Bug Reports · Contributing

- CloudCLI Cloud - Join our Discord -

- siteboon%2Fclaudecodeui | Trendshift + CloudCLI Cloud + Join our Discord +

+ siteboon%2Fclaudecodeui | Trendshift

English · Русский · Deutsch · 한국어 · 简体中文 · 繁體中文 · 日本語 · Türkçe
@@ -22,7 +22,7 @@ ## Screenshots
- +
@@ -43,7 +43,7 @@

CLI Selection

CLI Selection
-Select between Claude Code, Gemini, Cursor CLI and Codex +Select between Claude Code, Cursor CLI and Codex
@@ -63,7 +63,7 @@ - **Session Management** - Resume conversations, manage multiple sessions, and track history - **Plugin System** - Extend CloudCLI with custom plugins — add new tabs, backend services, and integrations. [Build your own →](https://github.com/cloudcli-ai/cloudcli-plugin-starter) - **TaskMaster AI Integration** *(Optional)* - Advanced project management with AI-powered task planning, PRD parsing, and workflow automation -- **Model Compatibility** - Works with Claude, GPT, and Gemini model families (the full list of supported models is available at runtime via `GET /api/providers/:provider/models`) +- **Model Compatibility** - Works with Claude and GPT model families (the full list of supported models is available at runtime via `GET /api/providers/:provider/models`) ## Quick Start @@ -103,7 +103,7 @@ Run agents in isolated sandboxes with hypervisor-level isolation. Starts Claude npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project ``` -Supports Claude Code, Codex, and Gemini CLI. See the [sandbox docs](docker/) for setup and advanced options. +Supports Claude Code and Codex. See the [sandbox docs](docker/) for setup and advanced options. ### Desktop Companion App @@ -131,7 +131,7 @@ CloudCLI UI is the open source UI layer that powers CloudCLI Cloud. You can self | **Machine needs to stay on** | Yes | Yes | No | | **Mobile access** | Any browser on your network | Any browser on your network | Any device | | **Desktop companion** | Optional. Choose Local CloudCLI | Optional. Choose Local CloudCLI | Optional. Opens cloud environments | -| **Agents supported** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI | +| **Agents supported** | Claude Code, Cursor CLI, Codex | Claude Code, Codex | Claude Code, Cursor CLI, Codex | | **File explorer and Git** | Yes | Yes | Yes | | **MCP configuration** | Synced with `~/.claude` | Managed via UI | Managed via UI | | **REST API** | Yes | Yes | Yes | @@ -176,7 +176,7 @@ CloudCLI has a plugin system that lets you add custom tabs with their own fronte | **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | Shows file counts, lines of code, file-type breakdown, largest files, and recently modified files for your current project | | **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | Full xterm.js terminal with multi-tab support | | **[Claude Watch](https://github.com/satsuki19980613/cloudcli-claude-watch)** | Watches long-running Claude Code sessions for hangs and exposes process controls | -| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | Create workspace-scoped scheduled prompts and execute them through a local CLI such as Codex, Claude Code, or Gemini CLI | +| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | Create workspace-scoped scheduled prompts and execute them through a local CLI such as Codex or Claude Code | | **[PRISM CloudCLI](https://github.com/jakeefr/cloudcli-plugin-prism)** | Session intelligence for Claude Code inside CloudCLI, including token burn visibility | | **[Sessions](https://github.com/strykereye2/cloudcli-plugin-session-manager)** | View, manage, and kill active Claude Code sessions | | **[Token Cost Calculator](https://github.com/NightmareAway/cloudcli-plugin-token-cost-calculator)** | Calculate API costs from model prices and token usage, with preset model pricing support | @@ -203,7 +203,7 @@ Here's what that means in practice: - **All your sessions, not just one** — CloudCLI UI auto-discovers every session from your `~/.claude` folder. Remote Control only exposes the single active session to make it available in the Claude mobile app. - **Your settings are your settings** — MCP servers, tool permissions, and project config you change in CloudCLI UI are written directly to your Claude Code config and take effect immediately, and vice versa. -- **Works with more agents** — Claude Code, Cursor CLI, Codex, and Gemini CLI, not just Claude Code. +- **Works with more agents** — Claude Code, Cursor CLI and Codex, not just Claude Code. - **Full UI, not just a chat window** — file explorer, Git integration, MCP management, and a shell terminal are all built in. - **CloudCLI Cloud runs in the cloud** — close your laptop, the agent keeps running. No terminal to babysit, no machine to keep awake. @@ -212,7 +212,7 @@ Here's what that means in practice:
Do I need to pay for an AI subscription separately? -Yes. CloudCLI provides the environment, not the AI. You bring your own Claude, Cursor, Codex, or Gemini subscription. CloudCLI Cloud starts at €7/month for the hosted environment on top of that. +Yes. CloudCLI provides the environment, not the AI. You bring your own Claude, Cursor, or Codex subscription. CloudCLI Cloud starts at €7/month for the hosted environment on top of that.
@@ -245,7 +245,7 @@ GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) — see [LIC This project is open source and free to use, modify, and distribute under the AGPL-3.0-or-later license. If you modify this software and run it as a network service, you must make your modified source code available to users of that service. -CloudCLI UI - (https://cloudcli.ai). +CloudCLI UI - (https://cloudcli.ai). ## Acknowledgments @@ -253,7 +253,6 @@ CloudCLI UI - (https://cloudcli.ai). - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropic's official CLI - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursor's official CLI - **[Codex](https://developers.openai.com/codex)** - OpenAI Codex -- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI - **[React](https://react.dev/)** - User interface library - **[Vite](https://vitejs.dev/)** - Fast build tool and dev server - **[Tailwind CSS](https://tailwindcss.com/)** - Utility-first CSS framework @@ -266,5 +265,5 @@ CloudCLI UI - (https://cloudcli.ai). ---
- Made with care for the Claude Code, Cursor and Codex community. + Made with care for the Claude Code, Cursor and Codex community.
diff --git a/README.ru.md b/README.ru.md index 12210761..8fb907e2 100644 --- a/README.ru.md +++ b/README.ru.md @@ -1,18 +1,18 @@
- CloudCLI UI -

Cloud CLI (aka Claude Code UI)

-

Десктопный и мобильный UI для Claude Code, Cursor CLI, Codex и Gemini-CLI.
Используйте локально или удалённо, чтобы просматривать активные проекты и сессии отовсюду.

+ CloudCLI UI +

Cloud CLI (aka Claude Code UI)

+

Десктопный и мобильный UI для Claude Code, Cursor CLI, Codex и .
Используйте локально или удалённо, чтобы просматривать активные проекты и сессии отовсюду.

- CloudCLI Cloud · Документация · Discord · Сообщить об ошибке · Участие в разработке + CloudCLI Cloud · Документация · Discord · Сообщить об ошибке · Участие в разработке

- CloudCLI Cloud - Join our Discord -

- siteboon%2Fclaudecodeui | Trendshift + CloudCLI Cloud + Join our Discord +

+ siteboon%2Fclaudecodeui | Trendshift

English · Русский · Deutsch · 한국어 · 简体中文 · 繁體中文 · 日本語 · Türkçe
@@ -43,7 +43,7 @@

Выбор CLI

CLI Selection
-Выбирайте между Claude Code, Gemini, Cursor CLI и Codex +Выбирайте между Claude Code, Cursor CLI и Codex @@ -62,7 +62,7 @@ - **Управление сессиями** - возобновляйте диалоги, управляйте несколькими сессиями и отслеживайте историю - **Система плагинов** - расширяйте CloudCLI кастомными плагинами — добавляйте новые вкладки, бэкенд-сервисы и интеграции. [Создать свой →](https://github.com/cloudcli-ai/cloudcli-plugin-starter) - **Интеграция с TaskMaster AI** *(опционально)* - продвинутое управление проектами с планированием задач на базе AI, разбором PRD и автоматизацией workflow -- **Совместимость с моделями** - работает с семействами моделей Claude, GPT и Gemini (полный список поддерживаемых моделей доступен через `GET /api/providers/:provider/models`) +- **Совместимость с моделями** - работает с семействами моделей Claude, GPT и (полный список поддерживаемых моделей доступен через `GET /api/providers/:provider/models`) ## Быстрый старт @@ -103,7 +103,7 @@ cloudcli npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project ``` -Поддерживаются Claude Code, Codex и Gemini CLI. Подробнее в [документации sandbox](docker/). +Поддерживаются Claude Code, Codex и. Подробнее в [документации sandbox](docker/). --- @@ -119,7 +119,7 @@ CloudCLI UI — это open source UI-слой, на котором постро | **Машина должна оставаться включённой** | Да | Нет | | **Доступ с мобильных устройств** | Любой браузер в вашей сети | Любое устройство, нативное приложение в разработке | | **Доступные сессии** | Все сессии автоматически обнаруживаются из `~/.claude` | Все сессии внутри вашей облачной среды | -| **Поддерживаемые агенты** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI | +| **Поддерживаемые агенты** | Claude Code, Cursor CLI, Codex | Claude Code, Cursor CLI, Codex | | **Проводник файлов и Git** | Да, встроены в UI | Да, встроены в UI | | **Конфигурация MCP** | Управляется через UI, синхронизируется с вашим локальным конфигом `~/.claude` | Управляется через UI | | **Доступ из IDE** | Ваша локальная IDE | Любая IDE, подключенная к вашей облачной среде | @@ -166,7 +166,7 @@ CloudCLI UI — это open source UI-слой, на котором постро | **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | Показывает количество файлов, строки кода, разбивку по типам файлов, самые большие файлы и недавно изменённые файлы для текущего проекта | | **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | Полноценный терминал xterm.js с поддержкой нескольких вкладок | | **[Claude Watch](https://github.com/satsuki19980613/cloudcli-claude-watch)** | Отслеживает зависания долгих сессий Claude Code и предоставляет управление процессами | -| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | Создаёт запланированные промпты для рабочей области и запускает их через локальную CLI, например Codex, Claude Code или Gemini CLI | +| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | Создаёт запланированные промпты для рабочей области и запускает их через локальную CLI, например Codex, Claude Code или | | **[PRISM CloudCLI](https://github.com/jakeefr/cloudcli-plugin-prism)** | Аналитика сессий Claude Code внутри CloudCLI, включая видимость расхода токенов | | **[Sessions](https://github.com/strykereye2/cloudcli-plugin-session-manager)** | Просмотр, управление и завершение активных сессий Claude Code | | **[Token Cost Calculator](https://github.com/NightmareAway/cloudcli-plugin-token-cost-calculator)** | Расчёт стоимости API по ценам моделей и использованию токенов, с поддержкой пресетов цен | @@ -193,7 +193,7 @@ CloudCLI UI и CloudCLI Cloud расширяют Claude Code, а не работ - **Все ваши сессии, а не одна** — CloudCLI UI автоматически находит каждую сессию из папки `~/.claude`. Remote Control предоставляет только одну активную сессию, чтобы сделать её доступной в мобильном приложении Claude. - **Ваши настройки — это ваши настройки** — MCP-серверы, права инструментов и конфигурация проекта, изменённые в CloudCLI UI, записываются напрямую в конфиг Claude Code и вступают в силу сразу же, и наоборот. -- **Работает с большим числом агентов** — Claude Code, Cursor CLI, Codex и Gemini CLI, а не только Claude Code. +- **Работает с большим числом агентов** — Claude Code, Cursor CLI, Codex и, а не только Claude Code. - **Полноценный UI, а не просто окно чата** — проводник файлов, Git-интеграция, управление MCP и shell-терминал — всё встроено. - **CloudCLI Cloud работает в облаке** — закройте ноутбук, и агент продолжит работать. Не нужно следить за терминалом и держать машину постоянно активной. @@ -202,7 +202,7 @@ CloudCLI UI и CloudCLI Cloud расширяют Claude Code, а не работ
Нужно ли отдельно платить за AI-подписку? -Да. CloudCLI предоставляет среду, а не сам AI. Вы приносите свою подписку Claude, Cursor, Codex или Gemini. CloudCLI Cloud начинается от €7/месяц за хостируемую среду поверх этого. +Да. CloudCLI предоставляет среду, а не сам AI. Вы приносите свою подписку Claude, Cursor, Codex или . CloudCLI Cloud начинается от €7/месяц за хостируемую среду поверх этого.
@@ -241,7 +241,6 @@ GNU General Public License v3.0 - подробности в файле [LICENSE] - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - официальный CLI от Anthropic - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - официальный CLI от Cursor - **[Codex](https://developers.openai.com/codex)** - OpenAI Codex -- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI - **[React](https://react.dev/)** - библиотека пользовательских интерфейсов - **[Vite](https://vitejs.dev/)** - быстрый инструмент сборки и dev-сервер - **[Tailwind CSS](https://tailwindcss.com/)** - utility-first CSS framework @@ -254,5 +253,5 @@ GNU General Public License v3.0 - подробности в файле [LICENSE] ---
- Сделано с заботой для сообщества Claude Code, Cursor и Codex. + Сделано с заботой для сообщества Claude Code, Cursor и Codex.
diff --git a/README.tr.md b/README.tr.md index bba939f9..6cd11c86 100644 --- a/README.tr.md +++ b/README.tr.md @@ -1,18 +1,18 @@
- CloudCLI UI -

Cloud CLI (Claude Code UI olarak da bilinir)

-

Claude Code, Cursor CLI, Codex ve Gemini-CLI için masaüstü ve mobil arayüz.
Yerel ya da uzaktan kullanarak aktif projelerine ve oturumlarına her yerden erişebilirsin.

+ CloudCLI UI +

Cloud CLI (Claude Code UI olarak da bilinir)

+

Claude Code, Cursor CLI, Codex ve için masaüstü ve mobil arayüz.
Yerel ya da uzaktan kullanarak aktif projelerine ve oturumlarına her yerden erişebilirsin.

- CloudCLI Cloud · Dokümantasyon · Discord · Sorun Bildir · Katkıda Bulun + CloudCLI Cloud · Dokümantasyon · Discord · Sorun Bildir · Katkıda Bulun

- CloudCLI Cloud - Discord'a Katıl -

- siteboon%2Fclaudecodeui | Trendshift + CloudCLI Cloud + Discord'a Katıl +

+ siteboon%2Fclaudecodeui | Trendshift

English · Русский · Deutsch · 한국어 · 简体中文 · 繁體中文 · 日本語 · Türkçe
@@ -43,7 +43,7 @@

CLI Seçimi

CLI Seçimi
-Claude Code, Gemini, Cursor CLI ve Codex arasında seçim yap +Claude Code, Cursor CLI ve Codex arasında seçim yap @@ -62,7 +62,7 @@ - **Oturum Yönetimi** — Konuşmalara devam et, birden fazla oturumu yönet ve geçmişi takip et - **Eklenti Sistemi** — CloudCLI'ı özel eklentilerle genişlet: yeni sekmeler, arka uç servisleri ve entegrasyonlar ekle. [Kendi eklentini yaz →](https://github.com/cloudcli-ai/cloudcli-plugin-starter) - **TaskMaster AI Entegrasyonu** *(İsteğe Bağlı)* — AI destekli görev planlama, PRD ayrıştırma ve iş akışı otomasyonu ile gelişmiş proje yönetimi -- **Model Uyumluluğu** — Claude, GPT ve Gemini model aileleriyle çalışır (desteklenen tüm modeller için `GET /api/providers/:provider/models` API'sine bak) +- **Model Uyumluluğu** — Claude, GPT ve model aileleriyle çalışır (desteklenen tüm modeller için `GET /api/providers/:provider/models` API'sine bak) ## Hızlı Başlangıç @@ -103,7 +103,7 @@ Ajanları hipervizör seviyesinde izolasyonlu sandbox'larda çalıştır. Varsay npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project ``` -Claude Code, Codex ve Gemini CLI destekler. Kurulum ve gelişmiş seçenekler için [sandbox dokümantasyonuna](docker/) bak. +Claude Code, Codex ve destekler. Kurulum ve gelişmiş seçenekler için [sandbox dokümantasyonuna](docker/) bak. --- @@ -120,7 +120,7 @@ CloudCLI UI, CloudCLI Cloud'u güçlendiren açık kaynak arayüz katmanıdır. | **İzolasyon** | Kendi host'unda çalışır | Hipervizör seviyesi sandbox (microVM) | Tam bulut izolasyonu | | **Makinenin açık kalması gerek** | Evet | Evet | Hayır | | **Mobil erişim** | Ağındaki herhangi bir tarayıcı | Ağındaki herhangi bir tarayıcı | Herhangi bir cihaz, native uygulama yolda | -| **Desteklenen ajanlar** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI | +| **Desteklenen ajanlar** | Claude Code, Cursor CLI, Codex | Claude Code, Codex | Claude Code, Cursor CLI, Codex | | **Dosya gezgini ve Git** | Evet | Evet | Evet | | **MCP yapılandırması** | `~/.claude` ile senkron | UI üzerinden yönetilir | UI üzerinden yönetilir | | **REST API** | Evet | Evet | Evet | @@ -165,7 +165,7 @@ CloudCLI, kendi frontend UI'sı ve isteğe bağlı Node.js arka ucu olan özel s | **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | Mevcut projen için dosya sayıları, kod satırları, dosya türü dağılımı, en büyük dosyalar ve son değiştirilen dosyaları gösterir | | **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | Çoklu sekme destekli tam xterm.js terminali | | **[Claude Watch](https://github.com/satsuki19980613/cloudcli-claude-watch)** | Uzun süren Claude Code oturumlarını takılmalara karşı izler ve süreç kontrolleri sunar | -| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | Çalışma alanı kapsamlı zamanlanmış prompt'lar oluşturur ve bunları Codex, Claude Code veya Gemini CLI gibi yerel CLI'larla çalıştırır | +| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | Çalışma alanı kapsamlı zamanlanmış prompt'lar oluşturur ve bunları Codex, Claude Code veya gibi yerel CLI'larla çalıştırır | | **[PRISM CloudCLI](https://github.com/jakeefr/cloudcli-plugin-prism)** | CloudCLI içinde Claude Code oturum zekası ve token tüketimi görünürlüğü sağlar | | **[Sessions](https://github.com/strykereye2/cloudcli-plugin-session-manager)** | Aktif Claude Code oturumlarını görüntülemeni, yönetmeni ve sonlandırmanı sağlar | | **[Token Cost Calculator](https://github.com/NightmareAway/cloudcli-plugin-token-cost-calculator)** | Model fiyatları ve token kullanımından API maliyetlerini hesaplar; model fiyatı hazır ayarlarını destekler | @@ -192,7 +192,7 @@ Pratikte bu ne demek: - **Tek oturum değil, tüm oturumların** — CloudCLI UI, `~/.claude` klasöründeki her oturumu otomatik keşfeder. Remote Control yalnızca tek aktif oturumu Claude mobil uygulamasına açar. - **Ayarların sana ait** — UI'da değiştirdiğin MCP sunucuları, araç izinleri ve proje yapılandırması doğrudan Claude Code yapılandırmana yazılır ve anında etkili olur; tersi de geçerli. -- **Daha fazla ajanla çalışır** — Sadece Claude Code değil; Cursor CLI, Codex ve Gemini CLI de. +- **Daha fazla ajanla çalışır** — Sadece Claude Code değil; Cursor CLI, Codex ve de. - **Sadece sohbet penceresi değil, tam UI** — dosya gezgini, Git entegrasyonu, MCP yönetimi ve shell terminali hepsi yerleşik. - **CloudCLI Cloud bulutta çalışır** — laptop'unu kapat, ajan çalışmaya devam eder. Beklemen gereken terminal yok, uyanık tutman gereken makine yok. @@ -201,7 +201,7 @@ Pratikte bu ne demek:
AI aboneliği için ayrıca ödeme yapmam gerekiyor mu? -Evet. CloudCLI AI'yi değil, ortamı sağlar. Kendi Claude, Cursor, Codex veya Gemini aboneliğini getirirsin. CloudCLI Cloud, barındırılan ortam için aylık 7 €'dan başlar — bunun üzerine eklenir. +Evet. CloudCLI AI'yi değil, ortamı sağlar. Kendi Claude, Cursor, Codex veya aboneliğini getirirsin. CloudCLI Cloud, barındırılan ortam için aylık 7 €'dan başlar — bunun üzerine eklenir.
@@ -242,7 +242,6 @@ CloudCLI UI — (https://cloudcli.ai). - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** — Anthropic'in resmi CLI'ı - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** — Cursor'un resmi CLI'ı - **[Codex](https://developers.openai.com/codex)** — OpenAI Codex -- **[Gemini-CLI](https://geminicli.com/)** — Google Gemini CLI - **[React](https://react.dev/)** — Kullanıcı arayüzü kütüphanesi - **[Vite](https://vitejs.dev/)** — Hızlı derleme aracı ve geliştirme sunucusu - **[Tailwind CSS](https://tailwindcss.com/)** — Utility-first CSS framework @@ -255,5 +254,5 @@ CloudCLI UI — (https://cloudcli.ai). ---
- Claude Code, Cursor ve Codex topluluğu için özenle yapıldı. + Claude Code, Cursor ve Codex topluluğu için özenle yapıldı.
diff --git a/README.zh-CN.md b/README.zh-CN.md index 97da920e..4de00ee8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,18 +1,18 @@
- CloudCLI UI -

Cloud CLI(又名 Claude Code UI)

-

Claude CodeCursor CLICodexGemini-CLI 的桌面和移动端 UI。可在本地或远程使用,从任何地方查看激活的项目与会话。

+ CloudCLI UI +

Cloud CLI(又名 Claude Code UI)

+

Claude CodeCursor CLICodex 和 的桌面和移动端 UI。可在本地或远程使用,从任何地方查看激活的项目与会话。

- CloudCLI Cloud · 文档 · Discord · Bug 报告 · 贡献指南 + CloudCLI Cloud · 文档 · Discord · Bug 报告 · 贡献指南

- CloudCLI Cloud - 加入 Discord 社区 -

- siteboon%2Fclaudecodeui | Trendshift + CloudCLI Cloud + 加入 Discord 社区 +

+ siteboon%2Fclaudecodeui | Trendshift

English · Русский · Deutsch · 한국어 · 简体中文 · 繁體中文 · 日本語 · Türkçe
@@ -43,7 +43,7 @@

CLI 选择

CLI 选择
-在 Claude Code、Gemini、Cursor CLI 与 Codex 之间进行选择 +在 Claude Code、、Cursor CLI 与 Codex 之间进行选择 @@ -60,7 +60,7 @@ - **会话管理** - 恢复对话、管理多个会话并跟踪历史记录 - **插件系统** - 通过自定义选项卡、后端服务与集成扩展 CloudCLI。 [开始构建 →](https://github.com/cloudcli-ai/cloudcli-plugin-starter) - **TaskMaster AI 集成** *(可选)* - 结合 AI 任务规划、PRD 分析与工作流自动化,实现高级项目管理 -- **模型兼容性** - 支持 Claude、GPT、Gemini 模型家族(完整支持列表可通过 `GET /api/providers/:provider/models` 接口获取) +- **模型兼容性** - 支持 Claude、GPT、 模型家族(完整支持列表可通过 `GET /api/providers/:provider/models` 接口获取) ## 快速开始 @@ -99,7 +99,7 @@ cloudcli npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project ``` -支持 Claude Code、Codex 和 Gemini CLI。详情请参阅 [沙箱文档](docker/)。 +支持 Claude Code、Codex 和。详情请参阅 [沙箱文档](docker/)。 --- @@ -115,7 +115,7 @@ CloudCLI UI 是 CloudCLI Cloud 的开源 UI 层。你可以在本地机器上自 | **机器需保持开机吗** | 是 | 否 | | **移动端访问** | 网络内任意浏览器 | 任意设备(原生应用即将推出) | | **可用会话** | 自动发现 `~/.claude` 中的所有会话 | 云端环境内的会话 | -| **支持的 Agents** | Claude Code、Cursor CLI、Codex、Gemini CLI | Claude Code、Cursor CLI、Codex、Gemini CLI | +| **支持的 Agents** | Claude Code、Cursor CLI、Codex、 | Claude Code、Cursor CLI、Codex、 | | **文件浏览与 Git** | 内置于 UI | 内置于 UI | | **MCP 配置** | UI 管理,与本地 `~/.claude` 配置同步 | UI 管理 | | **IDE 访问** | 本地 IDE | 任何连接到云环境的 IDE | @@ -160,7 +160,7 @@ CloudCLI 配备插件系统,允许你添加带自定义前端 UI 和可选 Nod | **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | 展示当前项目的文件数、代码行数、文件类型分布、最大文件以及最近修改的文件 | | **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | 支持多标签页的完整 xterm.js 终端 | | **[Claude Watch](https://github.com/satsuki19980613/cloudcli-claude-watch)** | 监控长时间运行的 Claude Code 会话是否卡住,并提供进程控制 | -| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | 创建工作区范围的定时提示词,并通过 Codex、Claude Code 或 Gemini CLI 等本地 CLI 执行 | +| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | 创建工作区范围的定时提示词,并通过 Codex、Claude Code 或 等本地 CLI 执行 | | **[PRISM CloudCLI](https://github.com/jakeefr/cloudcli-plugin-prism)** | 在 CloudCLI 中提供 Claude Code 会话智能分析,包括 token 消耗可视化 | | **[Sessions](https://github.com/strykereye2/cloudcli-plugin-session-manager)** | 查看、管理并终止活动的 Claude Code 会话 | | **[Token Cost Calculator](https://github.com/NightmareAway/cloudcli-plugin-token-cost-calculator)** | 根据模型价格和 token 用量计算 API 成本,并支持模型价格预设 | @@ -186,7 +186,7 @@ CloudCLI UI 与 CloudCLI Cloud 是对 Claude Code 的扩展,而非旁观 — M - **覆盖全部会话** — CloudCLI UI 会自动扫描 `~/.claude` 文件夹中的每个会话。Remote Control 只暴露当前活动的会话。 - **设置统一** — 在 CloudCLI UI 中修改的 MCP、工具权限等设置会立即写入 Claude Code。 -- **支持更多 Agents** — Claude Code、Cursor CLI、Codex、Gemini CLI。 +- **支持更多 Agents** — Claude Code、Cursor CLI、Codex、。 - **完整 UI** — 除了聊天界面,还包括文件浏览器、Git 集成、MCP 管理和 Shell 终端。 - **CloudCLI Cloud 保持运行于云端** — 关闭本地设备也不会中断代理运行,无需监控终端。 @@ -195,7 +195,7 @@ CloudCLI UI 与 CloudCLI Cloud 是对 Claude Code 的扩展,而非旁观 — M
需要额外购买 AI 订阅吗? -需要。CloudCLI 只提供环境。你仍需自行获取 Claude、Cursor、Codex 或 Gemini 订阅。CloudCLI Cloud 从 €7/月起提供托管环境。 +需要。CloudCLI 只提供环境。你仍需自行获取 Claude、Cursor、Codex 或 订阅。CloudCLI Cloud 从 €7/月起提供托管环境。
@@ -234,7 +234,6 @@ GNU 通用公共许可证 v3.0 - 详见 [LICENSE](LICENSE) 文件。 - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropic 官方 CLI - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursor 官方 CLI - **[Codex](https://developers.openai.com/codex)** - OpenAI Codex -- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI - **[React](https://react.dev/)** - 用户界面库 - **[Vite](https://vitejs.dev/)** - 快速构建工具与开发服务器 - **[Tailwind CSS](https://tailwindcss.com/)** - 实用先行 CSS 框架 @@ -246,5 +245,5 @@ GNU 通用公共许可证 v3.0 - 详见 [LICENSE](LICENSE) 文件。 ---
- 为 Claude Code、Cursor 和 Codex 社区精心打造。 + 为 Claude Code、Cursor 和 Codex 社区精心打造。
diff --git a/README.zh-TW.md b/README.zh-TW.md index 9f8c972a..bc7fd5ce 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -1,18 +1,18 @@
- CloudCLI UI -

Cloud CLI(又名 Claude Code UI)

-

Claude CodeCursor CLICodexGemini-CLI 的桌面和行動裝置 UI。可在本機或遠端使用,從任何地方查看您的專案與工作階段。

+ CloudCLI UI +

Cloud CLI(又名 Claude Code UI)

+

Claude CodeCursor CLICodex 和 的桌面和行動裝置 UI。可在本機或遠端使用,從任何地方查看您的專案與工作階段。

- CloudCLI Cloud · 文件 · Discord · Bug 回報 · 貢獻指南 + CloudCLI Cloud · 文件 · Discord · Bug 回報 · 貢獻指南

- CloudCLI Cloud - 加入 Discord 社群 -

- siteboon%2Fclaudecodeui | Trendshift + CloudCLI Cloud + 加入 Discord 社群 +

+ siteboon%2Fclaudecodeui | Trendshift

English · Русский · Deutsch · 한국어 · 简体中文 · 繁體中文 · 日本語 · Türkçe
@@ -43,7 +43,7 @@

CLI 選擇

CLI 選擇
-在 Claude Code、Gemini、Cursor CLI 與 Codex 之間進行選擇 +在 Claude Code、、Cursor CLI 與 Codex 之間進行選擇 @@ -60,7 +60,7 @@ - **工作階段管理** — 恢復對話、管理多個工作階段並追蹤歷史紀錄 - **外掛系統** — 透過自訂分頁、後端服務與整合來擴充 CloudCLI。[開始建構 →](https://github.com/cloudcli-ai/cloudcli-plugin-starter) - **TaskMaster AI 整合** *(選用)* — 結合 AI 任務規劃、PRD 分析與工作流程自動化,實現進階專案管理 -- **模型相容性** — 支援 Claude、GPT、Gemini 模型家族(完整支援列表可透過 `GET /api/providers/:provider/models` 介面取得) +- **模型相容性** — 支援 Claude、GPT、 模型家族(完整支援列表可透過 `GET /api/providers/:provider/models` 介面取得) ## 快速開始 @@ -99,7 +99,7 @@ cloudcli npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project ``` -支援 Claude Code、Codex 和 Gemini CLI。詳情請參閱[沙箱文件](docker/)。 +支援 Claude Code、Codex 和。詳情請參閱[沙箱文件](docker/)。 --- @@ -115,7 +115,7 @@ CloudCLI UI 是 CloudCLI Cloud 的開源 UI 層。你可以在本機上自架它 | **機器需保持開機嗎** | 是 | 否 | | **行動裝置存取** | 網路內任意瀏覽器 | 任意裝置(原生應用程式即將推出) | | **可用工作階段** | 自動發現 `~/.claude` 中的所有工作階段 | 雲端環境內的工作階段 | -| **支援的 Agents** | Claude Code、Cursor CLI、Codex、Gemini CLI | Claude Code、Cursor CLI、Codex、Gemini CLI | +| **支援的 Agents** | Claude Code、Cursor CLI、Codex、 | Claude Code、Cursor CLI、Codex、 | | **檔案瀏覽與 Git** | 內建於 UI | 內建於 UI | | **MCP 設定** | UI 管理,與本機 `~/.claude` 設定同步 | UI 管理 | | **IDE 存取** | 本機 IDE | 任何連線到雲端環境的 IDE | @@ -160,7 +160,7 @@ CloudCLI 配備外掛系統,允許你新增帶有自訂前端 UI 和選用 Nod | **[Project Stats](https://github.com/cloudcli-ai/cloudcli-plugin-starter)** | 展示目前專案的檔案數、程式碼行數、檔案類型分佈、最大檔案以及最近修改的檔案 | | **[Web Terminal](https://github.com/cloudcli-ai/cloudcli-plugin-terminal)** | 支援多分頁的完整 xterm.js 終端機 | | **[Claude Watch](https://github.com/satsuki19980613/cloudcli-claude-watch)** | 監控長時間執行的 Claude Code 工作階段是否卡住,並提供程序控制 | -| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | 建立工作區範圍的排程提示詞,並透過 Codex、Claude Code 或 Gemini CLI 等本機 CLI 執行 | +| **[CloudCLI Scheduler](https://github.com/grostim/cloudcli-cron)** | 建立工作區範圍的排程提示詞,並透過 Codex、Claude Code 或 等本機 CLI 執行 | | **[PRISM CloudCLI](https://github.com/jakeefr/cloudcli-plugin-prism)** | 在 CloudCLI 中提供 Claude Code 工作階段智慧分析,包括 token 消耗可視化 | | **[Sessions](https://github.com/strykereye2/cloudcli-plugin-session-manager)** | 檢視、管理並終止作用中的 Claude Code 工作階段 | | **[Token Cost Calculator](https://github.com/NightmareAway/cloudcli-plugin-token-cost-calculator)** | 根據模型價格與 token 用量計算 API 成本,並支援模型價格預設 | @@ -186,7 +186,7 @@ CloudCLI UI 與 CloudCLI Cloud 是對 Claude Code 的擴充,而非旁觀 — M - **涵蓋全部工作階段** — CloudCLI UI 會自動掃描 `~/.claude` 資料夾中的每個工作階段。Remote Control 只暴露目前活動的工作階段。 - **設定統一** — 在 CloudCLI UI 中修改的 MCP、工具權限等設定會立即寫入 Claude Code。 -- **支援更多 Agents** — Claude Code、Cursor CLI、Codex、Gemini CLI。 +- **支援更多 Agents** — Claude Code、Cursor CLI、Codex、。 - **完整 UI** — 除了聊天介面,還包括檔案瀏覽器、Git 整合、MCP 管理和 Shell 終端機。 - **CloudCLI Cloud 持續運作於雲端** — 關閉本機裝置也不會中斷代理執行,無需監控終端機。 @@ -195,7 +195,7 @@ CloudCLI UI 與 CloudCLI Cloud 是對 Claude Code 的擴充,而非旁觀 — M
需要額外購買 AI 訂閱嗎? -需要。CloudCLI 只提供環境。你仍需自行取得 Claude、Cursor、Codex 或 Gemini 訂閱。CloudCLI Cloud 從 €7/月起提供託管環境。 +需要。CloudCLI 只提供環境。你仍需自行取得 Claude、Cursor、Codex 或 訂閱。CloudCLI Cloud 從 €7/月起提供託管環境。
@@ -234,7 +234,6 @@ GNU 通用公共授權條款 v3.0 — 詳見 [LICENSE](LICENSE) 檔案。 - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** — Anthropic 官方 CLI - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** — Cursor 官方 CLI - **[Codex](https://developers.openai.com/codex)** — OpenAI Codex -- **[Gemini-CLI](https://geminicli.com/)** — Google Gemini CLI - **[React](https://react.dev/)** — 使用者介面函式庫 - **[Vite](https://vitejs.dev/)** — 快速建構工具與開發伺服器 - **[Tailwind CSS](https://tailwindcss.com/)** — 實用優先 CSS 框架 @@ -246,5 +245,5 @@ GNU 通用公共授權條款 v3.0 — 詳見 [LICENSE](LICENSE) 檔案。 ---
- 為 Claude Code、Cursor 和 Codex 社群精心打造。 + 為 Claude Code、Cursor 和 Codex 社群精心打造。
diff --git a/docker/README.md b/docker/README.md index 134ebad0..b84ffeb4 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,9 +1,9 @@ - + # Sandboxed coding agents with a web & mobile IDE (CloudCLI) -[Docker Sandbox](https://docs.docker.com/ai/sandboxes/) templates that add [CloudCLI](https://cloudcli.ai) on top of Claude Code, Codex, and Gemini CLI. You get a full web and mobile IDE accessible from any browser on any device. +[Docker Sandbox](https://docs.docker.com/ai/sandboxes/) templates that add [CloudCLI](https://cloudcli.ai) on top of Claude Code and Codex. You get a full web and mobile IDE accessible from any browser on any device. ## Get started @@ -42,10 +42,6 @@ Store the matching API key and pass `--agent`: # OpenAI Codex sbx secret set -g openai npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project --agent codex - -# Gemini CLI -sbx secret set -g google -npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project --agent gemini ``` ### Available templates @@ -54,7 +50,6 @@ npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project --agent gemini |-------|----------| | **Claude Code** (default) | `docker.io/cloudcliai/sandbox:claude-code` | | OpenAI Codex | `docker.io/cloudcliai/sandbox:codex` | -| Gemini CLI | `docker.io/cloudcliai/sandbox:gemini` | These are used with `--template` when running `sbx` directly (see [Advanced usage](#advanced-usage)). diff --git a/docker/gemini/Dockerfile b/docker/gemini/Dockerfile deleted file mode 100644 index dc06db69..00000000 --- a/docker/gemini/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM docker/sandbox-templates:gemini - -USER root -COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh -RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh - -USER agent -RUN npm install -g @cloudcli-ai/cloudcli && cloudcli --version - -COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh -RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc diff --git a/electron/launcher/launcher.js b/electron/launcher/launcher.js index 80e30b52..93abea41 100644 --- a/electron/launcher/launcher.js +++ b/electron/launcher/launcher.js @@ -12,7 +12,7 @@ window.__MOCK_STATE__ = { { id: 'env-api', name: 'api-gateway', subdomain: 'api-gateway', access_url: 'https://api-gateway.cloudcli.ai', status: 'running', region: 'fra1', agent: 'Claude Code' }, { id: 'env-web', name: 'web-frontend', subdomain: 'web-frontend', access_url: 'https://web-frontend.cloudcli.ai', status: 'stopped', region: 'sfo1', agent: 'Codex' }, { id: 'env-data', name: 'data-pipeline', subdomain: 'data-pipeline', access_url: 'https://data-pipeline.cloudcli.ai', status: 'stopped', region: 'fra1', agent: 'Cursor' }, - { id: 'env-ml', name: 'ml-trainer', subdomain: 'ml-trainer', access_url: 'https://ml-trainer.cloudcli.ai', status: 'paused', region: 'iad1', agent: 'Gemini' }, + { id: 'env-ml', name: 'ml-trainer', subdomain: 'ml-trainer', access_url: 'https://ml-trainer.cloudcli.ai', status: 'paused', region: 'iad1', agent: 'OpenCode' }, ], }; diff --git a/eslint.config.js b/eslint.config.js index ef79e003..df10d25e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -169,7 +169,6 @@ export default tseslint.config( type: "backend-legacy-runtime", // legacy runtime persistence modules used while providers migrate into server/modules pattern: [ "server/projects.js", - "server/sessionManager.js", "server/utils/runtime-paths.js", ], // provider history loading still resolves session data through these legacy runtime files mode: "file", diff --git a/package.json b/package.json index d1cb070c..1cad3c55 100644 --- a/package.json +++ b/package.json @@ -121,8 +121,6 @@ "claude-code-ui", "cloudcli", "codex", - "gemini", - "gemini-cli", "cursor", "cursor-cli", "anthropic", diff --git a/public/api-docs.html b/public/api-docs.html index 040d8680..c2d60173 100644 --- a/public/api-docs.html +++ b/public/api-docs.html @@ -489,7 +489,7 @@ http://localhost:3001/api/agent
-

Trigger an AI agent (Claude, Cursor, Codex, Gemini, or OpenCode) to work on a project.

+

Trigger an AI agent (Claude, Cursor, Codex, or OpenCode) to work on a project.

Request Body Parameters

@@ -524,7 +524,7 @@ - + @@ -837,7 +837,6 @@ data: {"type":"done"} const PROVIDER_ORDER = [ { id: 'claude', name: 'Anthropic' }, { id: 'codex', name: 'OpenAI' }, - { id: 'gemini', name: 'Google' }, { id: 'cursor', name: 'Cursor' }, { id: 'opencode', name: 'OpenCode' }, ]; diff --git a/public/icons/gemini-ai-icon.svg b/public/icons/gemini-ai-icon.svg deleted file mode 100644 index f1cf3575..00000000 --- a/public/icons/gemini-ai-icon.svg +++ /dev/null @@ -1 +0,0 @@ -Gemini \ No newline at end of file diff --git a/redirect-package/README.md b/redirect-package/README.md index bcc82037..7c7c8669 100644 --- a/redirect-package/README.md +++ b/redirect-package/README.md @@ -14,20 +14,20 @@ ---
- CloudCLI UI -

Cloud CLI (aka Claude Code UI)

-

A desktop and mobile UI for Claude Code, Cursor CLI, Codex, and Gemini-CLI.
Use it locally or remotely to view your active projects and sessions from everywhere.

+ CloudCLI UI +

Cloud CLI (aka Claude Code UI)

+

A desktop and mobile UI for Claude Code, Cursor CLI, and Codex.
Use it locally or remotely to view your active projects and sessions from everywhere.

- CloudCLI Cloud · Documentation · Discord · Bug Reports · Contributing + CloudCLI Cloud · Documentation · Discord · Bug Reports · Contributing

- CloudCLI Cloud - Join our Discord -

- siteboon%2Fclaudecodeui | Trendshift + CloudCLI Cloud + Join our Discord +

+ siteboon%2Fclaudecodeui | Trendshift

--- @@ -56,7 +56,7 @@

CLI Selection

CLI Selection
-Select between Claude Code, Gemini, Cursor CLI and Codex +Select between Claude Code, Cursor CLI and Codex
provider string Optionalclaude, cursor, codex, gemini, or opencode (default: claude)claude, cursor, codex, or opencode (default: claude)
stream
@@ -75,7 +75,7 @@ - **Session Management** - Resume conversations, manage multiple sessions, and track history - **Plugin System** - Extend CloudCLI with custom plugins — add new tabs, backend services, and integrations. [Build your own →](https://github.com/cloudcli-ai/cloudcli-plugin-starter) - **TaskMaster AI Integration** *(Optional)* - Advanced project management with AI-powered task planning, PRD parsing, and workflow automation -- **Model Compatibility** - Works with Claude, GPT, and Gemini model families (the full list of supported models is available at runtime via `GET /api/providers/:provider/models`) +- **Model Compatibility** - Works with Claude and GPT model families (the full list of supported models is available at runtime via `GET /api/providers/:provider/models`) ## Quick Start @@ -121,7 +121,7 @@ CloudCLI UI is the open source UI layer that powers CloudCLI Cloud. You can self | **Machine needs to stay on** | Yes | No | | **Mobile access** | Any browser on your network | Any device, native app coming | | **Sessions available** | All sessions auto-discovered from `~/.claude` | All sessions within your cloud environment | -| **Agents supported** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI | +| **Agents supported** | Claude Code, Cursor CLI, Codex | Claude Code, Cursor CLI, Codex | | **File explorer and Git** | Yes, built into the UI | Yes, built into the UI | | **MCP configuration** | Managed via UI, synced with your local `~/.claude` config | Managed via UI | | **IDE access** | Your local IDE | Any IDE connected to your cloud environment | @@ -181,7 +181,7 @@ Here's what that means in practice: - **All your sessions, not just one** — CloudCLI UI auto-discovers every session from your `~/.claude` folder. Remote Control only exposes the single active session to make it available in the Claude mobile app. - **Your settings are your settings** — MCP servers, tool permissions, and project config you change in CloudCLI UI are written directly to your Claude Code config and take effect immediately, and vice versa. -- **Works with more agents** — Claude Code, Cursor CLI, Codex, and Gemini CLI, not just Claude Code. +- **Works with more agents** — Claude Code, Cursor CLI and Codex, not just Claude Code. - **Full UI, not just a chat window** — file explorer, Git integration, MCP management, and a shell terminal are all built in. - **CloudCLI Cloud runs in the cloud** — close your laptop, the agent keeps running. No terminal to babysit, no machine to keep awake. @@ -190,7 +190,7 @@ Here's what that means in practice:
Do I need to pay for an AI subscription separately? -Yes. CloudCLI provides the environment, not the AI. You bring your own Claude, Cursor, Codex, or Gemini subscription. CloudCLI Cloud starts at $7/month for the hosted environment on top of that. +Yes. CloudCLI provides the environment, not the AI. You bring your own Claude, Cursor, or Codex subscription. CloudCLI Cloud starts at $7/month for the hosted environment on top of that.
@@ -223,7 +223,7 @@ GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) — see [LIC This project is open source and free to use, modify, and distribute under the AGPL-3.0-or-later license. If you modify this software and run it as a network service, you must make your modified source code available to users of that service. -CloudCLI UI - (https://cloudcli.ai). +CloudCLI UI - (https://cloudcli.ai). ## Acknowledgments @@ -231,7 +231,6 @@ CloudCLI UI - (https://cloudcli.ai). - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** - Anthropic's official CLI - **[Cursor CLI](https://docs.cursor.com/en/cli/overview)** - Cursor's official CLI - **[Codex](https://developers.openai.com/codex)** - OpenAI Codex -- **[Gemini-CLI](https://geminicli.com/)** - Google Gemini CLI - **[React](https://react.dev/)** - User interface library - **[Vite](https://vitejs.dev/)** - Fast build tool and dev server - **[Tailwind CSS](https://tailwindcss.com/)** - Utility-first CSS framework @@ -244,5 +243,5 @@ CloudCLI UI - (https://cloudcli.ai). ---
- Made with care for the Claude Code, Cursor and Codex community. + Made with care for the Claude Code, Cursor and Codex community.
diff --git a/redirect-package/package.json b/redirect-package/package.json index 04563d83..035ffb20 100644 --- a/redirect-package/package.json +++ b/redirect-package/package.json @@ -22,8 +22,6 @@ "claude-code-ui", "cloudcli", "codex", - "gemini", - "gemini-cli", "cursor", "cursor-cli", "anthropic", diff --git a/server/cli.js b/server/cli.js index 0be3bf72..08d3af48 100755 --- a/server/cli.js +++ b/server/cli.js @@ -256,13 +256,11 @@ async function updatePackage() { const SANDBOX_TEMPLATES = { claude: 'docker.io/cloudcliai/sandbox:claude-code', codex: 'docker.io/cloudcliai/sandbox:codex', - gemini: 'docker.io/cloudcliai/sandbox:gemini', }; const SANDBOX_SECRETS = { claude: 'anthropic', codex: 'openai', - gemini: 'google', }; function parseSandboxArgs(args) { @@ -338,7 +336,7 @@ Subcommands: ${c.bright('help')} Show this help Options: - -a, --agent Agent to use: claude, codex, gemini (default: claude) + -a, --agent Agent to use: claude, codex (default: claude) -n, --name Sandbox name (default: derived from workspace folder) -t, --template Custom template image -e, --env Set environment variable (repeatable) @@ -359,7 +357,6 @@ Prerequisites: sbx login sbx secret set -g anthropic # for Claude sbx secret set -g openai # for Codex - sbx secret set -g google # for Gemini Advanced usage: For branch mode, multiple workspaces, memory limits, network policies, diff --git a/server/gemini-cli.js b/server/gemini-cli.js deleted file mode 100644 index 34e53b3f..00000000 --- a/server/gemini-cli.js +++ /dev/null @@ -1,586 +0,0 @@ -import { promises as fs } from 'fs'; -import os from 'os'; -import path from 'path'; - -import crossSpawn from 'cross-spawn'; - -import sessionManager from './sessionManager.js'; -import GeminiResponseHandler from './gemini-response-handler.js'; -import { appendImagesInputTag } from './shared/image-attachments.js'; -import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js'; -import { providerAuthService } from './modules/providers/services/provider-auth.service.js'; -import { providerModelsService } from './modules/providers/services/provider-models.service.js'; -import { createCompleteMessage, createNormalizedMessage, flattenPromptForWindowsShell } from './shared/utils.js'; - -// cross-spawn resolves .cmd shims/PATHEXT on Windows and delegates to -// child_process.spawn everywhere else. -const spawnFunction = crossSpawn; - -let activeGeminiProcesses = new Map(); // Track active processes by session ID - -function mapGeminiExitCodeToMessage(exitCode) { - switch (exitCode) { - case 42: - return 'Gemini rejected the request input (exit code 42).'; - case 44: - return 'Gemini sandbox error (exit code 44). Check local sandbox/container settings.'; - case 52: - return 'Gemini configuration error (exit code 52). Check your Gemini settings files for invalid JSON/config.'; - case 53: - return 'Gemini conversation turn limit reached (exit code 53). Start a new Gemini session.'; - default: - return null; - } -} - -const GEMINI_AUTH_ENV_KEYS = [ - 'GEMINI_API_KEY', - 'GOOGLE_API_KEY', - 'GOOGLE_CLOUD_PROJECT', - 'GOOGLE_CLOUD_PROJECT_ID', - 'GOOGLE_CLOUD_LOCATION', - 'GOOGLE_APPLICATION_CREDENTIALS' -]; - -function parseEnvFileContent(content) { - const parsed = {}; - - for (const rawLine of content.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line || line.startsWith('#')) { - continue; - } - - const exportPrefix = 'export '; - const normalizedLine = line.startsWith(exportPrefix) ? line.slice(exportPrefix.length).trim() : line; - const separatorIndex = normalizedLine.indexOf('='); - - if (separatorIndex <= 0) { - continue; - } - - const key = normalizedLine.slice(0, separatorIndex).trim(); - if (!key) { - continue; - } - - let value = normalizedLine.slice(separatorIndex + 1).trim(); - const hasDoubleQuotes = value.startsWith('"') && value.endsWith('"'); - const hasSingleQuotes = value.startsWith('\'') && value.endsWith('\''); - - if (hasDoubleQuotes || hasSingleQuotes) { - value = value.slice(1, -1); - } else { - // Support inline comments in unquoted values: KEY=value # comment - value = value.replace(/\s+#.*$/, '').trim(); - } - - parsed[key] = value; - } - - return parsed; -} - -async function loadGeminiUserLevelEnv() { - const geminiCliHome = (process.env.GEMINI_CLI_HOME || '').trim() || os.homedir(); - const envCandidates = [ - path.join(geminiCliHome, '.gemini', '.env'), - path.join(geminiCliHome, '.env') - ]; - - for (const envPath of envCandidates) { - try { - await fs.access(envPath); - const content = await fs.readFile(envPath, 'utf8'); - return parseEnvFileContent(content); - } catch { - // Keep scanning for the next candidate. - } - } - - return {}; -} - -async function buildGeminiProcessEnv() { - const processEnv = { ...process.env }; - if (processEnv.GEMINI_API_KEY || processEnv.GOOGLE_API_KEY || processEnv.GOOGLE_APPLICATION_CREDENTIALS) { - return processEnv; - } - - // Gemini CLI docs recommend ~/.gemini/.env for persistent headless auth settings. - // When the server process was launched without shell profile variables, we still - // want the spawned CLI process to inherit those user-level credentials. - const userEnv = await loadGeminiUserLevelEnv(); - for (const key of GEMINI_AUTH_ENV_KEYS) { - if (!processEnv[key] && userEnv[key]) { - processEnv[key] = userEnv[key]; - } - } - - return processEnv; -} - -async function spawnGemini(command, options = {}, ws) { - const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options; - const resolvedModel = await providerModelsService.resolveResumeModel( - 'gemini', - sessionId, - options.model - ); - let capturedSessionId = sessionId; // Track session ID throughout the process - let sessionCreatedSent = false; // Track if we've already sent session-created event - let assistantBlocks = []; // Accumulate the full response blocks including tools - // Unified lifecycle contract: exactly one terminal `complete` per run - // (close and error handlers can both fire for spawn failures). - let completeSent = false; - - // Use tools settings passed from frontend, or defaults - const settings = toolsSettings || { - allowedTools: [], - disallowedTools: [], - skipPermissions: false - }; - - // Build Gemini CLI command - start with print/resume flags first - const args = []; - - // Image attachments ride along as an path list appended to - // the prompt; session history readers strip the tag back out for display. - // gemini is a .cmd shim on Windows, so the whole argument must be - // newline-free or cmd.exe silently truncates it at the first newline. - const finalCommand = command && command.trim() - ? flattenPromptForWindowsShell(appendImagesInputTag(command, images)) - : command; - - // Add prompt flag with command if we have a command - if (finalCommand && finalCommand.trim()) { - args.push('--prompt', finalCommand); - } - - // If we have a sessionId, we want to resume - if (sessionId) { - const session = sessionManager.getSession(sessionId); - if (session && session.cliSessionId) { - args.push('--resume', session.cliSessionId); - } - } - - // Use cwd (actual project directory) instead of projectPath (Gemini's metadata directory) - // Clean the path by removing any non-printable characters - const cleanPath = (cwd || projectPath || process.cwd()).replace(/[^\x20-\x7E]/g, '').trim(); - const workingDir = cleanPath; - - // Add basic flags for Gemini - if (options.debug) { - args.push('--debug'); - } - - // This integration runs Gemini in headless mode and cannot answer trust prompts. - // Skip folder-trust interactivity so authenticated runs don't fail with - // FatalUntrustedWorkspaceError in previously unseen directories. - args.push('--skip-trust'); - - // Add MCP config flag only if MCP servers are configured - try { - const geminiConfigPath = path.join(os.homedir(), '.gemini.json'); - let hasMcpServers = false; - - try { - await fs.access(geminiConfigPath); - const geminiConfigRaw = await fs.readFile(geminiConfigPath, 'utf8'); - const geminiConfig = JSON.parse(geminiConfigRaw); - - // Check global MCP servers - if (geminiConfig.mcpServers && Object.keys(geminiConfig.mcpServers).length > 0) { - hasMcpServers = true; - } - - // Check project-specific MCP servers - if (!hasMcpServers && geminiConfig.geminiProjects) { - const currentProjectPath = process.cwd(); - const projectConfig = geminiConfig.geminiProjects[currentProjectPath]; - if (projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0) { - hasMcpServers = true; - } - } - } catch (e) { - // Ignore if file doesn't exist or isn't parsable - } - - if (hasMcpServers) { - args.push('--mcp-config', geminiConfigPath); - } - } catch (error) { - // Ignore outer errors - } - - // Add model for all sessions (both new and resumed) - let modelToUse = resolvedModel || 'gemini-2.5-flash'; - args.push('--model', modelToUse); - args.push('--output-format', 'stream-json'); - - // Handle approval modes and allowed tools - if (settings.skipPermissions || options.skipPermissions || permissionMode === 'yolo') { - args.push('--yolo'); - } else if (permissionMode === 'auto_edit') { - args.push('--approval-mode', 'auto_edit'); - } else if (permissionMode === 'plan') { - args.push('--approval-mode', 'plan'); - } - - if (settings.allowedTools && settings.allowedTools.length > 0) { - args.push('--allowed-tools', settings.allowedTools.join(',')); - } - - // Try to find gemini in PATH first, then fall back to environment variable - const geminiPath = process.env.GEMINI_PATH || 'gemini'; - let spawnCmd = geminiPath; - let spawnArgs = args; - - // On non-Windows platforms, wrap the execution in a shell to avoid ENOEXEC - // which happens when the target is a script lacking a shebang. - if (os.platform() !== 'win32') { - spawnCmd = 'sh'; - // Use exec to replace the shell process, ensuring signals hit gemini directly - spawnArgs = ['-c', 'exec "$0" "$@"', geminiPath, ...args]; - } - - const spawnEnv = await buildGeminiProcessEnv(); - - return new Promise((resolve, reject) => { - const geminiProcess = spawnFunction(spawnCmd, spawnArgs, { - cwd: workingDir, - stdio: ['pipe', 'pipe', 'pipe'], - env: spawnEnv - }); - let terminalNotificationSent = false; - let terminalFailureReason = null; - - const notifyTerminalState = ({ code = null, error = null } = {}) => { - if (terminalNotificationSent) { - return; - } - - terminalNotificationSent = true; - - const finalSessionId = capturedSessionId || sessionId || processKey; - if (code === 0 && !error) { - notifyRunStopped({ - userId: ws?.userId || null, - provider: 'gemini', - sessionId: finalSessionId, - sessionName: sessionSummary, - stopReason: 'completed' - }); - return; - } - - notifyRunFailed({ - userId: ws?.userId || null, - provider: 'gemini', - sessionId: finalSessionId, - sessionName: sessionSummary, - error: error || terminalFailureReason || `Gemini CLI exited with code ${code}` - }); - }; - - // Store process reference for potential abort - const processKey = capturedSessionId || sessionId || Date.now().toString(); - activeGeminiProcesses.set(processKey, geminiProcess); - - // Store sessionId on the process object for debugging - geminiProcess.sessionId = processKey; - - // Close stdin to signal we're done sending input - geminiProcess.stdin.end(); - - // Add timeout handler - const timeoutMs = 120000; // 120 seconds for slower models - let timeout; - - const startTimeout = () => { - if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => { - const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey); - terminalFailureReason = `Gemini CLI timeout - no response received for ${timeoutMs / 1000} seconds`; - ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' })); - try { - geminiProcess.kill('SIGTERM'); - } catch (e) { } - }, timeoutMs); - }; - - startTimeout(); - - // Save user message to session when starting - if (command && capturedSessionId) { - sessionManager.addMessage(capturedSessionId, 'user', command); - } - - // Create response handler for NDJSON buffering - let responseHandler; - if (ws) { - responseHandler = new GeminiResponseHandler(ws, { - onContentFragment: (content) => { - if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') { - assistantBlocks[assistantBlocks.length - 1].text += content; - } else { - assistantBlocks.push({ type: 'text', text: content }); - } - }, - onToolUse: (event) => { - assistantBlocks.push({ - type: 'tool_use', - id: event.tool_id, - name: event.tool_name, - input: event.parameters - }); - }, - onToolResult: (event) => { - if (capturedSessionId) { - if (assistantBlocks.length > 0) { - sessionManager.addMessage(capturedSessionId, 'assistant', [...assistantBlocks]); - assistantBlocks = []; - } - sessionManager.addMessage(capturedSessionId, 'user', [{ - type: 'tool_result', - tool_use_id: event.tool_id, - content: event.output === undefined ? null : event.output, - is_error: event.status === 'error' - }]); - } - }, - onInit: (event) => { - const discoveredSessionId = event?.session_id; - if (!discoveredSessionId) { - return; - } - - // New Gemini sessions announce their canonical ID asynchronously via the - // initial `init` stream event. Avoid synthetic IDs and only register - // the session once that real ID is known (same model used by Claude/Codex). - if (!capturedSessionId) { - capturedSessionId = discoveredSessionId; - - sessionManager.createSession(capturedSessionId, cwd || process.cwd()); - if (command) { - sessionManager.addMessage(capturedSessionId, 'user', command); - } - - if (processKey !== capturedSessionId) { - activeGeminiProcesses.delete(processKey); - activeGeminiProcesses.set(capturedSessionId, geminiProcess); - } - - geminiProcess.sessionId = capturedSessionId; - - if (ws.setSessionId && typeof ws.setSessionId === 'function') { - ws.setSessionId(capturedSessionId); - } - - if (!sessionId && !sessionCreatedSent) { - sessionCreatedSent = true; - ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'gemini' })); - } - } - - const sess = sessionManager.getSession(capturedSessionId); - if (sess && !sess.cliSessionId) { - sess.cliSessionId = discoveredSessionId; - sessionManager.saveSession(capturedSessionId); - } - } - }); - } - - // Handle stdout - geminiProcess.stdout.on('data', (data) => { - const rawOutput = data.toString(); - startTimeout(); // Re-arm the timeout - - if (responseHandler) { - responseHandler.processData(rawOutput); - } else if (rawOutput) { - // Fallback to direct sending for raw CLI mode without WS - if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') { - assistantBlocks[assistantBlocks.length - 1].text += rawOutput; - } else { - assistantBlocks.push({ type: 'text', text: rawOutput }); - } - const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId); - ws.send(createNormalizedMessage({ kind: 'stream_delta', content: rawOutput, sessionId: socketSessionId, provider: 'gemini' })); - } - }); - - // Handle stderr - geminiProcess.stderr.on('data', (data) => { - const errorMsg = data.toString(); - - // Filter out deprecation warnings and "Loaded cached credentials" message - if (errorMsg.includes('[DEP0040]') || - errorMsg.includes('DeprecationWarning') || - errorMsg.includes('--trace-deprecation') || - errorMsg.includes('Loaded cached credentials')) { - return; - } - - const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId); - ws.send(createNormalizedMessage({ kind: 'error', content: errorMsg, sessionId: socketSessionId, provider: 'gemini' })); - }); - - // Handle process completion - geminiProcess.on('close', async (code) => { - clearTimeout(timeout); - - // Flush any remaining buffered content - if (responseHandler) { - responseHandler.forceFlush(); - responseHandler.destroy(); - } - - // Clean up process reference - const finalSessionId = capturedSessionId || sessionId || processKey; - activeGeminiProcesses.delete(finalSessionId); - - // Save assistant response to session if we have one - if (finalSessionId && assistantBlocks.length > 0) { - sessionManager.addMessage(finalSessionId, 'assistant', assistantBlocks); - } - - // Terminal complete — skipped for aborted runs (abort-session - // already sent the aborted complete on this run's behalf). - if (!completeSent && !geminiProcess.aborted) { - completeSent = true; - ws.send(createCompleteMessage({ provider: 'gemini', sessionId: finalSessionId, exitCode: code })); - } - - if (code === 0) { - notifyTerminalState({ code }); - resolve(); - } else { - const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId; - - // code 127 = shell "command not found" - check installation - if (code === 127) { - const installed = await providerAuthService.isProviderInstalled('gemini'); - if (!installed) { - terminalFailureReason = 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli'; - ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' })); - } - } else if (code === 41) { - // Gemini CLI documents exit code 41 as FatalAuthenticationError. - // Surface an actionable auth error instead of a generic exit-code message. - let authErrorSuffix = ''; - try { - const authStatus = await providerAuthService.getProviderAuthStatus('gemini'); - if (!authStatus?.authenticated && authStatus?.error) { - authErrorSuffix = ` Details: ${authStatus.error}`; - } - } catch { - // Keep base remediation text when auth status lookup fails. - } - - terminalFailureReason = - 'Gemini authentication failed (exit code 41). ' - + 'Run `gemini` in a terminal to choose an auth method, or configure a valid `GEMINI_API_KEY`.' - + authErrorSuffix; - ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' })); - } else { - const mappedError = mapGeminiExitCodeToMessage(code); - if (mappedError) { - terminalFailureReason = mappedError; - ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' })); - } - } - - notifyTerminalState({ - code, - error: code === null ? 'Gemini CLI process was terminated or timed out' : null - }); - reject( - new Error( - terminalFailureReason - || (code === null - ? 'Gemini CLI process was terminated or timed out' - : `Gemini CLI exited with code ${code}`) - ) - ); - } - }); - - // Handle process errors - geminiProcess.on('error', async (error) => { - // Clean up process reference on error - const finalSessionId = capturedSessionId || sessionId || processKey; - activeGeminiProcesses.delete(finalSessionId); - - // Check if Gemini CLI is installed for a clearer error message - const installed = await providerAuthService.isProviderInstalled('gemini'); - const errorContent = !installed - ? 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli' - : error.message; - - const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId; - ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'gemini' })); - if (!completeSent && !geminiProcess.aborted) { - completeSent = true; - ws.send(createCompleteMessage({ provider: 'gemini', sessionId: errorSessionId, exitCode: 1 })); - } - notifyTerminalState({ error }); - - reject(error); - }); - - }); -} - -function abortGeminiSession(sessionId) { - let geminiProc = activeGeminiProcesses.get(sessionId); - let processKey = sessionId; - - if (!geminiProc) { - for (const [key, proc] of activeGeminiProcesses.entries()) { - if (proc.sessionId === sessionId) { - geminiProc = proc; - processKey = key; - break; - } - } - } - - if (geminiProc) { - try { - // The abort handler sends the terminal complete (aborted: true); - // flag the process so its close handler does not emit a second one. - geminiProc.aborted = true; - geminiProc.kill('SIGTERM'); - setTimeout(() => { - if (activeGeminiProcesses.has(processKey)) { - try { - geminiProc.kill('SIGKILL'); - } catch (e) { } - } - }, 2000); // Wait 2 seconds before force kill - - return true; - } catch (error) { - return false; - } - } - return false; -} - -function isGeminiSessionActive(sessionId) { - return activeGeminiProcesses.has(sessionId); -} - -function getActiveGeminiSessions() { - return Array.from(activeGeminiProcesses.keys()); -} - -export { - spawnGemini, - abortGeminiSession, - isGeminiSessionActive, - getActiveGeminiSessions -}; diff --git a/server/gemini-response-handler.js b/server/gemini-response-handler.js deleted file mode 100644 index 6f314443..00000000 --- a/server/gemini-response-handler.js +++ /dev/null @@ -1,117 +0,0 @@ -// Gemini Response Handler - JSON Stream processing -import { sessionsService } from './modules/providers/services/sessions.service.js'; -import { createNormalizedMessage } from './shared/utils.js'; - -function buildGeminiTokenBudget(tokens) { - if (!tokens || typeof tokens !== 'object') { - return null; - } - - const parsedInputTokens = Number(tokens.input); - const parsedOutputTokens = Number(tokens.output); - const inputTokens = Number.isFinite(parsedInputTokens) ? parsedInputTokens : 0; - const outputTokens = Number.isFinite(parsedOutputTokens) ? parsedOutputTokens : 0; - const parsedUsed = Number(tokens.total); - const used = Number.isFinite(parsedUsed) ? parsedUsed : inputTokens + outputTokens; - if (!Number.isFinite(used) || used <= 0) { - return null; - } - - return { - used, - inputTokens, - outputTokens, - breakdown: { - input: inputTokens, - output: outputTokens, - }, - }; -} - -class GeminiResponseHandler { - constructor(ws, options = {}) { - this.ws = ws; - this.buffer = ''; - this.onContentFragment = options.onContentFragment || null; - this.onInit = options.onInit || null; - this.onToolUse = options.onToolUse || null; - this.onToolResult = options.onToolResult || null; - } - - // Process incoming raw data from Gemini stream-json - processData(data) { - this.buffer += data; - - // Split by newline - const lines = this.buffer.split('\n'); - - // Keep the last incomplete line in the buffer - this.buffer = lines.pop() || ''; - - for (const line of lines) { - if (!line.trim()) continue; - - try { - const event = JSON.parse(line); - this.handleEvent(event); - } catch (err) { - // Not a JSON line, probably debug output or CLI warnings - } - } - } - - handleEvent(event) { - const sid = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null; - - if (event.type === 'init') { - if (this.onInit) { - this.onInit(event); - } - return; - } - - // Invoke per-type callbacks for session tracking - if (event.type === 'message' && event.role === 'assistant') { - const content = event.content || ''; - if (this.onContentFragment && content) { - this.onContentFragment(content); - } - } else if (event.type === 'tool_use' && this.onToolUse) { - this.onToolUse(event); - } else if (event.type === 'tool_result' && this.onToolResult) { - this.onToolResult(event); - } - - // Normalize via adapter and send all resulting messages - const normalized = sessionsService.normalizeMessage('gemini', event, sid); - for (const msg of normalized) { - this.ws.send(msg); - } - - const tokenBudget = buildGeminiTokenBudget(event.tokens); - if (tokenBudget) { - this.ws.send(createNormalizedMessage({ - kind: 'status', - text: 'token_budget', - tokenBudget, - sessionId: sid, - provider: 'gemini', - })); - } - } - - forceFlush() { - if (this.buffer.trim()) { - try { - const event = JSON.parse(this.buffer); - this.handleEvent(event); - } catch (err) { } - } - } - - destroy() { - this.buffer = ''; - } -} - -export default GeminiResponseHandler; diff --git a/server/index.js b/server/index.js index c7cf322d..0832c490 100755 --- a/server/index.js +++ b/server/index.js @@ -35,15 +35,10 @@ import { queryCodex, abortCodexSession, } from './openai-codex.js'; -import { - spawnGemini, - abortGeminiSession, -} from './gemini-cli.js'; import { spawnOpenCode, abortOpenCodeSession, } from './opencode-cli.js'; -import sessionManager from './sessionManager.js'; import { stripAnsiSequences, normalizeDetectedUrl, @@ -61,7 +56,6 @@ import agentRoutes from './routes/agent.js'; import projectModuleRoutes from './modules/projects/projects.routes.js'; import notificationRoutes from './modules/notifications/notifications.routes.js'; import userRoutes from './routes/user.js'; -import geminiRoutes from './routes/gemini.js'; import pluginsRoutes from './routes/plugins.js'; import providerRoutes from './modules/providers/provider.routes.js'; import voiceRoutes from './voice-proxy.js'; @@ -119,14 +113,12 @@ const wss = createWebSocketServer(server, { claude: queryClaudeSDK, cursor: spawnCursor, codex: queryCodex, - gemini: spawnGemini, opencode: spawnOpenCode, }, abortFns: { claude: abortClaudeSDKSession, cursor: abortCursorSession, codex: abortCodexSession, - gemini: abortGeminiSession, opencode: abortOpenCodeSession, }, resolveToolApproval, @@ -135,14 +127,11 @@ const wss = createWebSocketServer(server, { shell: { resolveProviderSessionId: (sessionId, provider) => { const dbSession = sessionsDb.getSessionById(sessionId); - const legacyGeminiSession = - provider === 'gemini' ? sessionManager.getSession(sessionId) : null; - if (dbSession) { - return dbSession.provider_session_id ?? legacyGeminiSession?.cliSessionId ?? null; + return dbSession.provider_session_id ?? null; } - return legacyGeminiSession?.cliSessionId; + return null; }, stripAnsiSequences, normalizeDetectedUrl, @@ -214,9 +203,6 @@ app.use('/api/notifications', authenticateToken, notificationRoutes); // User API Routes (protected) app.use('/api/user', authenticateToken, userRoutes); -// Gemini API Routes (protected) -app.use('/api/gemini', authenticateToken, geminiRoutes); - // Plugins API Routes (protected) app.use('/api/plugins', authenticateToken, pluginsRoutes); @@ -1119,62 +1105,6 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate }); } - if (provider === 'gemini') { - const session = sessionsDb.getSessionById(safeSessionId); - const sessionFilePath = session?.jsonl_path; - if (!sessionFilePath) { - return res.json({ - used: 0, - inputTokens: 0, - outputTokens: 0, - breakdown: { input: 0, output: 0 }, - unsupported: true, - message: 'Token usage tracking not available for this Gemini session' - }); - } - - let fileContent; - try { - fileContent = await fsPromises.readFile(sessionFilePath, 'utf8'); - } catch (error) { - if (error.code === 'ENOENT') { - return res.status(404).json({ error: 'Session file not found', path: sessionFilePath }); - } - throw error; - } - - const lines = fileContent.trim().split('\n'); - let inputTokens = 0; - let outputTokens = 0; - let totalTokens = 0; - - for (let i = lines.length - 1; i >= 0; i--) { - try { - const entry = JSON.parse(lines[i]); - if (!entry.tokens || typeof entry.tokens !== 'object') { - continue; - } - - inputTokens = Number(entry.tokens.input || 0); - outputTokens = Number(entry.tokens.output || 0); - totalTokens = Number(entry.tokens.total || inputTokens + outputTokens || 0); - break; - } catch { - continue; - } - } - - return res.json({ - used: totalTokens, - inputTokens, - outputTokens, - breakdown: { - input: inputTokens, - output: outputTokens - } - }); - } - if (provider === 'opencode') { const dbPath = getOpenCodeDatabasePath(); if (!fs.existsSync(dbPath)) { diff --git a/server/modules/database/tests/sessions-provider-mapping.test.ts b/server/modules/database/tests/sessions-provider-mapping.test.ts index a9d91478..45e4a411 100644 --- a/server/modules/database/tests/sessions-provider-mapping.test.ts +++ b/server/modules/database/tests/sessions-provider-mapping.test.ts @@ -100,9 +100,9 @@ test('assignProviderSessionId merges a watcher-created duplicate into the app ro test('legacy provider-keyed rows stay resolvable through both lookups', async () => { await withIsolatedDatabase(() => { - sessionsDb.createSession('legacy-1', 'gemini', '/workspace/demo'); + sessionsDb.createSession('legacy-1', 'opencode', '/workspace/demo'); - assert.equal(sessionsDb.getSessionById('legacy-1')?.provider, 'gemini'); + assert.equal(sessionsDb.getSessionById('legacy-1')?.provider, 'opencode'); assert.equal(sessionsDb.getSessionByProviderSessionId('legacy-1')?.session_id, 'legacy-1'); }); }); diff --git a/server/modules/notifications/services/notification-orchestrator.service.js b/server/modules/notifications/services/notification-orchestrator.service.js index b9a1c6ee..ad700f1b 100644 --- a/server/modules/notifications/services/notification-orchestrator.service.js +++ b/server/modules/notifications/services/notification-orchestrator.service.js @@ -13,7 +13,6 @@ const PROVIDER_LABELS = { claude: 'Claude', cursor: 'Cursor', codex: 'Codex', - gemini: 'Gemini', system: 'System' }; diff --git a/server/modules/providers/README.md b/server/modules/providers/README.md index 32b862c8..a7f87ffa 100644 --- a/server/modules/providers/README.md +++ b/server/modules/providers/README.md @@ -36,7 +36,6 @@ Current provider ids in this repo are: - `claude` - `codex` - `cursor` -- `gemini` - `opencode` Those ids are mirrored in backend unions and frontend provider constants. If @@ -56,8 +55,7 @@ server/modules/providers/list// -session-synchronizer.provider.ts ``` -The existing provider folders are `claude`, `codex`, `cursor`, `gemini`, and -`opencode`. +The existing provider folders are `claude`, `codex`, `cursor`, and `opencode`. ## What Each Facet Does @@ -123,7 +121,6 @@ Current MCP formats in this repo are: | Claude | `.mcp.json` in user / local / project locations | `user`, `local`, `project` | `stdio`, `http`, `sse` | | Codex | `.codex/config.toml` | `user`, `project` | `stdio`, `http` | | Cursor | `.cursor/mcp.json` | `user`, `project` | `stdio`, `http` | -| Gemini | `.gemini/settings.json` | `user`, `project` | `stdio`, `http` | | OpenCode | `~/.config/opencode/opencode.json` or `/opencode.json` (`.jsonc` is read when present) | `user`, `project` | `stdio`, `http` | 5. Implement skills. @@ -144,7 +141,6 @@ Current skill discovery roots are: | Claude | `~/.claude/skills` | `/.claude/skills` | `/` | Also discovers Claude plugin skills from enabled plugin installs. Command skills live under `commands/`; markdown skills live under `skills/` and are scanned recursively. | | Codex | `~/.agents/skills`, `~/.codex/skills/.system`, `/etc/codex/skills` | `/.agents/skills`, `path.dirname(workspacePath)/.agents/skills`, topmost git root `.agents/skills` | `$` | Overlapping roots are deduplicated before scanning. | | Cursor | `~/.cursor/skills` | `/.cursor/skills`, `/.agents/skills` | `/` | Uses slash-style commands. | -| Gemini | `~/.gemini/skills`, `~/.agents/skills` | `/.gemini/skills`, `/.agents/skills` | `/` | Uses slash-style commands. | | OpenCode | `~/.config/opencode/skills`, `~/.claude/skills`, `~/.agents/skills` | Cwd-to-topmost-git-root `.opencode/skills`, `.claude/skills`, and `.agents/skills` | `/` | Reuses OpenCode, Claude, and Agents skill locations. Overlapping roots are deduplicated before scanning. | Command forms currently used by the providers are: @@ -153,7 +149,6 @@ Command forms currently used by the providers are: - Claude plugin skills: `/plugin-name:skill-name` - Codex skills: `$skill-name` - Cursor skills: `/skill-name` -- Gemini skills: `/skill-name` - OpenCode skills: `/skill-name` 6. Implement sessions. @@ -191,7 +186,6 @@ Current session sync roots are: | Claude | `~/.claude/projects/**/*.jsonl` | Uses `~/.claude/history.jsonl` for name lookup and the trailing `ai-title`, `last-prompt`, or `custom-title` entries for title recovery. | | Codex | `~/.codex/sessions/**/*.jsonl` | Uses `~/.codex/session_index.jsonl` for title lookup and the last `task_complete` message for a fallback title. | | Cursor | `~/.cursor/projects/**/*.jsonl` | Uses sibling `worker.log` to recover `workspacePath`, then derives the session title from the first user prompt. | -| Gemini | `~/.gemini/tmp/**/*.jsonl` | Current full scans only index temp JSONL chat artifacts. Single-file sync also accepts legacy `.json` files. | | OpenCode | `~/.local/share/opencode/opencode.db` | Reads active sessions/messages/parts from OpenCode's shared SQLite database and stores `jsonl_path` as `null` so deleting one app session cannot remove the shared DB. | 8. Register the provider. diff --git a/server/modules/providers/list/cursor/cursor-models.provider.ts b/server/modules/providers/list/cursor/cursor-models.provider.ts index bf2c4881..78105db3 100644 --- a/server/modules/providers/list/cursor/cursor-models.provider.ts +++ b/server/modules/providers/list/cursor/cursor-models.provider.ts @@ -445,11 +445,6 @@ export const CURSOR_FALLBACK_MODELS: ProviderModelsDefinition = { label: "gpt-5.2-xhigh-fast", description: "GPT-5.2 Extra High Fast", }, - { - value: "gemini-3.1-pro", - label: "gemini-3.1-pro", - description: "Gemini 3.1 Pro", - }, { value: "gpt-5.4-mini-none", label: "gpt-5.4-mini-none", @@ -530,16 +525,6 @@ export const CURSOR_FALLBACK_MODELS: ProviderModelsDefinition = { label: "gpt-5.1-high", description: "GPT-5.1 High", }, - { - value: "gemini-3-flash", - label: "gemini-3-flash", - description: "Gemini 3 Flash", - }, - { - value: "gemini-3.5-flash", - label: "gemini-3.5-flash", - description: "Gemini 3.5 Flash", - }, { value: "gpt-5.1-codex-mini-low", label: "gpt-5.1-codex-mini-low", diff --git a/server/modules/providers/list/gemini/gemini-auth.provider.ts b/server/modules/providers/list/gemini/gemini-auth.provider.ts deleted file mode 100644 index c6897e74..00000000 --- a/server/modules/providers/list/gemini/gemini-auth.provider.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { readFile } from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; - -import spawn from 'cross-spawn'; - -import type { IProviderAuth } from '@/shared/interfaces.js'; -import type { ProviderAuthStatus } from '@/shared/types.js'; -import { readObjectRecord, readOptionalString } from '@/shared/utils.js'; - -type GeminiCredentialsStatus = { - authenticated: boolean; - email: string | null; - method: string | null; - error?: string; -}; - -type GeminiAuthType = - | 'oauth-personal' - | 'gemini-api-key' - | 'vertex-ai' - | 'compute-default-credentials' - | 'gateway' - | 'cloud-shell' - | null; - -export class GeminiProviderAuth implements IProviderAuth { - /** - * Gemini CLI can override its home root via GEMINI_CLI_HOME. - * Use the same resolution so status checks match runtime behavior. - */ - private getGeminiCliHome(): string { - return process.env.GEMINI_CLI_HOME?.trim() || os.homedir(); - } - - /** - * Checks whether the Gemini CLI is available on this host. - */ - private checkInstalled(): boolean { - const cliPath = process.env.GEMINI_PATH || 'gemini'; - try { - spawn.sync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 }); - return true; - } catch { - return false; - } - } - - /** - * Returns Gemini CLI installation and credential status. - */ - async getStatus(): Promise { - const installed = this.checkInstalled(); - - if (!installed) { - return { - installed, - provider: 'gemini', - authenticated: false, - email: null, - method: null, - error: 'Gemini CLI is not installed', - }; - } - - const credentials = await this.checkCredentials(); - - return { - installed, - provider: 'gemini', - authenticated: credentials.authenticated, - email: credentials.email, - method: credentials.method, - error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated', - }; - } - - /** - * Parses dotenv-style key/value pairs. - */ - private parseEnvFile(content: string): Record { - const parsed: Record = {}; - - for (const rawLine of content.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line || line.startsWith('#')) { - continue; - } - - const normalizedLine = line.startsWith('export ') - ? line.slice('export '.length).trim() - : line; - const separatorIndex = normalizedLine.indexOf('='); - if (separatorIndex <= 0) { - continue; - } - - const key = normalizedLine.slice(0, separatorIndex).trim(); - if (!key) { - continue; - } - - let value = normalizedLine.slice(separatorIndex + 1).trim(); - const quoted = (value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\'')); - if (quoted) { - value = value.slice(1, -1); - } else { - value = value.replace(/\s+#.*$/, '').trim(); - } - - parsed[key] = value; - } - - return parsed; - } - - /** - * Loads user-level auth env in Gemini's "first file found" order. - */ - private async loadUserLevelAuthEnv(): Promise> { - const geminiCliHome = this.getGeminiCliHome(); - const envCandidates = [ - path.join(geminiCliHome, '.gemini', '.env'), - path.join(geminiCliHome, '.env'), - ]; - - for (const envPath of envCandidates) { - try { - const content = await readFile(envPath, 'utf8'); - return this.parseEnvFile(content); - } catch { - // Continue to the next fallback. - } - } - - return {}; - } - - /** - * Reads Gemini's selected auth type from settings.json when available. - */ - private async readSelectedAuthType(): Promise { - try { - const settingsPath = path.join(this.getGeminiCliHome(), '.gemini', 'settings.json'); - const content = await readFile(settingsPath, 'utf8'); - const settings = readObjectRecord(JSON.parse(content)); - const security = readObjectRecord(settings?.security); - const auth = readObjectRecord(security?.auth); - const selectedType = readOptionalString(auth?.selectedType); - if (!selectedType) { - return null; - } - - return selectedType as GeminiAuthType; - } catch { - return null; - } - } - - /** - * Checks Gemini credentials from API key env vars or local OAuth credential files. - */ - private async checkCredentials(): Promise { - if (process.env.GEMINI_API_KEY?.trim()) { - return { authenticated: true, email: 'API Key Auth', method: 'api_key' }; - } - - const userEnv = await this.loadUserLevelAuthEnv(); - if (readOptionalString(userEnv.GEMINI_API_KEY)) { - return { authenticated: true, email: 'API Key Auth', method: 'api_key' }; - } - - const selectedType = await this.readSelectedAuthType(); - if (selectedType === 'vertex-ai') { - const hasGoogleApiKey = Boolean( - process.env.GOOGLE_API_KEY?.trim() - || readOptionalString(userEnv.GOOGLE_API_KEY) - ); - const hasProject = Boolean( - process.env.GOOGLE_CLOUD_PROJECT?.trim() - || process.env.GOOGLE_CLOUD_PROJECT_ID?.trim() - || readOptionalString(userEnv.GOOGLE_CLOUD_PROJECT) - || readOptionalString(userEnv.GOOGLE_CLOUD_PROJECT_ID) - ); - const hasLocation = Boolean( - process.env.GOOGLE_CLOUD_LOCATION?.trim() - || readOptionalString(userEnv.GOOGLE_CLOUD_LOCATION) - ); - const hasServiceAccount = Boolean( - process.env.GOOGLE_APPLICATION_CREDENTIALS?.trim() - || readOptionalString(userEnv.GOOGLE_APPLICATION_CREDENTIALS) - ); - - if (hasGoogleApiKey || hasServiceAccount || (hasProject && hasLocation)) { - return { authenticated: true, email: 'Vertex AI Auth', method: 'vertex_ai' }; - } - - return { - authenticated: false, - email: null, - method: 'vertex_ai', - error: 'Gemini is set to Vertex AI, but required env vars are missing', - }; - } - - try { - const credsPath = path.join(this.getGeminiCliHome(), '.gemini', 'oauth_creds.json'); - const content = await readFile(credsPath, 'utf8'); - const creds = readObjectRecord(JSON.parse(content)) ?? {}; - const accessToken = readOptionalString(creds.access_token); - - if (!accessToken) { - return { - authenticated: false, - email: null, - method: null, - error: 'No valid tokens found in oauth_creds', - }; - } - - const refreshToken = readOptionalString(creds.refresh_token); - const tokenInfo = await this.getTokenInfoEmail(accessToken); - if (tokenInfo.valid) { - return { - authenticated: true, - email: tokenInfo.email || 'OAuth Session', - method: 'credentials_file', - }; - } - - if (!refreshToken) { - return { - authenticated: false, - email: null, - method: 'credentials_file', - error: 'Access token invalid and no refresh token found', - }; - } - - return { - authenticated: true, - email: await this.getActiveAccountEmail() || 'OAuth Session', - method: 'credentials_file', - }; - } catch { - if (selectedType === 'gemini-api-key') { - return { - authenticated: false, - email: null, - method: 'api_key', - error: 'Gemini is set to "Use Gemini API key", but GEMINI_API_KEY is unavailable', - }; - } - - if (selectedType === 'oauth-personal') { - return { - authenticated: false, - email: null, - method: 'credentials_file', - error: 'Gemini is set to Google sign-in, but no cached OAuth credentials were found', - }; - } - - // If no explicit auth type was selected, surface the generic "not configured" error. - return { - authenticated: false, - email: null, - method: null, - error: 'Gemini CLI not configured', - }; - } - } - - /** - * Validates a Gemini OAuth access token and returns an email when Google reports one. - */ - private async getTokenInfoEmail(accessToken: string): Promise<{ valid: boolean; email: string | null }> { - try { - const tokenRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${accessToken}`); - if (!tokenRes.ok) { - return { valid: false, email: null }; - } - - const tokenInfo = readObjectRecord(await tokenRes.json()); - return { - valid: true, - email: readOptionalString(tokenInfo?.email) ?? null, - }; - } catch { - return { valid: false, email: null }; - } - } - - /** - * Reads Gemini's active local Google account as an offline fallback for display. - */ - private async getActiveAccountEmail(): Promise { - try { - const accPath = path.join(this.getGeminiCliHome(), '.gemini', 'google_accounts.json'); - const accContent = await readFile(accPath, 'utf8'); - const accounts = readObjectRecord(JSON.parse(accContent)); - return readOptionalString(accounts?.active) ?? null; - } catch { - return null; - } - } -} diff --git a/server/modules/providers/list/gemini/gemini-mcp.provider.ts b/server/modules/providers/list/gemini/gemini-mcp.provider.ts deleted file mode 100644 index b86b8f2d..00000000 --- a/server/modules/providers/list/gemini/gemini-mcp.provider.ts +++ /dev/null @@ -1,110 +0,0 @@ -import os from 'node:os'; -import path from 'node:path'; - -import { McpProvider } from '@/modules/providers/shared/mcp/mcp.provider.js'; -import type { McpScope, ProviderMcpServer, UpsertProviderMcpServerInput } from '@/shared/types.js'; -import { - AppError, - readJsonConfig, - readObjectRecord, - readOptionalString, - readStringArray, - readStringRecord, - writeJsonConfig, -} from '@/shared/utils.js'; - -export class GeminiMcpProvider extends McpProvider { - constructor() { - super('gemini', ['user', 'project'], ['stdio', 'http', 'sse']); - } - - protected async readScopedServers(scope: McpScope, workspacePath: string): Promise> { - const filePath = scope === 'user' - ? path.join(os.homedir(), '.gemini', 'settings.json') - : path.join(workspacePath, '.gemini', 'settings.json'); - const config = await readJsonConfig(filePath); - return readObjectRecord(config.mcpServers) ?? {}; - } - - protected async writeScopedServers( - scope: McpScope, - workspacePath: string, - servers: Record, - ): Promise { - const filePath = scope === 'user' - ? path.join(os.homedir(), '.gemini', 'settings.json') - : path.join(workspacePath, '.gemini', 'settings.json'); - const config = await readJsonConfig(filePath); - config.mcpServers = servers; - await writeJsonConfig(filePath, config); - } - - protected buildServerConfig(input: UpsertProviderMcpServerInput): Record { - if (input.transport === 'stdio') { - if (!input.command?.trim()) { - throw new AppError('command is required for stdio MCP servers.', { - code: 'MCP_COMMAND_REQUIRED', - statusCode: 400, - }); - } - - return { - command: input.command, - args: input.args ?? [], - env: input.env ?? {}, - cwd: input.cwd, - }; - } - - if (!input.url?.trim()) { - throw new AppError('url is required for http/sse MCP servers.', { - code: 'MCP_URL_REQUIRED', - statusCode: 400, - }); - } - - return { - type: input.transport, - url: input.url, - headers: input.headers ?? {}, - }; - } - - protected normalizeServerConfig( - scope: McpScope, - name: string, - rawConfig: unknown, - ): ProviderMcpServer | null { - if (!rawConfig || typeof rawConfig !== 'object') { - return null; - } - - const config = rawConfig as Record; - if (typeof config.command === 'string') { - return { - provider: 'gemini', - name, - scope, - transport: 'stdio', - command: config.command, - args: readStringArray(config.args), - env: readStringRecord(config.env), - cwd: readOptionalString(config.cwd), - }; - } - - if (typeof config.url === 'string') { - const transport = readOptionalString(config.type) === 'sse' ? 'sse' : 'http'; - return { - provider: 'gemini', - name, - scope, - transport, - url: config.url, - headers: readStringRecord(config.headers), - }; - } - - return null; - } -} diff --git a/server/modules/providers/list/gemini/gemini-models.provider.ts b/server/modules/providers/list/gemini/gemini-models.provider.ts deleted file mode 100644 index d59612cd..00000000 --- a/server/modules/providers/list/gemini/gemini-models.provider.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { IProviderModels } from '@/shared/interfaces.js'; -import type { - ProviderChangeActiveModelInput, - ProviderCurrentActiveModel, - ProviderModelsDefinition, - ProviderSessionActiveModelChange, -} from '@/shared/types.js'; -import { - buildDefaultProviderCurrentActiveModel, - writeProviderSessionActiveModelChange, -} from '@/shared/utils.js'; - -export const GEMINI_FALLBACK_MODELS: ProviderModelsDefinition = { - OPTIONS: [ - { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' }, - { value: 'gemini-3.1-flash-lite-preview', label: 'Gemini 3.1 Flash Lite Preview' }, - { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, - { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' }, - { value: 'gemma-4-31b-it', label: 'Gemma 4 31B IT' }, - { value: 'gemma-4-26b-a4b-it', label: 'Gemma 4 26B A4B IT' }, - ], - DEFAULT: 'gemini-3-flash-preview', -}; - -export class GeminiProviderModels implements IProviderModels { - async getSupportedModels(): Promise { - return GEMINI_FALLBACK_MODELS; - } - - async getCurrentActiveModel(): Promise { - return buildDefaultProviderCurrentActiveModel(GEMINI_FALLBACK_MODELS); - } - - async changeActiveModel( - input: ProviderChangeActiveModelInput, - ): Promise { - return writeProviderSessionActiveModelChange('gemini', input); - } -} diff --git a/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts b/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts deleted file mode 100644 index 7ec3eff9..00000000 --- a/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +++ /dev/null @@ -1,405 +0,0 @@ -import crypto from 'node:crypto'; -import os from 'node:os'; -import path from 'node:path'; -import { readFile } from 'node:fs/promises'; - -import { projectsDb, sessionsDb } from '@/modules/database/index.js'; -import { - findFilesRecursivelyCreatedAfter, - normalizeProjectPath, - normalizeSessionName, - readFileTimestamps, -} from '@/shared/utils.js'; -import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js'; -import type { AnyRecord } from '@/shared/types.js'; - -type ParsedSession = { - sessionId: string; - projectPath: string; - sessionName?: string; -}; - -type GeminiJsonlMetadata = { - sessionId: string; - projectPath?: string; - projectHash?: string; - firstUserMessage?: string; -}; - -/** - * Session indexer for Gemini transcript artifacts. - */ -export class GeminiSessionSynchronizer implements IProviderSessionSynchronizer { - private readonly provider = 'gemini' as const; - private readonly geminiHome = path.join(os.homedir(), '.gemini'); - - /** - * Scans Gemini legacy JSON and new JSONL artifacts and upserts sessions into DB. - */ - async synchronize(since?: Date): Promise { - const projectHashLookup = this.buildProjectHashLookup(); - - // const legacySessionFiles = await findFilesRecursivelyCreatedAfter( - // path.join(this.geminiHome, 'sessions'), - // '.json', - // since ?? null - // ); - // Gemini creates overlapping artifacts across `sessions/` and `tmp/`. - // We currently index only `tmp/*/chats/*.jsonl` because those files are the - // live transcript source and avoid duplicate session rows from mirrored files. - // const legacyTempFiles = await findFilesRecursivelyCreatedAfter( - // path.join(this.geminiHome, 'tmp'), - // '.json', - // since ?? null - // ); - // const jsonlSessionFiles = await findFilesRecursivelyCreatedAfter( - // path.join(this.geminiHome, 'sessions'), - // '.jsonl', - // since ?? null - // ); - const jsonlTempFiles = await findFilesRecursivelyCreatedAfter( - path.join(this.geminiHome, 'tmp'), - '.jsonl', - since ?? null - ); - - // Current strategy: index only temp chat JSONL artifacts. - const files = [ - // ...legacySessionFiles, - // Intentionally disabled to avoid duplicate indexing from mirrored - // `sessions/*.json` and `sessions/*.jsonl` artifacts. - // ...legacyTempFiles, - // ...jsonlSessionFiles, - ...jsonlTempFiles, - ]; - - let processed = 0; - for (const filePath of files) { - if (this.shouldSkipTempArtifact(filePath)) { - continue; - } - - const parsed = filePath.endsWith('.jsonl') - ? await this.processJsonlSessionFile(filePath, projectHashLookup) - : await this.processLegacySessionFile(filePath); - if (!parsed) { - continue; - } - - const timestamps = await readFileTimestamps(filePath); - sessionsDb.createSession( - parsed.sessionId, - this.provider, - parsed.projectPath, - parsed.sessionName, - timestamps.createdAt, - timestamps.updatedAt, - filePath - ); - processed += 1; - } - - return processed; - } - - /** - * Parses and upserts one Gemini legacy JSON or JSONL artifact. - */ - async synchronizeFile(filePath: string): Promise { - if (!filePath.endsWith('.json') && !filePath.endsWith('.jsonl')) { - return null; - } - - if (this.shouldSkipTempArtifact(filePath)) { - return null; - } - - const parsed = filePath.endsWith('.jsonl') - ? await this.processJsonlSessionFile(filePath, this.buildProjectHashLookup()) - : await this.processLegacySessionFile(filePath); - if (!parsed) { - return null; - } - - const timestamps = await readFileTimestamps(filePath); - return sessionsDb.createSession( - parsed.sessionId, - this.provider, - parsed.projectPath, - parsed.sessionName, - timestamps.createdAt, - timestamps.updatedAt, - filePath - ); - } - - /** - * Extracts session metadata from one Gemini legacy JSON artifact. - */ - private async processLegacySessionFile(filePath: string): Promise { - try { - const content = await readFile(filePath, 'utf8'); - const data = JSON.parse(content) as AnyRecord; - - const sessionId = - typeof data.sessionId === 'string' - ? data.sessionId - : typeof data.id === 'string' - ? data.id - : undefined; - if (!sessionId) { - return null; - } - - const workspaceProjectPath = await this.resolveProjectPathFromChatWorkspace(filePath); - const projectPath = typeof data.projectPath === 'string' && data.projectPath.trim().length > 0 - ? data.projectPath - : workspaceProjectPath; - if (!projectPath) { - return null; - } - - const messages = Array.isArray(data.messages) ? data.messages : []; - const firstMessage = messages[0] as AnyRecord | undefined; - let rawName: string | undefined; - - if (Array.isArray(firstMessage?.content) && typeof firstMessage.content[0]?.text === 'string') { - rawName = firstMessage.content[0].text; - } else if (typeof firstMessage?.content === 'string') { - rawName = firstMessage.content; - } - - return { - sessionId, - projectPath, - sessionName: normalizeSessionName(rawName, 'New Gemini Chat'), - }; - } catch { - return null; - } - } - - /** - * Extracts session metadata from one Gemini JSONL artifact. - */ - private async processJsonlSessionFile( - filePath: string, - projectHashLookup: Map - ): Promise { - const metadata = await this.extractJsonlMetadata(filePath); - if (!metadata) { - return null; - } - - let projectPath = typeof metadata.projectPath === 'string' ? metadata.projectPath.trim() : ''; - if (!projectPath) { - const workspaceProjectPath = await this.resolveProjectPathFromChatWorkspace(filePath); - if (workspaceProjectPath) { - projectPath = workspaceProjectPath; - } - } - if (!projectPath && typeof metadata.projectHash === 'string') { - projectPath = projectHashLookup.get(metadata.projectHash.trim().toLowerCase()) ?? ''; - } - if (!projectPath) { - return null; - } - - // Once we resolve a project hash/path pair, keep it in-memory for this sync run. - if (typeof metadata.projectHash === 'string' && metadata.projectHash.trim()) { - projectHashLookup.set(metadata.projectHash.trim().toLowerCase(), projectPath); - } - - return { - sessionId: metadata.sessionId, - projectPath, - sessionName: normalizeSessionName(metadata.firstUserMessage, 'New Gemini Chat'), - }; - } - - /** - * Reads first useful metadata from Gemini JSONL files. - */ - private async extractJsonlMetadata(filePath: string): Promise { - try { - const content = await readFile(filePath, 'utf8'); - const lines = content.split('\n'); - - let sessionId: string | undefined; - let projectPath: string | undefined; - let projectHash: string | undefined; - let firstUserMessage: string | undefined; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) { - continue; - } - - let parsed: AnyRecord; - try { - parsed = JSON.parse(trimmed) as AnyRecord; - } catch { - continue; - } - - if (!sessionId && typeof parsed.sessionId === 'string') { - sessionId = parsed.sessionId; - } - if (!projectPath && typeof parsed.projectPath === 'string') { - projectPath = parsed.projectPath; - } - if (!projectHash && typeof parsed.projectHash === 'string') { - projectHash = parsed.projectHash; - } - - if (!firstUserMessage && parsed.type === 'user') { - firstUserMessage = this.extractGeminiTextContent(parsed.content); - } - - if (sessionId && (projectPath || projectHash) && firstUserMessage) { - break; - } - } - - if (!sessionId) { - return null; - } - - return { - sessionId, - projectPath, - projectHash, - firstUserMessage, - }; - } catch { - return null; - } - } - - /** - * Tries to resolve project root from Gemini tmp chat workspaces. - */ - private async resolveProjectPathFromChatWorkspace(filePath: string): Promise { - if (!filePath.includes(`${path.sep}chats${path.sep}`)) { - return ''; - } - - const chatsDir = path.dirname(filePath); - const workspaceDir = path.dirname(chatsDir); - const projectRootPath = path.join(workspaceDir, '.project_root'); - - try { - const rootContent = await readFile(projectRootPath, 'utf8'); - return rootContent.trim(); - } catch { - return ''; - } - } - - /** - * Builds a hash->path lookup for Gemini JSONL metadata that stores projectHash. - */ - private buildProjectHashLookup(): Map { - const lookup = new Map(); - const knownPaths = new Set(); - - for (const project of projectsDb.getProjectPaths()) { - if (typeof project.project_path === 'string' && project.project_path.trim()) { - knownPaths.add(project.project_path.trim()); - } - } - - for (const session of sessionsDb.getAllSessions()) { - if (session.provider === this.provider && typeof session.project_path === 'string' && session.project_path.trim()) { - knownPaths.add(session.project_path.trim()); - } - } - - for (const knownPath of knownPaths) { - this.addProjectHashCandidates(lookup, knownPath); - } - - return lookup; - } - - /** - * Adds likely Gemini hash variants for one project path. - */ - private addProjectHashCandidates(lookup: Map, projectPath: string): void { - const trimmed = projectPath.trim(); - if (!trimmed) { - return; - } - - const normalized = normalizeProjectPath(trimmed); - const resolved = path.resolve(trimmed); - const resolvedNormalized = normalizeProjectPath(resolved); - - const candidates = new Set([ - trimmed, - normalized, - resolved, - resolvedNormalized, - ]); - - if (process.platform === 'win32') { - for (const candidate of [...candidates]) { - candidates.add(candidate.toLowerCase()); - } - } - - for (const candidate of candidates) { - if (!candidate) { - continue; - } - - const hash = this.sha256(candidate); - if (!lookup.has(hash)) { - lookup.set(hash, trimmed); - } - } - } - - /** - * Returns first user text from Gemini content payload shapes. - */ - private extractGeminiTextContent(content: unknown): string | undefined { - if (typeof content === 'string' && content.trim().length > 0) { - return content; - } - - if (!Array.isArray(content)) { - return undefined; - } - - for (const part of content) { - if (typeof part === 'string' && part.trim().length > 0) { - return part; - } - - if (part && typeof part === 'object' && typeof (part as AnyRecord).text === 'string') { - const text = (part as AnyRecord).text; - if (text.trim().length > 0) { - return text; - } - } - } - - return undefined; - } - - /** - * Keeps tmp scanning scoped to chat artifacts only. - */ - private shouldSkipTempArtifact(filePath: string): boolean { - return ( - filePath.startsWith(path.join(this.geminiHome, 'tmp')) - && !filePath.includes(`${path.sep}chats${path.sep}`) - ); - } - - private sha256(value: string): string { - return crypto.createHash('sha256').update(value).digest('hex'); - } -} diff --git a/server/modules/providers/list/gemini/gemini-sessions.provider.ts b/server/modules/providers/list/gemini/gemini-sessions.provider.ts deleted file mode 100644 index 3554ee1b..00000000 --- a/server/modules/providers/list/gemini/gemini-sessions.provider.ts +++ /dev/null @@ -1,555 +0,0 @@ -import fsSync from 'node:fs'; -import fs from 'node:fs/promises'; -import readline from 'node:readline'; - -import { sessionsDb } from '@/modules/database/index.js'; -import { parseImagesInputTag } from '@/shared/image-attachments.js'; -import type { IProviderSessions } from '@/shared/interfaces.js'; -import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js'; -import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage } from '@/shared/utils.js'; - -const PROVIDER = 'gemini'; - -type GeminiHistoryResult = { - messages: AnyRecord[]; - tokenUsage?: unknown; -}; - -function mapGeminiRole(value: unknown): 'user' | 'assistant' | null { - if (value === 'user') { - return 'user'; - } - - if (value === 'gemini' || value === 'assistant') { - return 'assistant'; - } - - return null; -} - -function extractGeminiTextContent(content: unknown): string { - if (typeof content === 'string') { - return content; - } - - if (!Array.isArray(content)) { - return ''; - } - - return content - .map((part) => { - if (typeof part === 'string') { - return part; - } - if (!part || typeof part !== 'object') { - return ''; - } - - const record = part as AnyRecord; - if (typeof record.text === 'string') { - return record.text; - } - - return ''; - }) - .filter(Boolean) - .join('\n'); -} - -function extractGeminiThoughts(thoughts: unknown): string { - if (!Array.isArray(thoughts)) { - return ''; - } - - return thoughts - .map((item) => { - if (!item || typeof item !== 'object') { - return ''; - } - - const record = item as AnyRecord; - const subject = typeof record.subject === 'string' ? record.subject.trim() : ''; - const description = typeof record.description === 'string' ? record.description.trim() : ''; - - if (subject && description) { - return `${subject}: ${description}`; - } - - return description || subject; - }) - .filter(Boolean) - .join('\n'); -} - -function buildGeminiTokenUsage(tokens: unknown): AnyRecord | undefined { - if (!tokens || typeof tokens !== 'object') { - return undefined; - } - - const record = tokens as AnyRecord; - const input = Number(record.input || 0); - const output = Number(record.output || 0); - const total = Number(record.total || input + output || 0); - - return { - used: total, - inputTokens: input, - outputTokens: output, - breakdown: { - input, - output, - }, - }; -} - -async function getGeminiLegacySessionMessages(sessionFilePath: string): Promise { - try { - const data = await fs.readFile(sessionFilePath, 'utf8'); - const session = JSON.parse(data) as AnyRecord; - const sourceMessages = Array.isArray(session.messages) ? session.messages : []; - - const messages: AnyRecord[] = []; - for (const msg of sourceMessages) { - const role = mapGeminiRole(msg.type ?? msg.role); - if (!role) { - continue; - } - - messages.push({ - type: 'message', - uuid: typeof msg.id === 'string' ? msg.id : undefined, - message: { role, content: msg.content }, - timestamp: msg.timestamp || null, - }); - } - - return { messages }; - } catch { - return { messages: [] }; - } -} - -// Exported for tests: parses one Gemini CLI JSONL transcript from disk. -export async function getGeminiJsonlSessionMessages(sessionFilePath: string): Promise { - const messages: AnyRecord[] = []; - let tokenUsage: AnyRecord | undefined; - - try { - const fileStream = fsSync.createReadStream(sessionFilePath); - const lineReader = readline.createInterface({ - input: fileStream, - crlfDelay: Infinity, - }); - - for await (const line of lineReader) { - const trimmed = line.trim(); - if (!trimmed) { - continue; - } - - let entry: AnyRecord; - try { - entry = JSON.parse(trimmed) as AnyRecord; - } catch { - continue; - } - - // Metadata/update lines (e.g. {$set:{lastUpdated:...}}) do not represent chat messages. - if (entry.$set) { - continue; - } - - const role = mapGeminiRole(entry.type); - if (role) { - const rawTextContent = extractGeminiTextContent(entry.content); - // User prompts sent with attachments carry an path - // list; strip it for display and surface the paths as images. - const { text: textContent, attachments } = role === 'user' - ? parseImagesInputTag(rawTextContent) - : { text: rawTextContent, attachments: [] }; - if (textContent.trim() || attachments.length > 0) { - messages.push({ - type: 'message', - uuid: typeof entry.id === 'string' ? entry.id : undefined, - message: { role, content: textContent }, - images: attachments.length > 0 ? attachments : undefined, - timestamp: entry.timestamp || null, - }); - } - - const thinkingContent = extractGeminiThoughts(entry.thoughts); - if (thinkingContent.trim()) { - messages.push({ - type: 'thinking', - uuid: typeof entry.id === 'string' ? `${entry.id}_thinking` : undefined, - message: { role: 'assistant', content: thinkingContent }, - timestamp: entry.timestamp || null, - isReasoning: true, - }); - } - - if (role === 'assistant') { - const usage = buildGeminiTokenUsage(entry.tokens); - if (usage) { - tokenUsage = usage; - } - } - - continue; - } - - if (entry.type === 'tool_use') { - messages.push({ - type: 'tool_use', - uuid: typeof entry.id === 'string' ? entry.id : undefined, - timestamp: entry.timestamp || null, - toolName: entry.tool_name || entry.name || 'Tool', - toolInput: entry.parameters ?? entry.input ?? entry.arguments ?? '', - toolCallId: entry.tool_id || entry.toolCallId || entry.id, - }); - continue; - } - - if (entry.type === 'tool_result') { - messages.push({ - type: 'tool_result', - uuid: typeof entry.id === 'string' ? entry.id : undefined, - timestamp: entry.timestamp || null, - toolCallId: entry.tool_id || entry.toolCallId || entry.id || '', - output: entry.output ?? entry.result ?? '', - isError: Boolean(entry.error) || entry.status === 'error', - }); - } - } - } catch { - return { messages: [] }; - } - - messages.sort( - (a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(), - ); - - return { messages, tokenUsage }; -} - -async function getGeminiCliSessionMessages(sessionId: string): Promise { - const sessionFilePath = sessionsDb.getSessionById(sessionId)?.jsonl_path; - if (!sessionFilePath) { - return { messages: [] }; - } - - if (sessionFilePath.endsWith('.jsonl')) { - return getGeminiJsonlSessionMessages(sessionFilePath); - } - - return getGeminiLegacySessionMessages(sessionFilePath); -} - -export class GeminiSessionsProvider implements IProviderSessions { - /** - * Normalizes live Gemini stream-json events into the shared message shape. - * - * Gemini history uses a different session file shape, so fetchHistory handles - * that separately after loading raw persisted messages. - */ - normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] { - const raw = readObjectRecord(rawMessage); - if (!raw) { - return []; - } - - const ts = raw.timestamp || new Date().toISOString(); - const baseId = raw.uuid || generateMessageId('gemini'); - - if (raw.type === 'message' && raw.role === 'assistant') { - const content = raw.content || ''; - const messages: NormalizedMessage[] = []; - if (content) { - messages.push(createNormalizedMessage({ - id: baseId, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'stream_delta', - content, - })); - } - if (raw.delta !== true) { - messages.push(createNormalizedMessage({ - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'stream_end', - })); - } - return messages; - } - - if (raw.type === 'tool_use') { - return [createNormalizedMessage({ - id: baseId, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'tool_use', - toolName: raw.tool_name, - toolInput: raw.parameters || {}, - toolId: raw.tool_id || baseId, - })]; - } - - if (raw.type === 'tool_result') { - return [createNormalizedMessage({ - id: baseId, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'tool_result', - toolId: raw.tool_id || '', - content: raw.output === undefined ? '' : String(raw.output), - isError: raw.status === 'error', - })]; - } - - if (raw.type === 'result') { - const messages = [createNormalizedMessage({ - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'stream_end', - })]; - if (raw.stats?.total_tokens) { - messages.push(createNormalizedMessage({ - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'status', - text: 'Complete', - tokens: raw.stats.total_tokens, - canInterrupt: false, - })); - } - return messages; - } - - if (raw.type === 'error') { - return [createNormalizedMessage({ - id: baseId, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'error', - content: raw.error || raw.message || 'Unknown Gemini streaming error', - })]; - } - - return []; - } - - /** - * Loads Gemini history from Gemini CLI session files on disk. - */ - async fetchHistory( - sessionId: string, - options: FetchHistoryOptions = {}, - ): Promise { - const { limit = null, offset = 0 } = options; - - let result: GeminiHistoryResult; - try { - result = await getGeminiCliSessionMessages(sessionId); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn(`[GeminiProvider] Failed to load session ${sessionId}:`, message); - return { messages: [], total: 0, hasMore: false, offset: 0, limit: null }; - } - - const rawMessages = result.messages; - const normalized: NormalizedMessage[] = []; - - for (let i = 0; i < rawMessages.length; i++) { - const raw = rawMessages[i]; - const ts = raw.timestamp || new Date().toISOString(); - const baseId = raw.uuid || generateMessageId('gemini'); - - if (raw.type === 'thinking' || raw.isReasoning) { - const thinkingContent = typeof raw.message?.content === 'string' - ? raw.message.content - : typeof raw.content === 'string' - ? raw.content - : ''; - - if (thinkingContent.trim()) { - normalized.push(createNormalizedMessage({ - id: baseId, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'thinking', - content: thinkingContent, - })); - } - continue; - } - - if (raw.type === 'tool_use' || raw.toolName) { - normalized.push(createNormalizedMessage({ - id: baseId, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'tool_use', - toolName: raw.toolName || 'Tool', - toolInput: raw.toolInput, - toolId: raw.toolCallId || baseId, - })); - continue; - } - - if (raw.type === 'tool_result') { - normalized.push(createNormalizedMessage({ - id: baseId, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'tool_result', - toolId: raw.toolCallId || '', - content: raw.output === undefined ? '' : String(raw.output), - isError: Boolean(raw.isError), - })); - continue; - } - - const role = raw.message?.role || raw.role; - const content = raw.message?.content ?? raw.content ?? ''; - const rawImages = Array.isArray(raw.images) && raw.images.length > 0 ? raw.images : undefined; - if (!role || (!content && !rawImages)) { - continue; - } - - const normalizedRole = role === 'user' ? 'user' : 'assistant'; - - if (Array.isArray(content)) { - for (let partIdx = 0; partIdx < content.length; partIdx++) { - const part = content[partIdx] as AnyRecord | string; - - if (typeof part === 'string' && part.trim()) { - normalized.push(createNormalizedMessage({ - id: `${baseId}_${partIdx}`, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'text', - role: normalizedRole, - content: part, - })); - continue; - } - - if (!part || typeof part !== 'object') { - continue; - } - - if ((part.type === 'text' || !part.type) && typeof part.text === 'string' && part.text.trim()) { - normalized.push(createNormalizedMessage({ - id: `${baseId}_${partIdx}`, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'text', - role: normalizedRole, - content: part.text, - })); - } else if (part.type === 'tool_use') { - normalized.push(createNormalizedMessage({ - id: `${baseId}_${partIdx}`, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'tool_use', - toolName: part.name, - toolInput: part.input, - toolId: part.id || generateMessageId('gemini_tool'), - })); - } else if (part.type === 'tool_result') { - normalized.push(createNormalizedMessage({ - id: `${baseId}_${partIdx}`, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'tool_result', - toolId: part.tool_use_id || '', - content: part.content === undefined ? '' : String(part.content), - isError: Boolean(part.is_error), - })); - } - } - } else if (typeof content === 'string' && (content.trim() || rawImages)) { - // Legacy (non-JSONL) session files reach this branch with the raw - // prompt text, so strip any block here as well. - const { text: cleanContent, attachments } = normalizedRole === 'user' - ? parseImagesInputTag(content) - : { text: content, attachments: [] }; - normalized.push(createNormalizedMessage({ - id: baseId, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'text', - role: normalizedRole, - content: cleanContent, - images: rawImages ?? (attachments.length > 0 ? attachments : undefined), - })); - } else { - const textContent = extractGeminiTextContent(content); - if (textContent.trim()) { - normalized.push(createNormalizedMessage({ - id: baseId, - sessionId, - timestamp: ts, - provider: PROVIDER, - kind: 'text', - role: normalizedRole, - content: textContent, - })); - } - } - } - - const toolResultMap = new Map(); - for (const msg of normalized) { - if (msg.kind === 'tool_result' && msg.toolId) { - toolResultMap.set(msg.toolId, msg); - } - } - for (const msg of normalized) { - if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) { - const toolResult = toolResultMap.get(msg.toolId); - if (toolResult) { - msg.toolResult = { content: toolResult.content, isError: toolResult.isError }; - } - } - } - - const start = Math.max(0, offset); - const pageLimit = limit === null ? null : Math.max(0, limit); - // Tail pagination via the shared contract: offset 0 returns the most - // recent page, matching every other provider. - const { page, hasMore } = sliceTailPage(normalized, pageLimit, start); - let total = 0; - for (const msg of normalized) { - if (msg.kind !== 'tool_result') { - total += 1; - } - } - - return { - messages: page, - total, - hasMore, - offset: start, - limit: pageLimit, - tokenUsage: result.tokenUsage, - }; - } -} diff --git a/server/modules/providers/list/gemini/gemini-skills.provider.ts b/server/modules/providers/list/gemini/gemini-skills.provider.ts deleted file mode 100644 index f42ebb6f..00000000 --- a/server/modules/providers/list/gemini/gemini-skills.provider.ts +++ /dev/null @@ -1,44 +0,0 @@ -import os from 'node:os'; -import path from 'node:path'; - -import { SkillsProvider } from '@/modules/providers/shared/skills/skills.provider.js'; -import type { ProviderSkillSource } from '@/shared/types.js'; - -export class GeminiSkillsProvider extends SkillsProvider { - constructor() { - super('gemini'); - } - - protected async getSkillSources(workspacePath: string): Promise { - return [ - { - scope: 'user', - rootDir: path.join(os.homedir(), '.gemini', 'skills'), - commandPrefix: '/', - }, - { - scope: 'user', - rootDir: path.join(os.homedir(), '.agents', 'skills'), - commandPrefix: '/', - }, - { - scope: 'project', - rootDir: path.join(workspacePath, '.gemini', 'skills'), - commandPrefix: '/', - }, - { - scope: 'project', - rootDir: path.join(workspacePath, '.agents', 'skills'), - commandPrefix: '/', - }, - ]; - } - - protected async getGlobalSkillSource(): Promise { - return { - scope: 'user', - rootDir: path.join(os.homedir(), '.gemini', 'skills'), - commandPrefix: '/', - }; - } -} diff --git a/server/modules/providers/list/gemini/gemini.provider.ts b/server/modules/providers/list/gemini/gemini.provider.ts deleted file mode 100644 index 38d25245..00000000 --- a/server/modules/providers/list/gemini/gemini.provider.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js'; -import { GeminiProviderAuth } from '@/modules/providers/list/gemini/gemini-auth.provider.js'; -import { GeminiProviderModels } from '@/modules/providers/list/gemini/gemini-models.provider.js'; -import { GeminiMcpProvider } from '@/modules/providers/list/gemini/gemini-mcp.provider.js'; -import { GeminiSessionSynchronizer } from '@/modules/providers/list/gemini/gemini-session-synchronizer.provider.js'; -import { GeminiSessionsProvider } from '@/modules/providers/list/gemini/gemini-sessions.provider.js'; -import { GeminiSkillsProvider } from '@/modules/providers/list/gemini/gemini-skills.provider.js'; -import type { - IProviderAuth, - IProviderModels, - IProviderSessionSynchronizer, - IProviderSkills, - IProviderSessions, -} from '@/shared/interfaces.js'; - -export class GeminiProvider extends AbstractProvider { - readonly models: IProviderModels = new GeminiProviderModels(); - readonly mcp = new GeminiMcpProvider(); - readonly auth: IProviderAuth = new GeminiProviderAuth(); - readonly skills: IProviderSkills = new GeminiSkillsProvider(); - readonly sessions: IProviderSessions = new GeminiSessionsProvider(); - readonly sessionSynchronizer: IProviderSessionSynchronizer = new GeminiSessionSynchronizer(); - - constructor() { - super('gemini'); - } -} diff --git a/server/modules/providers/list/opencode/opencode-auth.provider.ts b/server/modules/providers/list/opencode/opencode-auth.provider.ts index 9f55d18b..ad24a9a9 100644 --- a/server/modules/providers/list/opencode/opencode-auth.provider.ts +++ b/server/modules/providers/list/opencode/opencode-auth.provider.ts @@ -19,7 +19,6 @@ const OPENCODE_ENV_CREDENTIAL_KEYS = [ 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', - 'GEMINI_API_KEY', 'GROQ_API_KEY', 'OPENROUTER_API_KEY', ]; diff --git a/server/modules/providers/list/opencode/opencode-models.provider.ts b/server/modules/providers/list/opencode/opencode-models.provider.ts index 592eeb1d..c18e265d 100644 --- a/server/modules/providers/list/opencode/opencode-models.provider.ts +++ b/server/modules/providers/list/opencode/opencode-models.provider.ts @@ -49,16 +49,6 @@ export const OPENCODE_FALLBACK_MODELS: ProviderModelsDefinition = { label: 'GPT-5.4 Mini', description: 'openai - openai/gpt-5.4-mini', }, - { - value: 'google/gemini-2.5-pro', - label: 'Gemini 2.5 Pro', - description: 'google - google/gemini-2.5-pro', - }, - { - value: 'google/gemini-2.5-flash', - label: 'Gemini 2.5 Flash', - description: 'google - google/gemini-2.5-flash', - }, ], DEFAULT: 'anthropic/claude-sonnet-4-5', }; @@ -239,6 +229,10 @@ const readOpenCodeModelParts = (id: string): { upstreamProvider: string; slug: s }; }; +const isSupportedOpenCodeModelId = (id: string): boolean => ( + readOpenCodeModelParts(id).upstreamProvider.toLowerCase() !== 'google' +); + const readOpenCodeVerboseModelId = (model: OpenCodeVerboseModel): string | null => { const id = readOptionalString(model.id); if (!id) { @@ -296,7 +290,7 @@ const readOpenCodeEffortValues = ( const mapOpenCodeVerboseModel = (model: OpenCodeVerboseModel): ProviderModelOption | null => { const value = readOpenCodeVerboseModelId(model); - if (!value) { + if (!value || !isSupportedOpenCodeModelId(value)) { return null; } @@ -315,11 +309,13 @@ const mapOpenCodeVerboseModel = (model: OpenCodeVerboseModel): ProviderModelOpti }; export const buildOpenCodeDefinitionFromIds = (ids: string[]): ProviderModelsDefinition => { - const options: ProviderModelOption[] = ids.map((value) => ({ - value, - label: labelForOpenCodeModelId(value), - description: descriptionForOpenCodeModelId(value), - })); + const options: ProviderModelOption[] = ids + .filter(isSupportedOpenCodeModelId) + .map((value) => ({ + value, + label: labelForOpenCodeModelId(value), + description: descriptionForOpenCodeModelId(value), + })); const defaultValue = options.find((option) => option.value === OPENCODE_FALLBACK_MODELS.DEFAULT)?.value ?? options[0]?.value diff --git a/server/modules/providers/provider.registry.ts b/server/modules/providers/provider.registry.ts index a9f0d26b..8c333fcb 100644 --- a/server/modules/providers/provider.registry.ts +++ b/server/modules/providers/provider.registry.ts @@ -1,7 +1,6 @@ import { ClaudeProvider } from '@/modules/providers/list/claude/claude.provider.js'; import { CodexProvider } from '@/modules/providers/list/codex/codex.provider.js'; import { CursorProvider } from '@/modules/providers/list/cursor/cursor.provider.js'; -import { GeminiProvider } from '@/modules/providers/list/gemini/gemini.provider.js'; import { OpenCodeProvider } from '@/modules/providers/list/opencode/opencode.provider.js'; import type { IProvider } from '@/shared/interfaces.js'; import type { LLMProvider } from '@/shared/types.js'; @@ -11,7 +10,6 @@ const providers: Record = { claude: new ClaudeProvider(), codex: new CodexProvider(), cursor: new CursorProvider(), - gemini: new GeminiProvider(), opencode: new OpenCodeProvider(), }; diff --git a/server/modules/providers/provider.routes.ts b/server/modules/providers/provider.routes.ts index 7fa7947e..2352f3a1 100644 --- a/server/modules/providers/provider.routes.ts +++ b/server/modules/providers/provider.routes.ts @@ -285,7 +285,6 @@ const parseProvider = (value: unknown): LLMProvider => { normalized === 'claude' || normalized === 'codex' || normalized === 'cursor' - || normalized === 'gemini' || normalized === 'opencode' ) { return normalized; diff --git a/server/modules/providers/services/provider-capabilities.service.ts b/server/modules/providers/services/provider-capabilities.service.ts index f50543c7..a1446062 100644 --- a/server/modules/providers/services/provider-capabilities.service.ts +++ b/server/modules/providers/services/provider-capabilities.service.ts @@ -62,16 +62,6 @@ const PROVIDER_CAPABILITIES: Record = { supportsTokenUsage: true, supportsEffort: true, }, - gemini: { - provider: 'gemini', - permissionModes: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], - defaultPermissionMode: 'default', - supportsImages: true, - supportsAbort: true, - supportsPermissionRequests: false, - supportsTokenUsage: true, - supportsEffort: false, - }, opencode: { provider: 'opencode', // Mapped by the runtime onto OpenCode's controls: `--agent plan` (plan), diff --git a/server/modules/providers/services/provider-models.service.ts b/server/modules/providers/services/provider-models.service.ts index cb183f99..3bd990f3 100644 --- a/server/modules/providers/services/provider-models.service.ts +++ b/server/modules/providers/services/provider-models.service.ts @@ -17,7 +17,7 @@ import { readProviderSessionActiveModelChange } from '@/shared/utils.js'; export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000; const PROVIDER_MODELS_CACHE_VERSION = 2; -const UNCACHED_PROVIDERS = new Set(['claude', 'gemini']); +const UNCACHED_PROVIDERS = new Set(['claude']); type ProviderModelsServiceDependencies = { resolveProvider?: (provider: LLMProvider) => Pick; diff --git a/server/modules/providers/services/session-conversations-search.service.ts b/server/modules/providers/services/session-conversations-search.service.ts index 101a0955..1854eadd 100644 --- a/server/modules/providers/services/session-conversations-search.service.ts +++ b/server/modules/providers/services/session-conversations-search.service.ts @@ -8,7 +8,7 @@ import { rgPath } from '@vscode/ripgrep'; import { projectsDb, sessionsDb } from '@/modules/database/index.js'; type AnyRecord = Record; -type SearchableProvider = 'claude' | 'codex' | 'gemini'; +type SearchableProvider = 'claude' | 'codex'; type SearchSnippetHighlight = { start: number; @@ -82,7 +82,7 @@ type ProjectBucket = { sessions: SearchableSessionRow[]; }; -const SUPPORTED_PROVIDERS = new Set(['claude', 'codex', 'gemini']); +const SUPPORTED_PROVIDERS = new Set(['claude', 'codex']); const MAX_MATCHES_PER_SESSION = 2; const RIPGREP_FILE_CHUNK_SIZE = 40; const RIPGREP_CHUNK_CONCURRENCY = 6; @@ -455,21 +455,6 @@ function extractCodexText(content: unknown): string { .join(' '); } -function extractGeminiText(content: unknown): string { - if (typeof content === 'string') { - return content; - } - - if (!Array.isArray(content)) { - return ''; - } - - return content - .filter((part: AnyRecord) => typeof part?.text === 'string') - .map((part: AnyRecord) => String(part.text)) - .join(' '); -} - function normalizeSearchableSessions(rows: SessionRepositoryRow[]): SearchableSessionRow[] { const normalizedRows: SearchableSessionRow[] = []; const projectArchiveStateByPath = new Map(); @@ -1065,81 +1050,6 @@ async function parseCodexSessionMatches( }; } -async function parseGeminiSessionMatches( - session: SearchableSessionRow, - runtime: SearchRuntime, -): Promise { - let data: string; - try { - data = await fs.readFile(session.jsonl_path, 'utf8'); - } catch { - return null; - } - - let parsed: AnyRecord; - try { - parsed = JSON.parse(data) as AnyRecord; - } catch { - return null; - } - - const sourceMessages = Array.isArray(parsed.messages) ? parsed.messages as AnyRecord[] : []; - if (sourceMessages.length === 0) { - return null; - } - - const matches: SessionConversationMatch[] = []; - let firstUserText: string | null = null; - - for (const msg of sourceMessages) { - if (runtime.totalMatches >= runtime.limit || runtime.isAborted()) { - break; - } - - const role = msg.type === 'user' - ? 'user' - : (msg.type === 'gemini' || msg.type === 'assistant') - ? 'assistant' - : null; - if (!role) { - continue; - } - - const text = extractGeminiText(msg.content); - if (!text) { - continue; - } - - if (role === 'user' && !firstUserText) { - firstUserText = text; - } - - if (!runtime.matchesQuery(text)) { - continue; - } - - const { snippet, highlights } = runtime.buildSnippet(text); - addSessionMatch(runtime, matches, { - role, - snippet, - highlights, - timestamp: msg.timestamp ? String(msg.timestamp) : null, - provider: 'gemini', - }); - } - - if (matches.length === 0) { - return null; - } - - return { - sessionId: session.session_id, - provider: 'gemini', - sessionSummary: toSummaryText(session.custom_name, firstUserText, 'Gemini Session'), - matches, - }; -} - async function parseSessionMatches( session: SearchableSessionRow, runtime: SearchRuntime, @@ -1150,7 +1060,7 @@ async function parseSessionMatches( if (session.provider === 'codex') { return parseCodexSessionMatches(session, runtime); } - return parseGeminiSessionMatches(session, runtime); + return null; } export async function searchConversations( diff --git a/server/modules/providers/services/session-synchronizer.service.ts b/server/modules/providers/services/session-synchronizer.service.ts index 55b41f9e..5bb2b864 100644 --- a/server/modules/providers/services/session-synchronizer.service.ts +++ b/server/modules/providers/services/session-synchronizer.service.ts @@ -21,7 +21,6 @@ export const sessionSynchronizerService = { claude: 0, codex: 0, cursor: 0, - gemini: 0, opencode: 0, }; const failures: string[] = []; diff --git a/server/modules/providers/services/sessions-watcher.service.ts b/server/modules/providers/services/sessions-watcher.service.ts index cfbdb887..96f13e2a 100644 --- a/server/modules/providers/services/sessions-watcher.service.ts +++ b/server/modules/providers/services/sessions-watcher.service.ts @@ -25,16 +25,6 @@ const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> = provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions'), }, - // { - // provider: 'gemini', - // rootPath: path.join(os.homedir(), '.gemini', 'sessions'), - // }, - // Keep `sessions/` watcher disabled: Gemini also mirrors artifacts there, - // which causes duplicate synchronization events. - { - provider: 'gemini', - rootPath: path.join(os.homedir(), '.gemini', 'tmp'), - }, { provider: 'opencode', rootPath: path.join(os.homedir(), '.local', 'share', 'opencode'), @@ -81,10 +71,6 @@ function isWatcherTargetFile(provider: LLMProvider, filePath: string): boolean { return path.basename(filePath) === 'opencode.db'; } - if (provider === 'gemini') { - return filePath.endsWith('.json') || filePath.endsWith('.jsonl'); - } - return filePath.endsWith('.jsonl'); } diff --git a/server/modules/providers/tests/mcp.test.ts b/server/modules/providers/tests/mcp.test.ts index f10b1354..439966ac 100644 --- a/server/modules/providers/tests/mcp.test.ts +++ b/server/modules/providers/tests/mcp.test.ts @@ -257,34 +257,15 @@ test('providerMcpService handles opencode MCP config and capability validation', }); /** - * This test covers Gemini/Cursor MCP JSON formats and user/project scope persistence. + * This test covers Cursor MCP JSON format and user/project scope persistence. */ -test('providerMcpService handles gemini and cursor MCP JSON config formats', { concurrency: false }, async () => { +test('providerMcpService handles cursor MCP JSON config formats', { concurrency: false }, async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-gc-')); const workspacePath = path.join(tempRoot, 'workspace'); await fs.mkdir(workspacePath, { recursive: true }); const restoreHomeDir = patchHomeDir(tempRoot); try { - await providerMcpService.upsertProviderMcpServer('gemini', { - name: 'gemini-stdio', - scope: 'user', - transport: 'stdio', - command: 'node', - args: ['server.js'], - env: { TOKEN: '$TOKEN' }, - cwd: './server', - }); - - await providerMcpService.upsertProviderMcpServer('gemini', { - name: 'gemini-http', - scope: 'project', - transport: 'http', - url: 'https://gemini.example.com/mcp', - headers: { Authorization: 'Bearer token' }, - workspacePath, - }); - await providerMcpService.upsertProviderMcpServer('cursor', { name: 'cursor-stdio', scope: 'project', @@ -303,15 +284,6 @@ test('providerMcpService handles gemini and cursor MCP JSON config formats', { c headers: { API_KEY: 'value' }, }); - const geminiUserConfig = await readJson(path.join(tempRoot, '.gemini', 'settings.json')); - const geminiUserServer = (geminiUserConfig.mcpServers as Record)['gemini-stdio'] as Record; - assert.equal(geminiUserServer.command, 'node'); - assert.equal(geminiUserServer.type, undefined); - - const geminiProjectConfig = await readJson(path.join(workspacePath, '.gemini', 'settings.json')); - const geminiProjectServer = (geminiProjectConfig.mcpServers as Record)['gemini-http'] as Record; - assert.equal(geminiProjectServer.type, 'http'); - const cursorUserConfig = await readJson(path.join(tempRoot, '.cursor', 'mcp.json')); const cursorHttpServer = (cursorUserConfig.mcpServers as Record)['cursor-http'] as Record; assert.equal(cursorHttpServer.url, 'http://localhost:3333/mcp'); @@ -341,7 +313,7 @@ test('providerMcpService global adder writes to all providers and rejects unsupp workspacePath, }); - assert.equal(globalResult.length, 5); + assert.equal(globalResult.length, 4); assert.ok(globalResult.every((entry) => entry.created === true)); const claudeProject = await readJson(path.join(workspacePath, '.mcp.json')); @@ -350,9 +322,6 @@ test('providerMcpService global adder writes to all providers and rejects unsupp const codexProject = TOML.parse(await fs.readFile(path.join(workspacePath, '.codex', 'config.toml'), 'utf8')) as Record; assert.ok((codexProject.mcp_servers as Record)['global-http']); - const geminiProject = await readJson(path.join(workspacePath, '.gemini', 'settings.json')); - assert.ok((geminiProject.mcpServers as Record)['global-http']); - const opencodeProject = await readJson(path.join(workspacePath, 'opencode.json')); assert.ok((opencodeProject.mcp as Record)['global-http']); diff --git a/server/modules/providers/tests/opencode-models.test.ts b/server/modules/providers/tests/opencode-models.test.ts index 097c1272..c6f0e72c 100644 --- a/server/modules/providers/tests/opencode-models.test.ts +++ b/server/modules/providers/tests/opencode-models.test.ts @@ -30,6 +30,7 @@ test('OpenCode models provider formats frontend labels from provider-prefixed id 'opencode/nemotron-3-super-free', 'anthropic/claude-3-5-sonnet-20241022', 'anthropic/claude-opus-4-7-fast', + 'google/model-alpha', 'openai/gpt-5.4-mini-fast', 'openai/gpt-5.5-pro', 'newprovider/alpha-v12-special-20261231', @@ -104,6 +105,12 @@ anthropic/claude-sonnet-5 } } } +google/model-alpha +{ + "id": "model-alpha", + "providerID": "google", + "name": "Model Alpha" +} `); const definition = buildOpenCodeDefinitionFromVerboseModels(models); diff --git a/server/modules/providers/tests/provider-image-history.test.ts b/server/modules/providers/tests/provider-image-history.test.ts index 207363a3..d52ea1a6 100644 --- a/server/modules/providers/tests/provider-image-history.test.ts +++ b/server/modules/providers/tests/provider-image-history.test.ts @@ -1,13 +1,9 @@ import assert from 'node:assert/strict'; -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; import test from 'node:test'; import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js'; import { CodexSessionsProvider, extractCodexUserImages } from '@/modules/providers/list/codex/codex-sessions.provider.js'; import { CursorSessionsProvider } from '@/modules/providers/list/cursor/cursor-sessions.provider.js'; -import { getGeminiJsonlSessionMessages } from '@/modules/providers/list/gemini/gemini-sessions.provider.js'; import { appendImagesInputTag } from '@/shared/image-attachments.js'; const SESSION_ID = 'session-1'; @@ -127,58 +123,6 @@ test('codex history: normalized user entries keep their images', () => { assert.deepEqual(messages[0].images, [{ path: '.cloudcli/assets/a.png' }]); }); -// ---------------------------------------------------------------- Gemini - -test('gemini history: tag is stripped and paths attached', async () => { - const tempDir = await mkdtemp(path.join(os.tmpdir(), 'gemini-image-history-')); - const jsonlPath = path.join(tempDir, 'session.jsonl'); - try { - const taggedPrompt = appendImagesInputTag('Compare these designs', [ - { path: '.cloudcli/assets/1-a.png' }, - { path: '.cloudcli/assets/2-b.png' }, - ]); - const lines = [ - JSON.stringify({ type: 'user', id: 'm1', content: taggedPrompt, timestamp: '2026-07-03T10:00:00.000Z' }), - JSON.stringify({ type: 'gemini', id: 'm2', content: 'They differ in spacing.', timestamp: '2026-07-03T10:00:05.000Z' }), - ]; - await writeFile(jsonlPath, `${lines.join('\n')}\n`, 'utf8'); - - const { messages } = await getGeminiJsonlSessionMessages(jsonlPath); - - assert.equal(messages.length, 2); - assert.equal(messages[0].message.role, 'user'); - assert.equal(messages[0].message.content, 'Compare these designs'); - assert.deepEqual(messages[0].images, [ - { path: '.cloudcli/assets/1-a.png' }, - { path: '.cloudcli/assets/2-b.png' }, - ]); - // Assistant text is left untouched. - assert.equal(messages[1].message.role, 'assistant'); - assert.equal(messages[1].images, undefined); - } finally { - await rm(tempDir, { recursive: true, force: true }); - } -}); - -test('gemini history: prompts without a tag are unchanged', async () => { - const tempDir = await mkdtemp(path.join(os.tmpdir(), 'gemini-image-history-')); - const jsonlPath = path.join(tempDir, 'session.jsonl'); - try { - await writeFile( - jsonlPath, - `${JSON.stringify({ type: 'user', id: 'm1', content: 'plain prompt', timestamp: '2026-07-03T10:00:00.000Z' })}\n`, - 'utf8', - ); - - const { messages } = await getGeminiJsonlSessionMessages(jsonlPath); - assert.equal(messages.length, 1); - assert.equal(messages[0].message.content, 'plain prompt'); - assert.equal(messages[0].images, undefined); - } finally { - await rm(tempDir, { recursive: true, force: true }); - } -}); - // ---------------------------------------------------------------- Cursor test('cursor history: inside user_query is stripped and attached', () => { diff --git a/server/modules/providers/tests/provider-models.service.test.ts b/server/modules/providers/tests/provider-models.service.test.ts index 36cbfc6d..5f44ab86 100644 --- a/server/modules/providers/tests/provider-models.service.test.ts +++ b/server/modules/providers/tests/provider-models.service.test.ts @@ -170,13 +170,13 @@ test('provider model cache is persisted across service instances', async () => { cachePath, resolveProvider: () => ({ models: { - getSupportedModels: async () => createModels('gemini-cached'), - getCurrentActiveModel: async () => createCurrentActiveModel('gemini-active'), - changeActiveModel: async (input) => createSessionActiveModelChange('gemini', input), + getSupportedModels: async () => createModels('cursor-cached'), + getCurrentActiveModel: async () => createCurrentActiveModel('cursor-active'), + changeActiveModel: async (input) => createSessionActiveModelChange('cursor', input), }, }), }); - await writer.getProviderModels('gemini'); + await writer.getProviderModels('cursor'); const reader = createProviderModelsService({ cachePath, @@ -185,13 +185,13 @@ test('provider model cache is persisted across service instances', async () => { getSupportedModels: async () => { throw new Error('loader should not be called for persisted cache hits'); }, - getCurrentActiveModel: async () => createCurrentActiveModel('gemini-active'), - changeActiveModel: async (input) => createSessionActiveModelChange('gemini', input), + getCurrentActiveModel: async () => createCurrentActiveModel('cursor-active'), + changeActiveModel: async (input) => createSessionActiveModelChange('cursor', input), }, }), }); - const models = await reader.getProviderModels('gemini'); - assert.equal(models.models.DEFAULT, 'gemini-cached'); + const models = await reader.getProviderModels('cursor'); + assert.equal(models.models.DEFAULT, 'cursor-cached'); assert.equal(models.cache.source, 'disk'); } finally { await rm(tempRoot, { recursive: true, force: true }); diff --git a/server/modules/providers/tests/skills.test.ts b/server/modules/providers/tests/skills.test.ts index 147366cc..e6bfacb2 100644 --- a/server/modules/providers/tests/skills.test.ts +++ b/server/modules/providers/tests/skills.test.ts @@ -444,34 +444,22 @@ test('providerSkillsService lists opencode project and user compatibility skills }); /** - * This test covers Gemini and Cursor skill directory rules, including shared + * This test covers Cursor skill directory rules, including shared * `.agents/skills` project support. */ -test('providerSkillsService lists gemini and cursor skills from their configured directories', { concurrency: false }, async () => { +test('providerSkillsService lists cursor skills from its configured directories', { concurrency: false }, async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-gc-')); const workspacePath = path.join(tempRoot, 'workspace'); await fs.mkdir(workspacePath, { recursive: true }); const restoreHomeDir = patchHomeDir(tempRoot); try { - await writeSkill( - path.join(tempRoot, '.gemini', 'skills'), - 'gemini-user-dir', - 'gemini-user', - 'Gemini user skill', - ); await writeSkill( path.join(tempRoot, '.agents', 'skills'), 'agents-user-dir', 'agents-user', 'Agents user skill', ); - await writeSkill( - path.join(workspacePath, '.gemini', 'skills'), - 'gemini-project-dir', - 'gemini-project', - 'Gemini project skill', - ); await writeSkill( path.join(workspacePath, '.agents', 'skills'), 'agents-project-dir', @@ -491,14 +479,6 @@ test('providerSkillsService lists gemini and cursor skills from their configured 'Cursor user skill', ); - const geminiSkills = await providerSkillsService.listProviderSkills('gemini', { workspacePath }); - const geminiByName = new Map(geminiSkills.map((skill) => [skill.name, skill])); - assert.equal(geminiByName.get('gemini-user')?.scope, 'user'); - assert.equal(geminiByName.get('agents-user')?.scope, 'user'); - assert.equal(geminiByName.get('gemini-project')?.scope, 'project'); - assert.equal(geminiByName.get('agents-project')?.scope, 'project'); - assert.equal(geminiByName.get('gemini-project')?.command, '/gemini-project'); - const cursorSkills = await providerSkillsService.listProviderSkills('cursor', { workspacePath }); const cursorByName = new Map(cursorSkills.map((skill) => [skill.name, skill])); assert.equal(cursorByName.get('agents-project')?.scope, 'project'); @@ -515,7 +495,7 @@ test('providerSkillsService lists gemini and cursor skills from their configured * This test covers managed global skill creation for providers that own a * writable user skill directory. */ -test('providerSkillsService adds global skills for claude, codex, gemini, and cursor', { concurrency: false }, async () => { +test('providerSkillsService adds global skills for claude, codex, and cursor', { concurrency: false }, async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-create-')); const restoreHomeDir = patchHomeDir(tempRoot); @@ -618,22 +598,6 @@ test('providerSkillsService adds global skills for claude, codex, gemini, and cu ); await assert.rejects(fs.stat(pendingBatchSkillPath), { code: 'ENOENT' }); - const createdGeminiSkills = await providerSkillsService.addProviderSkills('gemini', { - entries: [ - { - directoryName: 'gemini-global-dir', - content: '---\nname: gemini-global\ndescription: Gemini global skill\n---\n\nGemini body.\n', - }, - ], - }); - const createdGeminiSkill = createdGeminiSkills[0]; - assert.ok(createdGeminiSkill); - assert.equal(createdGeminiSkill.command, '/gemini-global'); - assert.equal( - createdGeminiSkill.sourcePath.endsWith(path.join('.gemini', 'skills', 'gemini-global-dir', 'SKILL.md')), - true, - ); - const createdCursorSkills = await providerSkillsService.addProviderSkills('cursor', { entries: [ { @@ -656,9 +620,6 @@ test('providerSkillsService adds global skills for claude, codex, gemini, and cu const listedCodexSkills = await providerSkillsService.listProviderSkills('codex'); assert.equal(listedCodexSkills.some((skill) => skill.name === 'replacement'), true); - const listedGeminiSkills = await providerSkillsService.listProviderSkills('gemini'); - assert.equal(listedGeminiSkills.some((skill) => skill.name === 'gemini-global'), true); - const listedCursorSkills = await providerSkillsService.listProviderSkills('cursor'); assert.equal(listedCursorSkills.some((skill) => skill.name === 'cursor-global'), true); diff --git a/server/modules/websocket/services/shell-websocket.service.ts b/server/modules/websocket/services/shell-websocket.service.ts index 55680b8c..bb4ac5aa 100644 --- a/server/modules/websocket/services/shell-websocket.service.ts +++ b/server/modules/websocket/services/shell-websocket.service.ts @@ -146,14 +146,6 @@ function buildShellCommand( return 'codex'; } - if (provider === 'gemini') { - const command = initialCommand || 'gemini'; - if (resumeSessionId) { - return `${command} --resume "${resumeSessionId}"`; - } - return command; - } - if (provider === 'opencode') { if (resumeSessionId) { return `opencode --session "${resumeSessionId}"`; @@ -477,9 +469,7 @@ export function handleShellConnection( ? 'Cursor' : provider === 'codex' ? 'Codex' - : provider === 'gemini' - ? 'Gemini' - : provider === 'opencode' + : provider === 'opencode' ? 'OpenCode' : 'Claude'; welcomeMsg = hasSession && resumeSessionId diff --git a/server/modules/websocket/tests/chat-run-registry.test.ts b/server/modules/websocket/tests/chat-run-registry.test.ts index cc6250c0..d8ea8a58 100644 --- a/server/modules/websocket/tests/chat-run-registry.test.ts +++ b/server/modules/websocket/tests/chat-run-registry.test.ts @@ -186,22 +186,22 @@ test('replayEvents returns only events after the requested seq', async () => { test('attachConnection reroutes the live stream to a new socket', async () => { await withIsolatedDatabase(() => { - sessionsDb.createAppSession('app-run-5', 'gemini', '/workspace/demo'); + sessionsDb.createAppSession('app-run-5', 'opencode', '/workspace/demo'); const firstConnection = new FakeConnection(); const run = chatRunRegistry.startRun({ appSessionId: 'app-run-5', - provider: 'gemini', + provider: 'opencode', providerSessionId: null, connection: firstConnection, userId: null, }); assert.ok(run); - run.writer.send({ kind: 'stream_delta', provider: 'gemini', sessionId: 'g', content: 'before' }); + run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'before' }); const secondConnection = new FakeConnection(); assert.equal(chatRunRegistry.attachConnection('app-run-5', secondConnection), true); - run.writer.send({ kind: 'stream_delta', provider: 'gemini', sessionId: 'g', content: 'after' }); + run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'after' }); assert.deepEqual(firstConnection.frames.map((frame) => frame.content), ['before']); assert.deepEqual(secondConnection.frames.map((frame) => frame.content), ['after']); diff --git a/server/routes/agent.js b/server/routes/agent.js index 2f9a29e2..1ae605b6 100644 --- a/server/routes/agent.js +++ b/server/routes/agent.js @@ -9,7 +9,6 @@ import { userDb, apiKeysDb, githubTokensDb, projectsDb } from '../modules/databa import { queryClaudeSDK } from '../claude-sdk.js'; import { spawnCursor } from '../cursor-cli.js'; import { queryCodex } from '../openai-codex.js'; -import { spawnGemini } from '../gemini-cli.js'; import { spawnOpenCode } from '../opencode-cli.js'; import { Octokit } from '@octokit/rest'; import { providerModelsService } from '../modules/providers/services/provider-models.service.js'; @@ -637,7 +636,7 @@ class ResponseCollector { * - Source for auto-generated branch names (if createBranch=true and no branchName) * - Fallback for PR title if no commits are made * - * @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'gemini' | 'opencode' + * @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'opencode' * Default: 'claude' * * @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates. @@ -649,7 +648,7 @@ class ResponseCollector { * * Claude models: 'default', 'sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]', 'fable' * Cursor models: 'gpt-5' (default), 'gpt-5.2', 'gpt-5.2-high', 'sonnet-4.5', 'opus-4.5', - * 'gemini-3-pro', 'composer-1', 'auto', 'gpt-5.1', 'gpt-5.1-high', + * 'composer-1', 'auto', 'gpt-5.1', 'gpt-5.1-high', * 'gpt-5.1-codex', 'gpt-5.1-codex-high', 'gpt-5.1-codex-max', * 'gpt-5.1-codex-max-high', 'opus-4.1', 'grok', and thinking variants * Codex models: 'gpt-5.4' (default), 'gpt-5.5', 'gpt-5.4-mini' @@ -760,7 +759,7 @@ class ResponseCollector { * Input Validations (400 Bad Request): * - Either githubUrl OR projectPath must be provided (not neither) * - message must be non-empty string - * - provider must be 'claude', 'cursor', 'codex', 'gemini', or 'opencode' + * - provider must be 'claude', 'cursor', 'codex', or 'opencode' * - createBranch/createPR requires githubUrl OR projectPath (not neither) * - branchName must pass Git naming rules (if provided) * @@ -871,8 +870,8 @@ router.post('/', validateExternalApiKey, async (req, res) => { return res.status(400).json({ error: 'message is required' }); } - if (!['claude', 'cursor', 'codex', 'gemini', 'opencode'].includes(provider)) { - return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", "gemini", or "opencode"' }); + if (!['claude', 'cursor', 'codex', 'opencode'].includes(provider)) { + return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", or "opencode"' }); } // Validate GitHub branch/PR creation requirements @@ -951,7 +950,6 @@ router.post('/', validateExternalApiKey, async (req, res) => { } const codexModels = (await providerModelsService.getProviderModels('codex')).models; - const geminiModels = (await providerModelsService.getProviderModels('gemini')).models; const opencodeModels = (await providerModelsService.getProviderModels('opencode')).models; // Start the appropriate session @@ -988,16 +986,6 @@ router.post('/', validateExternalApiKey, async (req, res) => { effort, permissionMode: 'bypassPermissions' }, writer); - } else if (provider === 'gemini') { - console.log('✨ Starting Gemini CLI session'); - - await spawnGemini(message.trim(), { - projectPath: finalProjectPath, - cwd: finalProjectPath, - sessionId: sessionId || null, - model: model || geminiModels.DEFAULT, - skipPermissions: true // CLI mode bypasses permissions - }, writer); } else if (provider === 'opencode') { console.log('Starting OpenCode CLI session'); diff --git a/server/routes/commands.js b/server/routes/commands.js index ea223f12..722fd545 100644 --- a/server/routes/commands.js +++ b/server/routes/commands.js @@ -15,13 +15,12 @@ const APP_ROOT = findAppRoot(__dirname); const router = express.Router(); -const MODEL_PROVIDERS = ["claude", "cursor", "codex", "gemini", "opencode"]; +const MODEL_PROVIDERS = ["claude", "cursor", "codex", "opencode"]; const MODEL_PROVIDER_LABELS = { claude: "Claude", cursor: "Cursor", codex: "Codex", - gemini: "Gemini", opencode: "OpenCode", }; diff --git a/server/routes/gemini.js b/server/routes/gemini.js deleted file mode 100644 index 341365b4..00000000 --- a/server/routes/gemini.js +++ /dev/null @@ -1,25 +0,0 @@ -import express from 'express'; - -import sessionManager from '../sessionManager.js'; -import { sessionsDb } from '../modules/database/index.js'; - -const router = express.Router(); - -router.delete('/sessions/:sessionId', async (req, res) => { - try { - const { sessionId } = req.params; - - if (!sessionId || typeof sessionId !== 'string' || !/^[a-zA-Z0-9_.-]{1,100}$/.test(sessionId)) { - return res.status(400).json({ success: false, error: 'Invalid session ID format' }); - } - - await sessionManager.deleteSession(sessionId); - sessionsDb.deleteSessionById(sessionId); - res.json({ success: true }); - } catch (error) { - console.error(`Error deleting Gemini session ${req.params.sessionId}:`, error); - res.status(500).json({ success: false, error: error.message }); - } -}); - -export default router; diff --git a/server/sessionManager.js b/server/sessionManager.js deleted file mode 100644 index 1bf33bd4..00000000 --- a/server/sessionManager.js +++ /dev/null @@ -1,226 +0,0 @@ -import { promises as fs } from 'fs'; -import path from 'path'; -import os from 'os'; - -class SessionManager { - constructor() { - // Store sessions in memory with conversation history - this.sessions = new Map(); - this.maxSessions = 100; - this.sessionsDir = path.join(os.homedir(), '.gemini', 'sessions'); - this.ready = this.init(); - } - - async init() { - await this.initSessionsDir(); - await this.loadSessions(); - } - - async initSessionsDir() { - try { - await fs.mkdir(this.sessionsDir, { recursive: true }); - } catch (error) { - // console.error('Error creating sessions directory:', error); - } - } - - // Create a new session - createSession(sessionId, projectPath) { - const session = { - id: sessionId, - projectPath: projectPath, - messages: [], - createdAt: new Date(), - lastActivity: new Date() - }; - - // Evict oldest session from memory if we exceed limit - if (this.sessions.size >= this.maxSessions) { - const oldestKey = this.sessions.keys().next().value; - if (oldestKey) this.sessions.delete(oldestKey); - } - - this.sessions.set(sessionId, session); - this.saveSession(sessionId); - - return session; - } - - // Add a message to session - addMessage(sessionId, role, content) { - let session = this.sessions.get(sessionId); - - if (!session) { - // Create session if it doesn't exist - session = this.createSession(sessionId, ''); - } - - const message = { - role: role, // 'user' or 'assistant' - content: content, - timestamp: new Date() - }; - - session.messages.push(message); - session.lastActivity = new Date(); - - this.saveSession(sessionId); - - return session; - } - - // Get session by ID - getSession(sessionId) { - return this.sessions.get(sessionId); - } - - // Get all sessions for a project - getProjectSessions(projectPath) { - const sessions = []; - - for (const [id, session] of this.sessions) { - if (session.projectPath === projectPath) { - sessions.push({ - id: session.id, - summary: this.getSessionSummary(session), - messageCount: session.messages.length, - lastActivity: session.lastActivity - }); - } - } - - return sessions.sort((a, b) => - new Date(b.lastActivity) - new Date(a.lastActivity) - ); - } - - // Get session summary - getSessionSummary(session) { - if (session.messages.length === 0) { - return 'New Session'; - } - - // Find first user message - const firstUserMessage = session.messages.find(m => m.role === 'user'); - if (firstUserMessage) { - const content = firstUserMessage.content; - return content.length > 50 ? content.substring(0, 50) + '...' : content; - } - - return 'New Session'; - } - - // Build conversation context for Gemini - buildConversationContext(sessionId, maxMessages = 10) { - const session = this.sessions.get(sessionId); - - if (!session || session.messages.length === 0) { - return ''; - } - - // Get last N messages for context - const recentMessages = session.messages.slice(-maxMessages); - - let context = 'Here is the conversation history:\n\n'; - - for (const msg of recentMessages) { - if (msg.role === 'user') { - context += `User: ${msg.content}\n`; - } else { - context += `Assistant: ${msg.content}\n`; - } - } - - context += '\nBased on the conversation history above, please answer the following:\n'; - - return context; - } - - // Prevent path traversal - _safeFilePath(sessionId) { - const safeId = String(sessionId).replace(/[/\\]|\.\./g, ''); - return path.join(this.sessionsDir, `${safeId}.json`); - } - - // Save session to disk - async saveSession(sessionId) { - const session = this.sessions.get(sessionId); - if (!session) return; - - try { - const filePath = this._safeFilePath(sessionId); - await fs.writeFile(filePath, JSON.stringify(session, null, 2)); - } catch (error) { - // console.error('Error saving session:', error); - } - } - - // Load sessions from disk - async loadSessions() { - try { - const files = await fs.readdir(this.sessionsDir); - - for (const file of files) { - if (file.endsWith('.json')) { - try { - const filePath = path.join(this.sessionsDir, file); - const data = await fs.readFile(filePath, 'utf8'); - const session = JSON.parse(data); - - // Convert dates - session.createdAt = new Date(session.createdAt); - session.lastActivity = new Date(session.lastActivity); - session.messages.forEach(msg => { - msg.timestamp = new Date(msg.timestamp); - }); - - this.sessions.set(session.id, session); - } catch (error) { - // console.error(`Error loading session ${file}:`, error); - } - } - } - - // Enforce eviction after loading to prevent massive memory usage - while (this.sessions.size > this.maxSessions) { - const oldestKey = this.sessions.keys().next().value; - if (oldestKey) this.sessions.delete(oldestKey); - } - } catch (error) { - // console.error('Error loading sessions:', error); - } - } - - // Delete a session - async deleteSession(sessionId) { - this.sessions.delete(sessionId); - - try { - const filePath = this._safeFilePath(sessionId); - await fs.unlink(filePath); - } catch (error) { - // console.error('Error deleting session file:', error); - } - } - - // Get session messages for display - getSessionMessages(sessionId) { - const session = this.sessions.get(sessionId); - if (!session) return []; - - return session.messages.map(msg => ({ - type: 'message', - message: { - role: msg.role, - content: msg.content - }, - timestamp: msg.timestamp.toISOString() - })); - } -} - -// Singleton instance -const sessionManager = new SessionManager(); - -export const ready = sessionManager.ready; -export default sessionManager; \ No newline at end of file diff --git a/server/shared/image-attachments.ts b/server/shared/image-attachments.ts index 897309d9..afdd93b8 100644 --- a/server/shared/image-attachments.ts +++ b/server/shared/image-attachments.ts @@ -9,7 +9,7 @@ import path from 'node:path'; * folder and referenced by absolute path everywhere else: * - Claude: paths are read back into base64 `image` content blocks. * - Codex: paths become `local_image` input items. - * - Gemini/Cursor: paths are appended to the prompt inside an + * - Cursor/OpenCode: paths are appended to the prompt inside an * `` tag, which is stripped again when history is read. * * The chat UI loads them through the dedicated `/api/assets/images/:filename` @@ -120,8 +120,8 @@ export type ParsedImagesInput = { }; /** - * Appends the `` reference block used by the Gemini, Cursor, - * and OpenCode CLIs. The block carries one numbered line per attachment with + * Appends the `` reference block used by the Cursor and + * OpenCode CLIs. The block carries one numbered line per attachment with * the stored file path (quote-free on purpose — Windows .cmd shims mangle * quoted text) and the user's original filename, plus an explicit instruction * to read the files and keep the block out of the reply. The same block is diff --git a/server/shared/types.ts b/server/shared/types.ts index c803856b..45092ecf 100644 --- a/server/shared/types.ts +++ b/server/shared/types.ts @@ -65,7 +65,7 @@ export type AuthenticatedWebSocketRequest = IncomingMessage & { * Use this as the source of truth whenever a function or payload needs to identify * a specific LLM integration. */ -export type LLMProvider = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode'; +export type LLMProvider = 'claude' | 'codex' | 'cursor' | 'opencode'; /** * One selectable model row in a provider model catalog. diff --git a/server/shared/utils.ts b/server/shared/utils.ts index ac1489b3..7dbf20be 100644 --- a/server/shared/utils.ts +++ b/server/shared/utils.ts @@ -1243,7 +1243,7 @@ export async function extractFirstValidJsonlData( //----------------- CLI PROMPT ARGUMENT UTILITIES ------------ /** * Makes a prompt safe to pass as one CLI argument to `.cmd`-shimmed tools on - * Windows (cursor-agent, gemini, opencode installed via npm-style shims). + * Windows (cursor-agent and opencode installed via npm-style shims). * * cmd.exe cannot carry newlines inside an argument: everything after the * first newline is silently dropped before the target CLI ever sees it, which @@ -1251,7 +1251,7 @@ export async function extractFirstValidJsonlData( * Collapsing newline runs to single spaces loses formatting but never loses * content, so runtimes should call this on win32 right before spawning. * - * Used by the cursor, gemini, and opencode spawn runtimes. + * Used by the cursor and opencode spawn runtimes. */ export function flattenPromptForWindowsShell(prompt: string): string { if (process.platform !== 'win32' || typeof prompt !== 'string') { diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts index b9f9165c..1f959ff0 100644 --- a/src/components/chat/hooks/useChatComposerState.ts +++ b/src/components/chat/hooks/useChatComposerState.ts @@ -39,7 +39,6 @@ interface UseChatComposerStateArgs { claudeModel: string; codexModel: string; currentProviderEffort: string; - geminiModel: string; opencodeModel: string; isLoading: boolean; canAbortSession: boolean; @@ -180,7 +179,6 @@ export function useChatComposerState({ claudeModel, codexModel, currentProviderEffort, - geminiModel, opencodeModel, isLoading, canAbortSession, @@ -353,9 +351,7 @@ export function useChatComposerState({ ? cursorModel : provider === 'codex' ? codexModel - : provider === 'gemini' - ? geminiModel - : provider === 'opencode' + : provider === 'opencode' ? opencodeModel : claudeModel, tokenUsage: tokenBudget, @@ -410,7 +406,6 @@ export function useChatComposerState({ codexModel, currentSessionId, cursorModel, - geminiModel, opencodeModel, handleBuiltInCommand, handleCustomCommand, @@ -741,9 +736,7 @@ export function useChatComposerState({ ? 'cursor-tools-settings' : provider === 'codex' ? 'codex-settings' - : provider === 'gemini' - ? 'gemini-settings' - : provider === 'opencode' + : provider === 'opencode' ? 'opencode-settings' : 'claude-settings'; const savedSettings = safeLocalStorage.getItem(settingsKey); @@ -767,9 +760,7 @@ export function useChatComposerState({ ? cursorModel : provider === 'codex' ? codexModel - : provider === 'gemini' - ? geminiModel - : provider === 'opencode' + : provider === 'opencode' ? opencodeModel : claudeModel; const effort = currentProviderEffort; @@ -815,7 +806,6 @@ export function useChatComposerState({ currentSessionId, cursorModel, executeCommand, - geminiModel, opencodeModel, isLoading, onSessionProcessing, diff --git a/src/components/chat/hooks/useChatProviderState.ts b/src/components/chat/hooks/useChatProviderState.ts index e856e309..1d0aac5a 100644 --- a/src/components/chat/hooks/useChatProviderState.ts +++ b/src/components/chat/hooks/useChatProviderState.ts @@ -20,11 +20,17 @@ const FALLBACK_DEFAULT_MODEL: Record = { claude: 'default', cursor: 'gpt-5.3-codex', codex: 'gpt-5.4', - gemini: 'gemini-3.1-pro-preview', opencode: 'anthropic/claude-sonnet-4-5', }; -const PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'gemini', 'opencode']; +const PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'opencode']; + +const readStoredProvider = (): LLMProvider => { + const storedProvider = localStorage.getItem('selected-provider'); + return PROVIDERS.includes(storedProvider as LLMProvider) + ? storedProvider as LLMProvider + : 'claude'; +}; /** * Fallback permission-mode matrix used only until the backend capability @@ -36,7 +42,6 @@ const FALLBACK_PERMISSION_MODES: Record = { claude: ['default', 'auto', 'acceptEdits', 'bypassPermissions', 'plan'], cursor: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], codex: ['default', 'acceptEdits', 'bypassPermissions'], - gemini: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], opencode: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], }; @@ -85,9 +90,7 @@ type ChangeActiveModelApiResponse = { export function useChatProviderState({ selectedSession, selectedProject: _selectedProject }: UseChatProviderStateArgs) { const [permissionMode, setPermissionMode] = useState('default'); const [pendingPermissionRequests, setPendingPermissionRequests] = useState([]); - const [provider, setProvider] = useState(() => { - return (localStorage.getItem('selected-provider') as LLMProvider) || 'claude'; - }); + const [provider, setProvider] = useState(readStoredProvider); const [cursorModel, setCursorModel] = useState(() => { return localStorage.getItem('cursor-model') || FALLBACK_DEFAULT_MODEL.cursor; }); @@ -103,9 +106,6 @@ export function useChatProviderState({ selectedSession, selectedProject: _select return acc; }, {}); }); - const [geminiModel, setGeminiModel] = useState(() => { - return localStorage.getItem('gemini-model') || FALLBACK_DEFAULT_MODEL.gemini; - }); const [opencodeModel, setOpenCodeModel] = useState(() => { return localStorage.getItem('opencode-model') || FALLBACK_DEFAULT_MODEL.opencode; }); @@ -151,12 +151,6 @@ export function useChatProviderState({ selectedSession, selectedProject: _select return; } - if (targetProvider === 'gemini') { - setGeminiModel(model); - localStorage.setItem('gemini-model', model); - return; - } - setOpenCodeModel(model); localStorage.setItem('opencode-model', model); }, []); @@ -360,9 +354,8 @@ export function useChatProviderState({ selectedSession, selectedProject: _select claude: claudeModel, cursor: cursorModel, codex: codexModel, - gemini: geminiModel, opencode: opencodeModel, - }), [claudeModel, cursorModel, codexModel, geminiModel, opencodeModel]); + }), [claudeModel, cursorModel, codexModel, opencodeModel]); useEffect(() => { const claude = providerModelCatalog.claude; @@ -403,19 +396,6 @@ export function useChatProviderState({ selectedSession, selectedProject: _select } }, [providerModelCatalog.codex, codexModel]); - useEffect(() => { - const gemini = providerModelCatalog.gemini; - if (gemini) { - const next = pickStoredOrCurrent('gemini-model', geminiModel, gemini); - if (next !== geminiModel) { - setGeminiModel(next); - } - if (localStorage.getItem('gemini-model') !== next) { - localStorage.setItem('gemini-model', next); - } - } - }, [providerModelCatalog.gemini, geminiModel]); - useEffect(() => { const opencode = providerModelCatalog.opencode; if (opencode) { @@ -589,8 +569,6 @@ export function useChatProviderState({ selectedSession, selectedProject: _select setCodexModel, currentProviderEffort, currentProviderEffortOptions, - geminiModel, - setGeminiModel, opencodeModel, setOpenCodeModel, permissionMode, diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index f46d3f1a..2d7a8dcb 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -71,8 +71,6 @@ function ChatInterface({ setCodexModel, currentProviderEffort, currentProviderEffortOptions, - geminiModel, - setGeminiModel, opencodeModel, setOpenCodeModel, permissionMode, @@ -204,7 +202,6 @@ function ChatInterface({ claudeModel, codexModel, currentProviderEffort, - geminiModel, opencodeModel, isLoading: isProcessing, canAbortSession, @@ -300,9 +297,7 @@ function ChatInterface({ ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') - : provider === 'gemini' - ? t('messageTypes.gemini') - : provider === 'opencode' + : provider === 'opencode' ? t('messageTypes.opencode', { defaultValue: 'OpenCode' }) : t('messageTypes.claude'); @@ -342,8 +337,6 @@ function ChatInterface({ setCursorModel={setCursorModel} codexModel={codexModel} setCodexModel={setCodexModel} - geminiModel={geminiModel} - setGeminiModel={setGeminiModel} opencodeModel={opencodeModel} setOpenCodeModel={setOpenCodeModel} providerModelCatalog={providerModelCatalog} @@ -451,9 +444,7 @@ function ChatInterface({ ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') - : provider === 'gemini' - ? t('messageTypes.gemini') - : provider === 'opencode' + : provider === 'opencode' ? t('messageTypes.opencode', { defaultValue: 'OpenCode' }) : t('messageTypes.claude'), })} diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index ccf6ea21..23ffaef3 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -38,8 +38,6 @@ interface ChatMessagesPaneProps { setCursorModel: (model: string) => void; codexModel: string; setCodexModel: (model: string) => void; - geminiModel: string; - setGeminiModel: (model: string) => void; opencodeModel: string; setOpenCodeModel: (model: string) => void; providerModelCatalog: Partial>; @@ -88,8 +86,6 @@ function ChatMessagesPane({ setCursorModel, codexModel, setCodexModel, - geminiModel, - setGeminiModel, opencodeModel, setOpenCodeModel, providerModelCatalog, @@ -189,8 +185,6 @@ function ChatMessagesPane({ setCursorModel={setCursorModel} codexModel={codexModel} setCodexModel={setCodexModel} - geminiModel={geminiModel} - setGeminiModel={setGeminiModel} opencodeModel={opencodeModel} setOpenCodeModel={setOpenCodeModel} providerModelCatalog={providerModelCatalog} diff --git a/src/components/chat/view/subcomponents/CommandResultModal.tsx b/src/components/chat/view/subcomponents/CommandResultModal.tsx index fc71aa36..5096734d 100644 --- a/src/components/chat/view/subcomponents/CommandResultModal.tsx +++ b/src/components/chat/view/subcomponents/CommandResultModal.tsx @@ -61,7 +61,6 @@ const PROVIDER_LABELS: Record = { claude: 'Claude', cursor: 'Cursor', codex: 'Codex', - gemini: 'Gemini', opencode: 'OpenCode', }; diff --git a/src/components/chat/view/subcomponents/MessageComponent.tsx b/src/components/chat/view/subcomponents/MessageComponent.tsx index b1a07878..5d116f25 100644 --- a/src/components/chat/view/subcomponents/MessageComponent.tsx +++ b/src/components/chat/view/subcomponents/MessageComponent.tsx @@ -149,9 +149,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, s ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') - : provider === 'gemini' - ? t('messageTypes.gemini') - : provider === 'opencode' + : provider === 'opencode' ? t('messageTypes.opencode', { defaultValue: 'OpenCode' }) : t('messageTypes.claude'))} diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx index 1c50c8b1..6772ae09 100644 --- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx +++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx @@ -26,7 +26,6 @@ import { const PROVIDER_META: { id: LLMProvider; name: string }[] = [ { id: "claude", name: "Anthropic" }, { id: "codex", name: "OpenAI" }, - { id: "gemini", name: "Google" }, { id: "cursor", name: "Cursor" }, { id: "opencode", name: "OpenCode" }, ]; @@ -57,8 +56,6 @@ type ProviderSelectionEmptyStateProps = { setCursorModel: (model: string) => void; codexModel: string; setCodexModel: (model: string) => void; - geminiModel: string; - setGeminiModel: (model: string) => void; opencodeModel: string; setOpenCodeModel: (model: string) => void; providerModelCatalog: Partial>; @@ -88,12 +85,10 @@ function getCurrentModel( c: string, cu: string, co: string, - g: string, o: string, ) { if (p === "claude") return c; if (p === "codex") return co; - if (p === "gemini") return g; if (p === "opencode") return o; return cu; } @@ -103,7 +98,7 @@ function getProviderDisplayName(p: LLMProvider) { if (p === "cursor") return "Cursor"; if (p === "codex") return "Codex"; if (p === "opencode") return "OpenCode"; - return "Gemini"; + return "Claude"; } export default function ProviderSelectionEmptyState({ @@ -118,8 +113,6 @@ export default function ProviderSelectionEmptyState({ setCursorModel, codexModel, setCodexModel, - geminiModel, - setGeminiModel, opencodeModel, setOpenCodeModel, providerModelCatalog, @@ -149,7 +142,6 @@ export default function ProviderSelectionEmptyState({ claudeModel, cursorModel, codexModel, - geminiModel, opencodeModel, ); @@ -169,9 +161,6 @@ export default function ProviderSelectionEmptyState({ } else if (providerId === "codex") { setCodexModel(modelValue); localStorage.setItem("codex-model", modelValue); - } else if (providerId === "gemini") { - setGeminiModel(modelValue); - localStorage.setItem("gemini-model", modelValue); } else if (providerId === "opencode") { setOpenCodeModel(modelValue); localStorage.setItem("opencode-model", modelValue); @@ -180,7 +169,7 @@ export default function ProviderSelectionEmptyState({ localStorage.setItem("cursor-model", modelValue); } }, - [setClaudeModel, setCursorModel, setCodexModel, setGeminiModel, setOpenCodeModel], + [setClaudeModel, setCursorModel, setCodexModel, setOpenCodeModel], ); const handleModelSelect = useCallback( @@ -323,9 +312,6 @@ export default function ProviderSelectionEmptyState({ codex: t("providerSelection.readyPrompt.codex", { model: codexModel, }), - gemini: t("providerSelection.readyPrompt.gemini", { - model: geminiModel, - }), opencode: t("providerSelection.readyPrompt.opencode", { model: opencodeModel, defaultValue: "Ready with OpenCode {{model}}", diff --git a/src/components/llm-logo-provider/GeminiLogo.tsx b/src/components/llm-logo-provider/GeminiLogo.tsx deleted file mode 100644 index 17212bd1..00000000 --- a/src/components/llm-logo-provider/GeminiLogo.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { useId } from 'react'; - -type GeminiLogoProps = { - className?: string; -}; - -const GeminiLogo = ({ className = 'w-5 h-5' }: GeminiLogoProps) => { - const id = useId().replace(/:/g, ''); - const maskId = `${id}-gemini-mask`; - const gradientId = `${id}-gemini-gradient`; - const filterIds = Array.from({ length: 11 }, (_, index) => `${id}-gemini-filter-${index}`); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default GeminiLogo; diff --git a/src/components/llm-logo-provider/SessionProviderLogo.tsx b/src/components/llm-logo-provider/SessionProviderLogo.tsx index e29ecd6d..b93d9eb5 100644 --- a/src/components/llm-logo-provider/SessionProviderLogo.tsx +++ b/src/components/llm-logo-provider/SessionProviderLogo.tsx @@ -2,7 +2,6 @@ import type { LLMProvider } from '../../types/app'; import ClaudeLogo from './ClaudeLogo'; import CodexLogo from './CodexLogo'; import CursorLogo from './CursorLogo'; -import GeminiLogo from './GeminiLogo'; import OpenCodeLogo from './OpenCodeLogo'; type SessionProviderLogoProps = { @@ -22,10 +21,6 @@ export default function SessionProviderLogo({ return ; } - if (provider === 'gemini') { - return ; - } - if (provider === 'opencode') { return ; } diff --git a/src/components/mcp/constants.ts b/src/components/mcp/constants.ts index 2e23c5cb..5a1848d6 100644 --- a/src/components/mcp/constants.ts +++ b/src/components/mcp/constants.ts @@ -4,7 +4,6 @@ export const MCP_PROVIDER_NAMES: Record = { claude: 'Claude', cursor: 'Cursor', codex: 'Codex', - gemini: 'Gemini', opencode: 'OpenCode', }; @@ -12,7 +11,6 @@ export const MCP_SUPPORTED_SCOPES: Record = { claude: ['user', 'project', 'local'], cursor: ['user', 'project'], codex: ['user', 'project'], - gemini: ['user', 'project'], opencode: ['user', 'project'], }; @@ -20,7 +18,6 @@ export const MCP_SUPPORTED_TRANSPORTS: Record = { claude: ['stdio', 'http', 'sse'], cursor: ['stdio', 'http'], codex: ['stdio', 'http'], - gemini: ['stdio', 'http', 'sse'], opencode: ['stdio', 'http'], }; @@ -32,7 +29,6 @@ export const MCP_PROVIDER_BUTTON_CLASSES: Record = { claude: 'bg-primary text-primary-foreground hover:bg-primary/90', cursor: 'bg-primary text-primary-foreground hover:bg-primary/90', codex: 'bg-primary text-primary-foreground hover:bg-primary/90', - gemini: 'bg-primary text-primary-foreground hover:bg-primary/90', opencode: 'bg-primary text-primary-foreground hover:bg-primary/90', }; @@ -40,7 +36,6 @@ export const MCP_SUPPORTS_WORKING_DIRECTORY: Record = { claude: false, cursor: false, codex: true, - gemini: true, opencode: false, }; diff --git a/src/components/mcp/view/McpServers.tsx b/src/components/mcp/view/McpServers.tsx index 327fd740..a1436fe9 100644 --- a/src/components/mcp/view/McpServers.tsx +++ b/src/components/mcp/view/McpServers.tsx @@ -127,9 +127,9 @@ export default function McpServers({ selectedProvider, currentProjects }: McpSer }); const globalButtonLabel = 'Add Global MCP Server'; const providerButtonLabel = `Add ${providerName} MCP Server`; - const globalAddDescription = 'Add Global MCP Server writes one common stdio or HTTP server to Claude, Cursor, Codex, and Gemini.'; + const globalAddDescription = 'Add Global MCP Server writes one common stdio or HTTP server to Claude, Cursor, Codex, and OpenCode.'; const providerAddDescription = `${providerButtonLabel} only changes ${providerName}.`; - const globalModalDescription = 'Adds this MCP server to every provider: Claude, Cursor, Codex, and Gemini. ' + const globalModalDescription = 'Adds this MCP server to every provider: Claude, Cursor, Codex, and OpenCode. ' + 'Only stdio and HTTP transports are supported because the same config must work across all providers.'; return ( diff --git a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx index 776e9823..d9148a95 100644 --- a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx +++ b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx @@ -30,13 +30,6 @@ const providerCards = [ iconContainerClassName: 'bg-gray-100 dark:bg-gray-800', loginButtonClassName: 'bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600', }, - { - provider: 'gemini' as const, - title: 'Gemini', - connectedClassName: 'bg-teal-50 dark:bg-teal-900/20 border-teal-200 dark:border-teal-800', - iconContainerClassName: 'bg-teal-100 dark:bg-teal-900/30', - loginButtonClassName: 'bg-teal-600 hover:bg-teal-700', - }, { provider: 'opencode' as const, title: 'OpenCode', diff --git a/src/components/provider-auth/types.ts b/src/components/provider-auth/types.ts index afa08094..5511c184 100644 --- a/src/components/provider-auth/types.ts +++ b/src/components/provider-auth/types.ts @@ -10,13 +10,12 @@ export type ProviderAuthStatus = { export type ProviderAuthStatusMap = Record; -export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'gemini', 'opencode']; +export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'opencode']; export const PROVIDER_AUTH_STATUS_ENDPOINTS: Record = { claude: '/api/providers/claude/auth/status', cursor: '/api/providers/cursor/auth/status', codex: '/api/providers/codex/auth/status', - gemini: '/api/providers/gemini/auth/status', opencode: '/api/providers/opencode/auth/status', }; @@ -24,6 +23,5 @@ export const createInitialProviderAuthStatusMap = (loading = true): ProviderAuth claude: { authenticated: false, email: null, method: null, error: null, loading }, cursor: { authenticated: false, email: null, method: null, error: null, loading }, codex: { authenticated: false, email: null, method: null, error: null, loading }, - gemini: { authenticated: false, email: null, method: null, error: null, loading }, opencode: { authenticated: false, email: null, method: null, error: null, loading }, }); diff --git a/src/components/provider-auth/view/ProviderLoginModal.tsx b/src/components/provider-auth/view/ProviderLoginModal.tsx index 9de1d227..2a0cfc2a 100644 --- a/src/components/provider-auth/view/ProviderLoginModal.tsx +++ b/src/components/provider-auth/view/ProviderLoginModal.tsx @@ -1,4 +1,4 @@ -import { ExternalLink, KeyRound, X } from 'lucide-react'; +import { X } from 'lucide-react'; import StandaloneShell from '../../standalone-shell/view/StandaloneShell'; import { DEFAULT_PROJECT_FOR_EMPTY_SHELL, IS_PLATFORM } from '../../../constants/config'; import type { LLMProvider } from '../../../types/app'; @@ -41,7 +41,7 @@ const getProviderCommand = ({ return 'opencode auth login'; } - return 'gemini status'; + return 'claude --dangerously-skip-permissions /login'; }; const getProviderTitle = (provider: LLMProvider) => { @@ -49,7 +49,7 @@ const getProviderTitle = (provider: LLMProvider) => { if (provider === 'cursor') return 'Cursor CLI Login'; if (provider === 'codex') return 'Codex CLI Login'; if (provider === 'opencode') return 'OpenCode CLI Login'; - return 'Gemini CLI Configuration'; + return 'Claude CLI Login'; }; export default function ProviderLoginModal({ @@ -87,61 +87,7 @@ export default function ProviderLoginModal({
- {provider === 'gemini' ? ( -
-
- -
- -

Setup Gemini API Access

- -

- The Gemini CLI requires an API key to function. Configure it in your terminal first. -

- -
-
    -
  1. -
    - 1 -
    -
    -

    Get your API key

    - - Google AI Studio - -
    -
  2. -
  3. -
    - 2 -
    -
    -

    Run configuration

    -

    Open your terminal and run:

    - - gemini config set api_key YOUR_KEY - -
    -
  4. -
-
- - -
- ) : ( - - )} +
diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts index 366c5989..8c429083 100644 --- a/src/components/settings/constants/constants.ts +++ b/src/components/settings/constants/constants.ts @@ -39,7 +39,7 @@ export const SETTINGS_MAIN_TABS: SettingsMainTabMeta[] = [ { id: 'about', label: 'About', keywords: 'about version info', icon: Info }, ]; -export const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'gemini', 'opencode']; +export const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'opencode']; export const AGENT_CATEGORIES: AgentCategory[] = ['account', 'permissions', 'mcp']; export const DEFAULT_PROJECT_SORT_ORDER: ProjectSortOrder = 'name'; diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts index e42a1e1b..cc57d377 100644 --- a/src/components/settings/hooks/useSettingsController.ts +++ b/src/components/settings/hooks/useSettingsController.ts @@ -14,7 +14,6 @@ import type { CodeEditorSettingsState, CodexPermissionMode, CursorPermissionsState, - GeminiPermissionMode, NotificationPreferencesState, ProjectSortOrder, SettingsMainTab, @@ -159,7 +158,6 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl createDefaultNotificationPreferences() )); const [codexPermissionMode, setCodexPermissionMode] = useState('default'); - const [geminiPermissionMode, setGeminiPermissionMode] = useState('default'); const [showLoginModal, setShowLoginModal] = useState(false); const [loginProvider, setLoginProvider] = useState(''); @@ -198,12 +196,6 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl ); setCodexPermissionMode(toCodexPermissionMode(savedCodexSettings.permissionMode)); - const savedGeminiSettings = parseJson<{ permissionMode?: GeminiPermissionMode }>( - localStorage.getItem('gemini-settings'), - {}, - ); - setGeminiPermissionMode(savedGeminiSettings.permissionMode || 'default'); - try { const notificationResponse = await authenticatedFetch('/api/settings/notification-preferences'); if (notificationResponse.ok) { @@ -276,11 +268,6 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl lastUpdated: now, })); - localStorage.setItem('gemini-settings', JSON.stringify({ - permissionMode: geminiPermissionMode, - lastUpdated: now, - })); - const notificationResponse = await authenticatedFetch('/api/settings/notification-preferences', { method: 'PUT', body: JSON.stringify(notificationPreferences), @@ -303,7 +290,6 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl cursorPermissions.disallowedCommands, cursorPermissions.skipPermissions, notificationPreferences, - geminiPermissionMode, projectSortOrder, ]); @@ -409,8 +395,6 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl codexPermissionMode, setCodexPermissionMode, providerAuthStatus, - geminiPermissionMode, - setGeminiPermissionMode, openLoginForProvider, showLoginModal, setShowLoginModal, diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts index c6801f39..514de2f3 100644 --- a/src/components/settings/types/types.ts +++ b/src/components/settings/types/types.ts @@ -9,7 +9,6 @@ export type AgentCategory = 'account' | 'permissions' | 'mcp' | 'skills'; export type ProjectSortOrder = 'name' | 'date'; export type SaveStatus = 'success' | 'error' | null; export type CodexPermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions'; -export type GeminiPermissionMode = 'default' | 'auto_edit' | 'yolo'; export type SettingsProject = { name: string; diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx index 72499556..37f40bc9 100644 --- a/src/components/settings/view/Settings.tsx +++ b/src/components/settings/view/Settings.tsx @@ -52,8 +52,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set codexPermissionMode, setCodexPermissionMode, providerAuthStatus, - geminiPermissionMode, - setGeminiPermissionMode, openLoginForProvider, showLoginModal, setShowLoginModal, @@ -187,8 +185,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set onCursorPermissionsChange={setCursorPermissions} codexPermissionMode={codexPermissionMode} onCodexPermissionModeChange={setCodexPermissionMode} - geminiPermissionMode={geminiPermissionMode} - onGeminiPermissionModeChange={setGeminiPermissionMode} projects={projects} /> )} diff --git a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx index b23784ee..52e87c32 100644 --- a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx +++ b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx @@ -12,7 +12,7 @@ type AgentListItemProps = { type AgentConfig = { name: string; - color: 'blue' | 'purple' | 'gray' | 'indigo' | 'zinc'; + color: 'blue' | 'purple' | 'gray' | 'zinc'; }; const agentConfig: Record = { @@ -28,10 +28,6 @@ const agentConfig: Record = { name: 'Codex', color: 'gray', }, - gemini: { - name: 'Gemini', - color: 'indigo', - }, opencode: { name: 'OpenCode', color: 'zinc', @@ -48,9 +44,6 @@ const colorClasses = { gray: { dot: 'bg-foreground/60', }, - indigo: { - dot: 'bg-indigo-500', - }, zinc: { dot: 'bg-zinc-500', }, diff --git a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx index a5221f83..31216a10 100644 --- a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx +++ b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx @@ -16,8 +16,6 @@ export default function AgentsSettingsTab({ onCursorPermissionsChange, codexPermissionMode, onCodexPermissionModeChange, - geminiPermissionMode, - onGeminiPermissionModeChange, projects, }: AgentsSettingsTabProps) { const [selectedAgent, setSelectedAgent] = useState('claude'); @@ -29,7 +27,7 @@ export default function AgentsSettingsTab({ ), [selectedAgent]); const visibleAgents = useMemo(() => { - return ['claude', 'cursor', 'codex', 'gemini', 'opencode']; + return ['claude', 'cursor', 'codex', 'opencode']; }, []); const agentContextById = useMemo>(() => ({ @@ -45,10 +43,6 @@ export default function AgentsSettingsTab({ authStatus: providerAuthStatus.codex, onLogin: () => onProviderLogin('codex'), }, - gemini: { - authStatus: providerAuthStatus.gemini, - onLogin: () => onProviderLogin('gemini'), - }, opencode: { authStatus: providerAuthStatus.opencode, onLogin: () => onProviderLogin('opencode'), @@ -58,7 +52,6 @@ export default function AgentsSettingsTab({ providerAuthStatus.claude, providerAuthStatus.codex, providerAuthStatus.cursor, - providerAuthStatus.gemini, providerAuthStatus.opencode, ]); @@ -95,8 +88,6 @@ export default function AgentsSettingsTab({ onCursorPermissionsChange={onCursorPermissionsChange} codexPermissionMode={codexPermissionMode} onCodexPermissionModeChange={onCodexPermissionModeChange} - geminiPermissionMode={geminiPermissionMode} - onGeminiPermissionModeChange={onGeminiPermissionModeChange} projects={projects} /> diff --git a/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx b/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx index a6d017fa..e25733b4 100644 --- a/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx +++ b/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx @@ -7,7 +7,6 @@ const AGENT_NAMES: Record = { claude: 'Claude', cursor: 'Cursor', codex: 'Codex', - gemini: 'Gemini', opencode: 'OpenCode', }; @@ -24,7 +23,6 @@ export default function AgentSelectorSection({ const dotColor = agent === 'claude' ? 'bg-blue-500' : agent === 'cursor' ? 'bg-purple-500' : - agent === 'gemini' ? 'bg-indigo-500' : agent === 'opencode' ? 'bg-zinc-500' : 'bg-foreground/60'; return ( diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx index c0ed69d5..83fbb249 100644 --- a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx +++ b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx @@ -45,15 +45,6 @@ const agentConfig: Record = { subtextClass: 'text-gray-700 dark:text-gray-300', buttonClass: 'bg-gray-800 hover:bg-gray-900 active:bg-gray-950 dark:bg-gray-700 dark:hover:bg-gray-600 dark:active:bg-gray-500', }, - gemini: { - name: 'Gemini', - description: 'Google Gemini AI assistant', - bgClass: 'bg-indigo-50 dark:bg-indigo-900/20', - borderClass: 'border-indigo-200 dark:border-indigo-800', - textClass: 'text-indigo-900 dark:text-indigo-100', - subtextClass: 'text-indigo-700 dark:text-indigo-300', - buttonClass: 'bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800', - }, opencode: { name: 'OpenCode', description: 'OpenCode CLI assistant', diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/PermissionsContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/PermissionsContent.tsx index fa545aaa..6c41cfa6 100644 --- a/src/components/settings/view/tabs/agents-settings/sections/content/PermissionsContent.tsx +++ b/src/components/settings/view/tabs/agents-settings/sections/content/PermissionsContent.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { AlertTriangle, Plus, Shield, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button, Input } from '../../../../../../../shared/view/ui'; -import type { CodexPermissionMode, GeminiPermissionMode } from '../../../../../types/types'; +import type { CodexPermissionMode } from '../../../../../types/types'; const COMMON_CLAUDE_TOOLS = [ 'Bash(git log:*)', @@ -579,111 +579,7 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit void; -}; - -// Gemini Permissions -function GeminiPermissions({ permissionMode, onPermissionModeChange }: Omit) { - const { t } = useTranslation(['settings', 'chat']); - return ( -
-
-
- -

- {t('gemini.permissionMode')} -

-
-

- {t('gemini.description')} -

- - {/* Default Mode */} -
onPermissionModeChange('default')} - > - -
- - {/* Auto Edit Mode */} -
onPermissionModeChange('auto_edit')} - > - -
- - {/* YOLO Mode */} -
onPermissionModeChange('yolo')} - > - -
-
-
- ); -} - -type PermissionsContentProps = ClaudePermissionsProps | CursorPermissionsProps | CodexPermissionsProps | GeminiPermissionsProps; +type PermissionsContentProps = ClaudePermissionsProps | CursorPermissionsProps | CodexPermissionsProps; export default function PermissionsContent(props: PermissionsContentProps) { if (props.agent === 'claude') { @@ -694,9 +590,5 @@ export default function PermissionsContent(props: PermissionsContentProps) { return ; } - if (props.agent === 'gemini') { - return ; - } - return ; } diff --git a/src/components/settings/view/tabs/agents-settings/types.ts b/src/components/settings/view/tabs/agents-settings/types.ts index 731ce8d3..a5ed788e 100644 --- a/src/components/settings/view/tabs/agents-settings/types.ts +++ b/src/components/settings/view/tabs/agents-settings/types.ts @@ -5,7 +5,6 @@ import type { ClaudePermissionsState, CursorPermissionsState, CodexPermissionMode, - GeminiPermissionMode, SettingsProject, } from '../../../types/types'; @@ -26,8 +25,6 @@ export type AgentsSettingsTabProps = { onCursorPermissionsChange: (value: CursorPermissionsState) => void; codexPermissionMode: CodexPermissionMode; onCodexPermissionModeChange: (value: CodexPermissionMode) => void; - geminiPermissionMode: GeminiPermissionMode; - onGeminiPermissionModeChange: (value: GeminiPermissionMode) => void; projects: SettingsProject[]; }; @@ -55,7 +52,5 @@ export type AgentCategoryContentSectionProps = { onCursorPermissionsChange: (value: CursorPermissionsState) => void; codexPermissionMode: CodexPermissionMode; onCodexPermissionModeChange: (value: CodexPermissionMode) => void; - geminiPermissionMode: GeminiPermissionMode; - onGeminiPermissionModeChange: (value: GeminiPermissionMode) => void; projects: SettingsProject[]; }; diff --git a/src/components/skills/view/ProviderSkills.tsx b/src/components/skills/view/ProviderSkills.tsx index 42f7159c..908ed076 100644 --- a/src/components/skills/view/ProviderSkills.tsx +++ b/src/components/skills/view/ProviderSkills.tsx @@ -59,7 +59,6 @@ const PROVIDER_NAMES: Record = { claude: 'Claude', codex: 'Codex', cursor: 'Cursor', - gemini: 'Gemini', opencode: 'OpenCode', }; @@ -67,7 +66,6 @@ const PROVIDER_SKILL_PATHS: Record, string> claude: '~/.claude/skills//SKILL.md', codex: '~/.agents/skills//SKILL.md', cursor: '~/.cursor/skills//SKILL.md', - gemini: '~/.gemini/skills//SKILL.md', }; const SCOPE_LABELS: Record = { diff --git a/src/i18n/locales/de/chat.json b/src/i18n/locales/de/chat.json index f0b10d7a..244cdd12 100644 --- a/src/i18n/locales/de/chat.json +++ b/src/i18n/locales/de/chat.json @@ -17,8 +17,7 @@ "tool": "Werkzeug", "claude": "Claude", "cursor": "Cursor", - "codex": "Codex", - "gemini": "Gemini" + "codex": "Codex" }, "tools": { "settings": "Werkzeugeinstellungen", @@ -103,24 +102,6 @@ }, "technicalDetails": "Technische Details" }, - "gemini": { - "permissionMode": "Gemini-Berechtigungsmodus", - "description": "Steuert, wie Gemini CLI Vorgangsgenehmigungen handhabt.", - "modes": { - "default": { - "title": "Standard (Genehmigung erforderlich)", - "description": "Gemini fordert vor der Ausführung von Befehlen, dem Schreiben von Dateien und dem Abrufen von Web-Ressourcen eine Genehmigung an." - }, - "autoEdit": { - "title": "Auto-Bearbeitung (Dateigenehmigungen überspringen)", - "description": "Gemini genehmigt automatisch Dateibearbeitungen und Web-Abrufe, fragt aber weiterhin bei Shell-Befehlen nach." - }, - "yolo": { - "title": "YOLO (Alle Berechtigungen umgehen)", - "description": "Gemini führt alle Vorgänge ohne Rückfrage aus. Vorsicht ist geboten." - } - } - }, "input": { "placeholder": "/ für Befehle, @ für Dateien eingeben oder {{provider}} etwas fragen...", "placeholderDefault": "Nachricht eingeben...", @@ -152,7 +133,6 @@ "claude": "Bereit, Claude mit {{model}} zu verwenden. Gib unten deine Nachricht ein.", "cursor": "Bereit, Cursor mit {{model}} zu verwenden. Gib unten deine Nachricht ein.", "codex": "Bereit, Codex mit {{model}} zu verwenden. Gib unten deine Nachricht ein.", - "gemini": "Bereit, Gemini mit {{model}} zu verwenden. Gib unten deine Nachricht ein.", "default": "Wähl oben einen Anbieter, um zu beginnen" }, "pressToSearch": "Drücke {{shortcut}}, um Sitzungen, Dateien und Commits zu durchsuchen" diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index 958735bd..94a1516c 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -322,9 +322,6 @@ }, "codex": { "description": "OpenAI Codex KI-Assistent" - }, - "gemini": { - "description": "Google Gemini KI-Assistent" } }, "connectionStatus": "Verbindungsstatus", diff --git a/src/i18n/locales/en/chat.json b/src/i18n/locales/en/chat.json index d6f21c25..87f22adb 100644 --- a/src/i18n/locales/en/chat.json +++ b/src/i18n/locales/en/chat.json @@ -18,7 +18,6 @@ "claude": "Claude", "cursor": "Cursor", "codex": "Codex", - "gemini": "Gemini", "opencode": "OpenCode" }, "tools": { @@ -104,24 +103,6 @@ }, "technicalDetails": "Technical details" }, - "gemini": { - "permissionMode": "Gemini Permission Mode", - "description": "Control how Gemini CLI handles operation approvals.", - "modes": { - "default": { - "title": "Standard (Ask for Approval)", - "description": "Gemini will prompt for approval before executing commands, writing files, and fetching web resources." - }, - "autoEdit": { - "title": "Auto Edit (Skip File Approvals)", - "description": "Gemini will automatically approve file edits and web fetches, but will still prompt for shell commands." - }, - "yolo": { - "title": "YOLO (Bypass All Permissions)", - "description": "Gemini will execute all operations without asking for approval. Exercise caution." - } - } - }, "voice": { "input": "Voice input", "stopRecording": "Stop recording", @@ -171,7 +152,6 @@ "claude": "Ready to use Claude with {{model}}. Start typing your message below.", "cursor": "Ready to use Cursor with {{model}}. Start typing your message below.", "codex": "Ready to use Codex with {{model}}. Start typing your message below.", - "gemini": "Ready to use Gemini with {{model}}. Start typing your message below.", "opencode": "Ready to use OpenCode with {{model}}. Start typing your message below.", "default": "Select a provider above to begin" }, diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index aabfc143..079dfe46 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -349,9 +349,6 @@ "codex": { "description": "OpenAI Codex AI assistant" }, - "gemini": { - "description": "Google Gemini AI assistant" - }, "opencode": { "description": "OpenCode CLI assistant" } diff --git a/src/i18n/locales/fr/chat.json b/src/i18n/locales/fr/chat.json index 524c8d00..89dcb7b3 100644 --- a/src/i18n/locales/fr/chat.json +++ b/src/i18n/locales/fr/chat.json @@ -18,7 +18,6 @@ "claude": "Claude", "cursor": "Cursor", "codex": "Codex", - "gemini": "Gemini", "opencode": "OpenCode" }, "tools": { @@ -104,24 +103,6 @@ }, "technicalDetails": "Détails techniques" }, - "gemini": { - "permissionMode": "Mode de permission Gemini", - "description": "Contrôlez comment Gemini CLI gère les approbations d'opérations.", - "modes": { - "default": { - "title": "Standard (demander l'approbation)", - "description": "Gemini demandera une approbation avant d'exécuter des commandes, d'écrire des fichiers et de récupérer des ressources web." - }, - "autoEdit": { - "title": "Modification automatique (ignorer les approbations de fichiers)", - "description": "Gemini approuvera automatiquement les modifications de fichiers et les récupérations web, mais demandera toujours pour les commandes shell." - }, - "yolo": { - "title": "YOLO (contourner toutes les permissions)", - "description": "Gemini exécutera toutes les opérations sans demander d'approbation. Utilisez avec prudence." - } - } - }, "input": { "placeholder": "Tapez / pour les commandes, @ pour les fichiers, ou posez une question à {{provider}}...", "placeholderDefault": "Tapez votre message...", @@ -153,7 +134,6 @@ "claude": "Prêt à utiliser Claude avec {{model}}. Commencez à taper votre message ci-dessous.", "cursor": "Prêt à utiliser Cursor avec {{model}}. Commencez à taper votre message ci-dessous.", "codex": "Prêt à utiliser Codex avec {{model}}. Commencez à taper votre message ci-dessous.", - "gemini": "Prêt à utiliser Gemini avec {{model}}. Commencez à taper votre message ci-dessous.", "opencode": "Prêt à utiliser OpenCode avec {{model}}. Commencez à taper votre message ci-dessous.", "default": "Sélectionnez un fournisseur ci-dessus pour commencer" }, diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index bb25cd8a..bf8994ed 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -323,9 +323,6 @@ "codex": { "description": "Assistant IA Codex d'OpenAI" }, - "gemini": { - "description": "Assistant IA Gemini de Google" - }, "opencode": { "description": "Assistant CLI OpenCode" } diff --git a/src/i18n/locales/it/chat.json b/src/i18n/locales/it/chat.json index ae845d9a..71f4ac3d 100644 --- a/src/i18n/locales/it/chat.json +++ b/src/i18n/locales/it/chat.json @@ -17,8 +17,7 @@ "tool": "Strumento", "claude": "Claude", "cursor": "Cursor", - "codex": "Codex", - "gemini": "Gemini" + "codex": "Codex" }, "tools": { "settings": "Impostazioni strumento", @@ -103,24 +102,6 @@ }, "technicalDetails": "Dettagli tecnici" }, - "gemini": { - "permissionMode": "Modalità permessi Gemini", - "description": "Controlla come Gemini CLI gestisce le approvazioni delle operazioni.", - "modes": { - "default": { - "title": "Standard (chiedi approvazione)", - "description": "Gemini chiederà l'approvazione prima di eseguire comandi, scrivere file e recuperare risorse web." - }, - "autoEdit": { - "title": "Modifica automatica (salta approvazioni file)", - "description": "Gemini approverà automaticamente modifiche ai file e recupero web, ma chiederà conferma per i comandi shell." - }, - "yolo": { - "title": "YOLO (ignora tutti i permessi)", - "description": "Gemini eseguirà tutte le operazioni senza chiedere approvazione. Usa con cautela." - } - } - }, "input": { "placeholder": "Digita / per i comandi, @ per i file, o chiedi qualcosa a {{provider}}...", "placeholderDefault": "Scrivi il tuo messaggio...", @@ -152,7 +133,6 @@ "claude": "Pronto a usare Claude con {{model}}. Inizia a digitare il tuo messaggio qui sotto.", "cursor": "Pronto a usare Cursor con {{model}}. Inizia a digitare il tuo messaggio qui sotto.", "codex": "Pronto a usare Codex con {{model}}. Inizia a digitare il tuo messaggio qui sotto.", - "gemini": "Pronto a usare Gemini con {{model}}. Inizia a digitare il tuo messaggio qui sotto.", "default": "Seleziona un provider sopra per iniziare" }, "pressToSearch": "Premi {{shortcut}} per cercare sessioni, file e commit" diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json index 45f320f1..914c6aa5 100644 --- a/src/i18n/locales/it/settings.json +++ b/src/i18n/locales/it/settings.json @@ -322,9 +322,6 @@ }, "codex": { "description": "Assistente AI OpenAI Codex" - }, - "gemini": { - "description": "Assistente AI Google Gemini" } }, "connectionStatus": "Stato connessione", diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json index ee3b7f20..abe4df4f 100644 --- a/src/i18n/locales/ja/settings.json +++ b/src/i18n/locales/ja/settings.json @@ -322,9 +322,6 @@ }, "codex": { "description": "OpenAI Codex AIアシスタント" - }, - "gemini": { - "description": "Google Gemini AIアシスタント" } }, "connectionStatus": "接続状態", diff --git a/src/i18n/locales/ko/chat.json b/src/i18n/locales/ko/chat.json index c9df5c2e..7c7f7da3 100644 --- a/src/i18n/locales/ko/chat.json +++ b/src/i18n/locales/ko/chat.json @@ -17,8 +17,7 @@ "tool": "도구", "claude": "Claude", "cursor": "Cursor", - "codex": "Codex", - "gemini": "Gemini" + "codex": "Codex" }, "tools": { "settings": "도구 설정", @@ -134,7 +133,6 @@ "claude": "{{model}} 모델로 Claude를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.", "cursor": "{{model}} 모델로 Cursor를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.", "codex": "{{model}} 모델로 Codex를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.", - "gemini": "{{model}} 모델로 Gemini를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.", "default": "시작하려면 위에서 제공자를 선택하세요" }, "pressToSearch": "{{shortcut}}를 눌러 세션, 파일 및 커밋을 검색하세요" diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index 6a23ac6c..8577ecd4 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -322,9 +322,6 @@ }, "codex": { "description": "OpenAI Codex AI 어시스턴트" - }, - "gemini": { - "description": "Google Gemini AI 어시스턴트" } }, "connectionStatus": "연결 상태", diff --git a/src/i18n/locales/ru/chat.json b/src/i18n/locales/ru/chat.json index 8d3e9e09..af853ce1 100644 --- a/src/i18n/locales/ru/chat.json +++ b/src/i18n/locales/ru/chat.json @@ -17,8 +17,7 @@ "tool": "Инструмент", "claude": "Claude", "cursor": "Cursor", - "codex": "Codex", - "gemini": "Gemini" + "codex": "Codex" }, "tools": { "settings": "Настройки инструмента", @@ -103,24 +102,6 @@ }, "technicalDetails": "Технические детали" }, - "gemini": { - "permissionMode": "Режим разрешений Gemini", - "description": "Управление тем, как Gemini CLI обрабатывает подтверждения операций.", - "modes": { - "default": { - "title": "Стандартный (запрашивать подтверждение)", - "description": "Gemini будет запрашивать подтверждение перед выполнением команд, записью файлов и получением веб-ресурсов." - }, - "autoEdit": { - "title": "Автоматическое редактирование (пропускать подтверждения файлов)", - "description": "Gemini будет автоматически подтверждать редактирование файлов и веб-запросы, но все еще будет запрашивать подтверждение для команд оболочки." - }, - "yolo": { - "title": "YOLO (обход всех разрешений)", - "description": "Gemini будет выполнять все операции без запроса подтверждения. Будьте осторожны." - } - } - }, "input": { "placeholder": "Введите / для команд, @ для файлов, или спросите {{provider}} что угодно...", "placeholderDefault": "Введите ваше сообщение...", @@ -152,7 +133,6 @@ "claude": "Готов использовать Claude с {{model}}. Начните вводить сообщение ниже.", "cursor": "Готов использовать Cursor с {{model}}. Начните вводить сообщение ниже.", "codex": "Готов использовать Codex с {{model}}. Начните вводить сообщение ниже.", - "gemini": "Готов использовать Gemini с {{model}}. Начните вводить сообщение ниже.", "default": "Выберите провайдера выше для начала" }, "pressToSearch": "Нажмите {{shortcut}}, чтобы искать сессии, файлы и коммиты" diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index 480cbcdf..7e47acda 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -322,9 +322,6 @@ }, "codex": { "description": "AI-ассистент OpenAI Codex" - }, - "gemini": { - "description": "AI-ассистент Google Gemini" } }, "connectionStatus": "Статус подключения", diff --git a/src/i18n/locales/tr/chat.json b/src/i18n/locales/tr/chat.json index 74ce9548..19e6ee4d 100644 --- a/src/i18n/locales/tr/chat.json +++ b/src/i18n/locales/tr/chat.json @@ -17,8 +17,7 @@ "tool": "Araç", "claude": "Claude", "cursor": "Cursor", - "codex": "Codex", - "gemini": "Gemini" + "codex": "Codex" }, "tools": { "settings": "Araç Ayarları", @@ -103,24 +102,6 @@ }, "technicalDetails": "Teknik ayrıntılar" }, - "gemini": { - "permissionMode": "Gemini İzin Modu", - "description": "Gemini CLI'ın işlem onaylarını nasıl yönettiğini kontrol et.", - "modes": { - "default": { - "title": "Standart (Onay İste)", - "description": "Gemini, komut çalıştırmadan, dosya yazmadan ve web kaynağı getirmeden önce onay ister." - }, - "autoEdit": { - "title": "Otomatik Düzenleme (Dosya Onaylarını Atla)", - "description": "Gemini dosya düzenlemelerini ve web getirmelerini otomatik onaylar, ama shell komutları için yine de onay ister." - }, - "yolo": { - "title": "YOLO (Tüm İzinleri Atla)", - "description": "Gemini tüm işlemleri onay almadan çalıştırır. Dikkatli kullan." - } - } - }, "input": { "placeholder": "Komutlar için /, dosyalar için @ yaz ya da {{provider}}'a her şeyi sor...", "placeholderDefault": "Mesajını yaz...", @@ -152,7 +133,6 @@ "claude": "Claude'u {{model}} ile kullanmaya hazır. Mesajını aşağıya yazmaya başla.", "cursor": "Cursor'ı {{model}} ile kullanmaya hazır. Mesajını aşağıya yazmaya başla.", "codex": "Codex'i {{model}} ile kullanmaya hazır. Mesajını aşağıya yazmaya başla.", - "gemini": "Gemini'yi {{model}} ile kullanmaya hazır. Mesajını aşağıya yazmaya başla.", "default": "Başlamak için yukarıdan bir sağlayıcı seç" }, "pressToSearch": "Oturumlarda, dosyalarda ve commit'lerde arama yapmak için {{shortcut}} tuşlarına bas" diff --git a/src/i18n/locales/tr/settings.json b/src/i18n/locales/tr/settings.json index 00b0465b..52377b3a 100644 --- a/src/i18n/locales/tr/settings.json +++ b/src/i18n/locales/tr/settings.json @@ -323,9 +323,6 @@ }, "codex": { "description": "OpenAI Codex AI asistanı" - }, - "gemini": { - "description": "Google Gemini AI asistanı" } }, "connectionStatus": "Bağlantı Durumu", diff --git a/src/i18n/locales/zh-CN/chat.json b/src/i18n/locales/zh-CN/chat.json index 8ebe68d1..67abca24 100644 --- a/src/i18n/locales/zh-CN/chat.json +++ b/src/i18n/locales/zh-CN/chat.json @@ -17,8 +17,7 @@ "tool": "工具", "claude": "Claude", "cursor": "Cursor", - "codex": "Codex", - "gemini": "Gemini" + "codex": "Codex" }, "tools": { "settings": "工具设置", @@ -134,7 +133,6 @@ "claude": "准备好使用带有 {{model}} 的 Claude。请在下方开始输入您的消息。", "cursor": "准备好使用带有 {{model}} 的 Cursor。请在下方开始输入您的消息。", "codex": "准备好使用带有 {{model}} 的 Codex。请在下方开始输入您的消息。", - "gemini": "准备好使用带有 {{model}} 的 Gemini。请在下方开始输入您的消息。", "default": "请在上方选择一个提供者以开始" }, "pressToSearch": "按 {{shortcut}} 搜索会话、文件和提交" diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index e383f84f..381c10b8 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -323,9 +323,6 @@ }, "codex": { "description": "OpenAI Codex AI 助手" - }, - "gemini": { - "description": "Google Gemini AI 助手" } }, "connectionStatus": "连接状态", diff --git a/src/i18n/locales/zh-TW/chat.json b/src/i18n/locales/zh-TW/chat.json index 1440bd52..4474d7ee 100644 --- a/src/i18n/locales/zh-TW/chat.json +++ b/src/i18n/locales/zh-TW/chat.json @@ -17,8 +17,7 @@ "tool": "工具", "claude": "Claude", "cursor": "Cursor", - "codex": "Codex", - "gemini": "Gemini" + "codex": "Codex" }, "tools": { "settings": "工具設定", @@ -170,7 +169,6 @@ "claude": "準備好使用 {{model}} 的 Claude。請在下方開始輸入您的訊息。", "cursor": "準備好使用 {{model}} 的 Cursor。請在下方開始輸入您的訊息。", "codex": "準備好使用 {{model}} 的 Codex。請在下方開始輸入您的訊息。", - "gemini": "準備好使用 {{model}} 的 Gemini。請在下方開始輸入您的訊息。", "default": "請在上方選擇一個提供者以開始" }, "pressToSearch": "按 {{shortcut}} 搜尋工作階段、檔案和提交" diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index 1f1fe22f..5fffcd68 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -323,9 +323,6 @@ }, "codex": { "description": "OpenAI Codex AI 助手" - }, - "gemini": { - "description": "Google Gemini AI 助手" } }, "connectionStatus": "連線狀態", diff --git a/src/types/app.ts b/src/types/app.ts index b2ea97c4..8fa4a684 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -1,4 +1,4 @@ -export type LLMProvider = 'claude' | 'cursor' | 'codex' | 'gemini' | 'opencode'; +export type LLMProvider = 'claude' | 'cursor' | 'codex' | 'opencode'; export type ProviderModelOption = { value: string;